Add API 31 sources
Test: None
Change-Id: Ie45894f7a232b2a15e2439b2527ca1813f334cc5
diff --git a/android/view/AbsSavedState.java b/android/view/AbsSavedState.java
new file mode 100644
index 0000000..009898e
--- /dev/null
+++ b/android/view/AbsSavedState.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A {@link Parcelable} implementation that should be used by inheritance
+ * hierarchies to ensure the state of all classes along the chain is saved.
+ */
+public abstract class AbsSavedState implements Parcelable {
+ public static final AbsSavedState EMPTY_STATE = new AbsSavedState() {};
+
+ private final Parcelable mSuperState;
+
+ /**
+ * Constructor used to make the EMPTY_STATE singleton
+ */
+ private AbsSavedState() {
+ mSuperState = null;
+ }
+
+ /**
+ * Constructor called by derived classes when creating their SavedState objects
+ *
+ * @param superState The state of the superclass of this view
+ */
+ protected AbsSavedState(Parcelable superState) {
+ if (superState == null) {
+ throw new IllegalArgumentException("superState must not be null");
+ }
+ mSuperState = superState != EMPTY_STATE ? superState : null;
+ }
+
+ /**
+ * Constructor used when reading from a parcel. Reads the state of the superclass.
+ *
+ * @param source parcel to read from
+ */
+ protected AbsSavedState(Parcel source) {
+ this(source, null);
+ }
+
+ /**
+ * Constructor used when reading from a parcel using a given class loader.
+ * Reads the state of the superclass.
+ *
+ * @param source parcel to read from
+ * @param loader ClassLoader to use for reading
+ */
+ protected AbsSavedState(Parcel source, ClassLoader loader) {
+ Parcelable superState = source.readParcelable(loader);
+ mSuperState = superState != null ? superState : EMPTY_STATE;
+ }
+
+ final public Parcelable getSuperState() {
+ return mSuperState;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mSuperState, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AbsSavedState> CREATOR
+ = new Parcelable.ClassLoaderCreator<AbsSavedState>() {
+
+ @Override
+ public AbsSavedState createFromParcel(Parcel in) {
+ return createFromParcel(in, null);
+ }
+
+ @Override
+ public AbsSavedState createFromParcel(Parcel in, ClassLoader loader) {
+ Parcelable superState = in.readParcelable(loader);
+ if (superState != null) {
+ throw new IllegalStateException("superState must be null");
+ }
+ return EMPTY_STATE;
+ }
+
+ @Override
+ public AbsSavedState[] newArray(int size) {
+ return new AbsSavedState[size];
+ }
+ };
+}
diff --git a/android/view/AccessibilityEmbeddedConnection.java b/android/view/AccessibilityEmbeddedConnection.java
new file mode 100644
index 0000000..5d34669
--- /dev/null
+++ b/android/view/AccessibilityEmbeddedConnection.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.os.IBinder;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityEmbeddedConnection;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This class is an interface this ViewRootImpl provides to the host view to the latter
+ * can interact with the view hierarchy in SurfaceControlViewHost.
+ *
+ * @hide
+ */
+final class AccessibilityEmbeddedConnection extends IAccessibilityEmbeddedConnection.Stub {
+ private final WeakReference<ViewRootImpl> mViewRootImpl;
+ private final Matrix mTmpScreenMatrix = new Matrix();
+
+ AccessibilityEmbeddedConnection(ViewRootImpl viewRootImpl) {
+ mViewRootImpl = new WeakReference<>(viewRootImpl);
+ }
+
+ @Override
+ public @Nullable IBinder associateEmbeddedHierarchy(@NonNull IBinder host, int hostViewId) {
+ final ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null) {
+ final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
+ viewRootImpl.mContext);
+ viewRootImpl.mAttachInfo.mLeashedParentToken = host;
+ viewRootImpl.mAttachInfo.mLeashedParentAccessibilityViewId = hostViewId;
+ if (accessibilityManager.isEnabled()) {
+ accessibilityManager.associateEmbeddedHierarchy(host, viewRootImpl.mLeashToken);
+ }
+ return viewRootImpl.mLeashToken;
+ }
+ return null;
+ }
+
+ @Override
+ public void disassociateEmbeddedHierarchy() {
+ final ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null) {
+ final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
+ viewRootImpl.mContext);
+ viewRootImpl.mAttachInfo.mLeashedParentToken = null;
+ viewRootImpl.mAttachInfo.mLeashedParentAccessibilityViewId = View.NO_ID;
+ viewRootImpl.mAttachInfo.mLocationInParentDisplay.set(0, 0);
+ if (accessibilityManager.isEnabled()) {
+ accessibilityManager.disassociateEmbeddedHierarchy(viewRootImpl.mLeashToken);
+ }
+ }
+ }
+
+ @Override
+ public void setScreenMatrix(float[] matrixValues) {
+ final ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null) {
+ mTmpScreenMatrix.setValues(matrixValues);
+ if (viewRootImpl.mAttachInfo.mScreenMatrixInEmbeddedHierarchy == null) {
+ viewRootImpl.mAttachInfo.mScreenMatrixInEmbeddedHierarchy = new Matrix();
+ }
+ viewRootImpl.mAttachInfo.mScreenMatrixInEmbeddedHierarchy.set(mTmpScreenMatrix);
+ }
+ }
+}
diff --git a/android/view/AccessibilityInteractionController.java b/android/view/AccessibilityInteractionController.java
new file mode 100644
index 0000000..199ecb3
--- /dev/null
+++ b/android/view/AccessibilityInteractionController.java
@@ -0,0 +1,1642 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_REQUESTED_KEY;
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
+
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.style.AccessibilityClickableSpan;
+import android.text.style.ClickableSpan;
+import android.util.LongSparseArray;
+import android.util.Slog;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeIdManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityRequestPreparer;
+import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.function.Predicate;
+
+/**
+ * Class for managing accessibility interactions initiated from the system
+ * and targeting the view hierarchy. A *ClientThread method is to be
+ * called from the interaction connection ViewAncestor gives the system to
+ * talk to it and a corresponding *UiThread method that is executed on the
+ * UI thread.
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class AccessibilityInteractionController {
+
+ private static final String LOG_TAG = "AccessibilityInteractionController";
+
+ // Debugging flag
+ private static final boolean ENFORCE_NODE_TREE_CONSISTENT = false;
+
+ // Constants for readability
+ private static final boolean IGNORE_REQUEST_PREPARERS = true;
+ private static final boolean CONSIDER_REQUEST_PREPARERS = false;
+
+ // If an app holds off accessibility for longer than this, the hold-off is canceled to prevent
+ // accessibility from hanging
+ private static final long REQUEST_PREPARER_TIMEOUT_MS = 500;
+
+ // Callbacks should have the same configuration of the flags below to allow satisfying a pending
+ // node request on prefetch
+ private static final int FLAGS_AFFECTING_REPORTED_DATA =
+ AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ | AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
+
+ private final ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList =
+ new ArrayList<AccessibilityNodeInfo>();
+
+ private final Object mLock = new Object();
+
+ private final PrivateHandler mHandler;
+
+ private final ViewRootImpl mViewRootImpl;
+
+ private final AccessibilityNodePrefetcher mPrefetcher;
+
+ private final long mMyLooperThreadId;
+
+ private final int mMyProcessId;
+
+ private final AccessibilityManager mA11yManager;
+
+ private final ArrayList<View> mTempArrayList = new ArrayList<View>();
+
+ private final Point mTempPoint = new Point();
+ private final Rect mTempRect = new Rect();
+ private final Rect mTempRect1 = new Rect();
+ private final Rect mTempRect2 = new Rect();
+ private final RectF mTempRectF = new RectF();
+
+ private AddNodeInfosForViewId mAddNodeInfosForViewId;
+
+ @GuardedBy("mLock")
+ private ArrayList<Message> mPendingFindNodeByIdMessages;
+
+ @GuardedBy("mLock")
+ private int mNumActiveRequestPreparers;
+ @GuardedBy("mLock")
+ private List<MessageHolder> mMessagesWaitingForRequestPreparer;
+ @GuardedBy("mLock")
+ private int mActiveRequestPreparerId;
+
+ public AccessibilityInteractionController(ViewRootImpl viewRootImpl) {
+ Looper looper = viewRootImpl.mHandler.getLooper();
+ mMyLooperThreadId = looper.getThread().getId();
+ mMyProcessId = Process.myPid();
+ mHandler = new PrivateHandler(looper);
+ mViewRootImpl = viewRootImpl;
+ mPrefetcher = new AccessibilityNodePrefetcher();
+ mA11yManager = mViewRootImpl.mContext.getSystemService(AccessibilityManager.class);
+ mPendingFindNodeByIdMessages = new ArrayList<>();
+ }
+
+ private void scheduleMessage(Message message, int interrogatingPid, long interrogatingTid,
+ boolean ignoreRequestPreparers) {
+ if (ignoreRequestPreparers
+ || !holdOffMessageIfNeeded(message, interrogatingPid, interrogatingTid)) {
+ // If the interrogation is performed by the same thread as the main UI
+ // thread in this process, set the message as a static reference so
+ // after this call completes the same thread but in the interrogating
+ // client can handle the message to generate the result.
+ if (interrogatingPid == mMyProcessId && interrogatingTid == mMyLooperThreadId
+ && mHandler.hasAccessibilityCallback(message)) {
+ AccessibilityInteractionClient.getInstanceForThread(
+ interrogatingTid).setSameThreadMessage(message);
+ } else {
+ // For messages without callback of interrogating client, just handle the
+ // message immediately if this is UI thread.
+ if (!mHandler.hasAccessibilityCallback(message)
+ && Thread.currentThread().getId() == mMyLooperThreadId) {
+ mHandler.handleMessage(message);
+ } else {
+ mHandler.sendMessage(message);
+ }
+ }
+ }
+ }
+
+ private boolean isShown(View view) {
+ return (view != null) && (view.getWindowVisibility() == View.VISIBLE && view.isShown());
+ }
+
+ public void findAccessibilityNodeInfoByAccessibilityIdClientThread(
+ long accessibilityNodeId, Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
+ long interrogatingTid, MagnificationSpec spec, Bundle arguments) {
+ final Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID;
+ message.arg1 = flags;
+
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.argi3 = interactionId;
+ args.arg1 = callback;
+ args.arg2 = spec;
+ args.arg3 = interactiveRegion;
+ args.arg4 = arguments;
+ message.obj = args;
+
+ synchronized (mLock) {
+ mPendingFindNodeByIdMessages.add(message);
+ scheduleMessage(message, interrogatingPid, interrogatingTid,
+ CONSIDER_REQUEST_PREPARERS);
+ }
+ }
+
+ /**
+ * Check if this message needs to be held off while the app prepares to meet either this
+ * request, or a request ahead of it.
+ *
+ * @param originalMessage The message to be processed
+ * @param callingPid The calling process id
+ * @param callingTid The calling thread id
+ *
+ * @return {@code true} if the message is held off and will be processed later, {@code false} if
+ * the message should be posted.
+ */
+ private boolean holdOffMessageIfNeeded(
+ Message originalMessage, int callingPid, long callingTid) {
+ synchronized (mLock) {
+ // If a request is already pending, queue this request for when it's finished
+ if (mNumActiveRequestPreparers != 0) {
+ queueMessageToHandleOncePrepared(originalMessage, callingPid, callingTid);
+ return true;
+ }
+
+ // Currently the only message that can hold things off is findByA11yId with extra data.
+ if (originalMessage.what
+ != PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID) {
+ return false;
+ }
+ SomeArgs originalMessageArgs = (SomeArgs) originalMessage.obj;
+ Bundle requestArguments = (Bundle) originalMessageArgs.arg4;
+ if (requestArguments == null) {
+ return false;
+ }
+
+ // If nothing it registered for this view, nothing to do
+ int accessibilityViewId = originalMessageArgs.argi1;
+ final List<AccessibilityRequestPreparer> preparers =
+ mA11yManager.getRequestPreparersForAccessibilityId(accessibilityViewId);
+ if (preparers == null) {
+ return false;
+ }
+
+ // If the bundle doesn't request the extra data, nothing to do
+ final String extraDataKey = requestArguments.getString(EXTRA_DATA_REQUESTED_KEY);
+ if (extraDataKey == null) {
+ return false;
+ }
+
+ // Send the request to the AccessibilityRequestPreparers on the UI thread
+ mNumActiveRequestPreparers = preparers.size();
+ for (int i = 0; i < preparers.size(); i++) {
+ final Message requestPreparerMessage = mHandler.obtainMessage(
+ PrivateHandler.MSG_PREPARE_FOR_EXTRA_DATA_REQUEST);
+ final SomeArgs requestPreparerArgs = SomeArgs.obtain();
+ // virtualDescendentId
+ requestPreparerArgs.argi1 =
+ (originalMessageArgs.argi2 == AccessibilityNodeInfo.UNDEFINED_ITEM_ID)
+ ? AccessibilityNodeProvider.HOST_VIEW_ID : originalMessageArgs.argi2;
+ requestPreparerArgs.arg1 = preparers.get(i);
+ requestPreparerArgs.arg2 = extraDataKey;
+ requestPreparerArgs.arg3 = requestArguments;
+ Message preparationFinishedMessage = mHandler.obtainMessage(
+ PrivateHandler.MSG_APP_PREPARATION_FINISHED);
+ preparationFinishedMessage.arg1 = ++mActiveRequestPreparerId;
+ requestPreparerArgs.arg4 = preparationFinishedMessage;
+
+ requestPreparerMessage.obj = requestPreparerArgs;
+ scheduleMessage(requestPreparerMessage, callingPid, callingTid,
+ IGNORE_REQUEST_PREPARERS);
+ mHandler.obtainMessage(PrivateHandler.MSG_APP_PREPARATION_TIMEOUT);
+ mHandler.sendEmptyMessageDelayed(PrivateHandler.MSG_APP_PREPARATION_TIMEOUT,
+ REQUEST_PREPARER_TIMEOUT_MS);
+ }
+
+ // Set the initial request aside
+ queueMessageToHandleOncePrepared(originalMessage, callingPid, callingTid);
+ return true;
+ }
+ }
+
+ private void prepareForExtraDataRequestUiThread(Message message) {
+ SomeArgs args = (SomeArgs) message.obj;
+ final int virtualDescendantId = args.argi1;
+ final AccessibilityRequestPreparer preparer = (AccessibilityRequestPreparer) args.arg1;
+ final String extraDataKey = (String) args.arg2;
+ final Bundle requestArguments = (Bundle) args.arg3;
+ final Message preparationFinishedMessage = (Message) args.arg4;
+
+ preparer.onPrepareExtraData(virtualDescendantId, extraDataKey,
+ requestArguments, preparationFinishedMessage);
+ }
+
+ private void queueMessageToHandleOncePrepared(Message message, int interrogatingPid,
+ long interrogatingTid) {
+ if (mMessagesWaitingForRequestPreparer == null) {
+ mMessagesWaitingForRequestPreparer = new ArrayList<>(1);
+ }
+ MessageHolder messageHolder =
+ new MessageHolder(message, interrogatingPid, interrogatingTid);
+ mMessagesWaitingForRequestPreparer.add(messageHolder);
+ }
+
+ private void requestPreparerDoneUiThread(Message message) {
+ synchronized (mLock) {
+ if (message.arg1 != mActiveRequestPreparerId) {
+ Slog.e(LOG_TAG, "Surprising AccessibilityRequestPreparer callback (likely late)");
+ return;
+ }
+ mNumActiveRequestPreparers--;
+ if (mNumActiveRequestPreparers <= 0) {
+ mHandler.removeMessages(PrivateHandler.MSG_APP_PREPARATION_TIMEOUT);
+ scheduleAllMessagesWaitingForRequestPreparerLocked();
+ }
+ }
+ }
+
+ private void requestPreparerTimeoutUiThread() {
+ synchronized (mLock) {
+ Slog.e(LOG_TAG, "AccessibilityRequestPreparer timed out");
+ scheduleAllMessagesWaitingForRequestPreparerLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleAllMessagesWaitingForRequestPreparerLocked() {
+ int numMessages = mMessagesWaitingForRequestPreparer.size();
+ for (int i = 0; i < numMessages; i++) {
+ MessageHolder request = mMessagesWaitingForRequestPreparer.get(i);
+ scheduleMessage(request.mMessage, request.mInterrogatingPid,
+ request.mInterrogatingTid,
+ (i == 0) /* the app is ready for the first request */);
+ }
+ mMessagesWaitingForRequestPreparer.clear();
+ mNumActiveRequestPreparers = 0; // Just to be safe - should be unnecessary
+ mActiveRequestPreparerId = -1;
+ }
+
+ private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
+ synchronized (mLock) {
+ mPendingFindNodeByIdMessages.remove(message);
+ }
+ final int flags = message.arg1;
+
+ SomeArgs args = (SomeArgs) message.obj;
+ final int accessibilityViewId = args.argi1;
+ final int virtualDescendantId = args.argi2;
+ final int interactionId = args.argi3;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ final MagnificationSpec spec = (MagnificationSpec) args.arg2;
+ final Region interactiveRegion = (Region) args.arg3;
+ final Bundle arguments = (Bundle) args.arg4;
+
+ args.recycle();
+
+ View requestedView = null;
+ AccessibilityNodeInfo requestedNode = null;
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
+ return;
+ }
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
+ requestedView = findViewByAccessibilityId(accessibilityViewId);
+ if (requestedView != null && isShown(requestedView)) {
+ requestedNode = populateAccessibilityNodeInfoForView(
+ requestedView, arguments, virtualDescendantId);
+ }
+ } finally {
+ updateInfoForViewportAndReturnFindNodeResult(
+ requestedNode == null ? null : AccessibilityNodeInfo.obtain(requestedNode),
+ callback, interactionId, spec, interactiveRegion);
+ }
+ ArrayList<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
+ infos.clear();
+ mPrefetcher.prefetchAccessibilityNodeInfos(requestedView,
+ requestedNode == null ? null : AccessibilityNodeInfo.obtain(requestedNode),
+ virtualDescendantId, flags, infos);
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ updateInfosForViewPort(infos, spec, interactiveRegion);
+ final SatisfiedFindAccessibilityNodeByAccessibilityIdRequest satisfiedRequest =
+ getSatisfiedRequestInPrefetch(
+ requestedNode == null ? null : requestedNode, infos, flags);
+
+ if (satisfiedRequest != null && satisfiedRequest.mSatisfiedRequestNode != requestedNode) {
+ infos.remove(satisfiedRequest.mSatisfiedRequestNode);
+ }
+
+ returnPrefetchResult(interactionId, infos, callback);
+
+ if (satisfiedRequest != null) {
+ returnFindNodeResult(satisfiedRequest);
+ }
+ }
+
+ private AccessibilityNodeInfo populateAccessibilityNodeInfoForView(
+ View view, Bundle arguments, int virtualViewId) {
+ AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
+ // Determine if we'll be populating extra data
+ final String extraDataRequested = (arguments == null) ? null
+ : arguments.getString(EXTRA_DATA_REQUESTED_KEY);
+ AccessibilityNodeInfo root = null;
+ if (provider == null) {
+ root = view.createAccessibilityNodeInfo();
+ if (root != null) {
+ if (extraDataRequested != null) {
+ view.addExtraDataToAccessibilityNodeInfo(root, extraDataRequested, arguments);
+ }
+ }
+ } else {
+ root = provider.createAccessibilityNodeInfo(virtualViewId);
+ if (root != null) {
+ if (extraDataRequested != null) {
+ provider.addExtraDataToAccessibilityNodeInfo(
+ virtualViewId, root, extraDataRequested, arguments);
+ }
+ }
+ }
+ return root;
+ }
+
+ public void findAccessibilityNodeInfosByViewIdClientThread(long accessibilityNodeId,
+ String viewId, Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
+ long interrogatingTid, MagnificationSpec spec) {
+ Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID;
+ message.arg1 = flags;
+ message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = interactionId;
+ args.arg1 = callback;
+ args.arg2 = spec;
+ args.arg3 = viewId;
+ args.arg4 = interactiveRegion;
+ message.obj = args;
+
+ scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void findAccessibilityNodeInfosByViewIdUiThread(Message message) {
+ final int flags = message.arg1;
+ final int accessibilityViewId = message.arg2;
+
+ SomeArgs args = (SomeArgs) message.obj;
+ final int interactionId = args.argi1;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ final MagnificationSpec spec = (MagnificationSpec) args.arg2;
+ final String viewId = (String) args.arg3;
+ final Region interactiveRegion = (Region) args.arg4;
+ args.recycle();
+
+ final List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
+ infos.clear();
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null
+ || viewId == null) {
+ return;
+ }
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
+ final View root = findViewByAccessibilityId(accessibilityViewId);
+ if (root != null) {
+ final int resolvedViewId = root.getContext().getResources()
+ .getIdentifier(viewId, null, null);
+ if (resolvedViewId <= 0) {
+ return;
+ }
+ if (mAddNodeInfosForViewId == null) {
+ mAddNodeInfosForViewId = new AddNodeInfosForViewId();
+ }
+ mAddNodeInfosForViewId.init(resolvedViewId, infos);
+ root.findViewByPredicate(mAddNodeInfosForViewId);
+ mAddNodeInfosForViewId.reset();
+ }
+ } finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ updateInfosForViewportAndReturnFindNodeResult(
+ infos, callback, interactionId, spec, interactiveRegion);
+ }
+ }
+
+ public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId,
+ String text, Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
+ long interrogatingTid, MagnificationSpec spec) {
+ Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT;
+ message.arg1 = flags;
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = text;
+ args.arg2 = callback;
+ args.arg3 = spec;
+ args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.argi3 = interactionId;
+ args.arg4 = interactiveRegion;
+ message.obj = args;
+
+ scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void findAccessibilityNodeInfosByTextUiThread(Message message) {
+ final int flags = message.arg1;
+
+ SomeArgs args = (SomeArgs) message.obj;
+ final String text = (String) args.arg1;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg2;
+ final MagnificationSpec spec = (MagnificationSpec) args.arg3;
+ final int accessibilityViewId = args.argi1;
+ final int virtualDescendantId = args.argi2;
+ final int interactionId = args.argi3;
+ final Region interactiveRegion = (Region) args.arg4;
+ args.recycle();
+
+ List<AccessibilityNodeInfo> infos = null;
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
+ return;
+ }
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
+ final View root = findViewByAccessibilityId(accessibilityViewId);
+ if (root != null && isShown(root)) {
+ AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider();
+ if (provider != null) {
+ infos = provider.findAccessibilityNodeInfosByText(text,
+ virtualDescendantId);
+ } else if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ ArrayList<View> foundViews = mTempArrayList;
+ foundViews.clear();
+ root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT
+ | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION
+ | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS);
+ if (!foundViews.isEmpty()) {
+ infos = mTempAccessibilityNodeInfoList;
+ infos.clear();
+ final int viewCount = foundViews.size();
+ for (int i = 0; i < viewCount; i++) {
+ View foundView = foundViews.get(i);
+ if (isShown(foundView)) {
+ provider = foundView.getAccessibilityNodeProvider();
+ if (provider != null) {
+ List<AccessibilityNodeInfo> infosFromProvider =
+ provider.findAccessibilityNodeInfosByText(text,
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+ if (infosFromProvider != null) {
+ infos.addAll(infosFromProvider);
+ }
+ } else {
+ infos.add(foundView.createAccessibilityNodeInfo());
+ }
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ updateInfosForViewportAndReturnFindNodeResult(
+ infos, callback, interactionId, spec, interactiveRegion);
+ }
+ }
+
+ public void findFocusClientThread(long accessibilityNodeId, int focusType,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
+ long interrogatingTid, MagnificationSpec spec) {
+ Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_FIND_FOCUS;
+ message.arg1 = flags;
+ message.arg2 = focusType;
+
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = interactionId;
+ args.argi2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ args.argi3 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.arg1 = callback;
+ args.arg2 = spec;
+ args.arg3 = interactiveRegion;
+
+ message.obj = args;
+
+ scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void findFocusUiThread(Message message) {
+ final int flags = message.arg1;
+ final int focusType = message.arg2;
+
+ SomeArgs args = (SomeArgs) message.obj;
+ final int interactionId = args.argi1;
+ final int accessibilityViewId = args.argi2;
+ final int virtualDescendantId = args.argi3;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ final MagnificationSpec spec = (MagnificationSpec) args.arg2;
+ final Region interactiveRegion = (Region) args.arg3;
+ args.recycle();
+
+ AccessibilityNodeInfo focused = null;
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
+ return;
+ }
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
+ final View root = findViewByAccessibilityId(accessibilityViewId);
+ if (root != null && isShown(root)) {
+ switch (focusType) {
+ case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: {
+ View host = mViewRootImpl.mAccessibilityFocusedHost;
+ // If there is no accessibility focus host or it is not a descendant
+ // of the root from which to start the search, then the search failed.
+ if (host == null || !ViewRootImpl.isViewDescendantOf(host, root)) {
+ break;
+ }
+ // The focused view not shown, we failed.
+ if (!isShown(host)) {
+ break;
+ }
+ // If the host has a provider ask this provider to search for the
+ // focus instead fetching all provider nodes to do the search here.
+ AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();
+ if (provider != null) {
+ if (mViewRootImpl.mAccessibilityFocusedVirtualView != null) {
+ focused = AccessibilityNodeInfo.obtain(
+ mViewRootImpl.mAccessibilityFocusedVirtualView);
+ }
+ } else if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ focused = host.createAccessibilityNodeInfo();
+ }
+ } break;
+ case AccessibilityNodeInfo.FOCUS_INPUT: {
+ View target = root.findFocus();
+ if (!isShown(target)) {
+ break;
+ }
+ AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
+ if (provider != null) {
+ focused = provider.findFocus(focusType);
+ }
+ if (focused == null) {
+ focused = target.createAccessibilityNodeInfo();
+ }
+ } break;
+ default:
+ throw new IllegalArgumentException("Unknown focus type: " + focusType);
+ }
+ }
+ } finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ updateInfoForViewportAndReturnFindNodeResult(
+ focused, callback, interactionId, spec, interactiveRegion);
+ }
+ }
+
+ public void focusSearchClientThread(long accessibilityNodeId, int direction,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
+ long interrogatingTid, MagnificationSpec spec) {
+ Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_FOCUS_SEARCH;
+ message.arg1 = flags;
+ message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+
+ SomeArgs args = SomeArgs.obtain();
+ args.argi2 = direction;
+ args.argi3 = interactionId;
+ args.arg1 = callback;
+ args.arg2 = spec;
+ args.arg3 = interactiveRegion;
+
+ message.obj = args;
+
+ scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void focusSearchUiThread(Message message) {
+ final int flags = message.arg1;
+ final int accessibilityViewId = message.arg2;
+
+ SomeArgs args = (SomeArgs) message.obj;
+ final int direction = args.argi2;
+ final int interactionId = args.argi3;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ final MagnificationSpec spec = (MagnificationSpec) args.arg2;
+ final Region interactiveRegion = (Region) args.arg3;
+
+ args.recycle();
+
+ AccessibilityNodeInfo next = null;
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
+ return;
+ }
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
+ final View root = findViewByAccessibilityId(accessibilityViewId);
+ if (root != null && isShown(root)) {
+ View nextView = root.focusSearch(direction);
+ if (nextView != null) {
+ next = nextView.createAccessibilityNodeInfo();
+ }
+ }
+ } finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ updateInfoForViewportAndReturnFindNodeResult(
+ next, callback, interactionId, spec, interactiveRegion);
+ }
+ }
+
+ public void performAccessibilityActionClientThread(long accessibilityNodeId, int action,
+ Bundle arguments, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
+ long interrogatingTid) {
+ Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_PERFORM_ACCESSIBILITY_ACTION;
+ message.arg1 = flags;
+ message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.argi2 = action;
+ args.argi3 = interactionId;
+ args.arg1 = callback;
+ args.arg2 = arguments;
+
+ message.obj = args;
+
+ scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void performAccessibilityActionUiThread(Message message) {
+ final int flags = message.arg1;
+ final int accessibilityViewId = message.arg2;
+
+ SomeArgs args = (SomeArgs) message.obj;
+ final int virtualDescendantId = args.argi1;
+ final int action = args.argi2;
+ final int interactionId = args.argi3;
+ final IAccessibilityInteractionConnectionCallback callback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ Bundle arguments = (Bundle) args.arg2;
+
+ args.recycle();
+
+ boolean succeeded = false;
+ try {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null ||
+ mViewRootImpl.mStopped || mViewRootImpl.mPausedForTransition) {
+ return;
+ }
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
+ final View target = findViewByAccessibilityId(accessibilityViewId);
+ if (target != null && isShown(target)) {
+ mA11yManager.notifyPerformingAction(action);
+ if (action == R.id.accessibilityActionClickOnClickableSpan) {
+ // Handle this hidden action separately
+ succeeded = handleClickableSpanActionUiThread(
+ target, virtualDescendantId, arguments);
+ } else {
+ AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
+ if (provider != null) {
+ succeeded = provider.performAction(virtualDescendantId, action,
+ arguments);
+ } else if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ succeeded = target.performAccessibilityAction(action, arguments);
+ }
+ }
+ mA11yManager.notifyPerformingAction(0);
+ }
+ } finally {
+ try {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ callback.setPerformAccessibilityActionResult(succeeded, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - the other side will time out */
+ }
+ }
+ }
+
+ /**
+ * Finds the accessibility focused node in the root, and clears the accessibility focus.
+ */
+ public void clearAccessibilityFocusClientThread() {
+ final Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_CLEAR_ACCESSIBILITY_FOCUS;
+
+ // Don't care about pid and tid because there's no interrogating client for this message.
+ scheduleMessage(message, 0, 0, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void clearAccessibilityFocusUiThread() {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
+ return;
+ }
+ try {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags =
+ AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+ final View root = mViewRootImpl.mView;
+ if (root != null && isShown(root)) {
+ final View host = mViewRootImpl.mAccessibilityFocusedHost;
+ // If there is no accessibility focus host or it is not a descendant
+ // of the root from which to start the search, then the search failed.
+ if (host == null || !ViewRootImpl.isViewDescendantOf(host, root)) {
+ return;
+ }
+ final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();
+ final AccessibilityNodeInfo focusNode =
+ mViewRootImpl.mAccessibilityFocusedVirtualView;
+ if (provider != null && focusNode != null) {
+ final int virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId(
+ focusNode.getSourceNodeId());
+ provider.performAction(virtualNodeId,
+ AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS.getId(),
+ null);
+ } else {
+ host.performAccessibilityAction(
+ AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS.getId(),
+ null);
+ }
+ }
+ } finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ }
+ }
+
+ /**
+ * Notify outside touch event to the target window.
+ */
+ public void notifyOutsideTouchClientThread() {
+ final Message message = mHandler.obtainMessage();
+ message.what = PrivateHandler.MSG_NOTIFY_OUTSIDE_TOUCH;
+
+ // Don't care about pid and tid because there's no interrogating client for this message.
+ scheduleMessage(message, 0, 0, CONSIDER_REQUEST_PREPARERS);
+ }
+
+ private void notifyOutsideTouchUiThread() {
+ if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null
+ || mViewRootImpl.mStopped || mViewRootImpl.mPausedForTransition) {
+ return;
+ }
+ final View root = mViewRootImpl.mView;
+ if (root != null && isShown(root)) {
+ // trigger ACTION_OUTSIDE to notify windows
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_OUTSIDE,
+ 0, 0, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ mViewRootImpl.dispatchInputEvent(event);
+ }
+ }
+
+ private View findViewByAccessibilityId(int accessibilityId) {
+ if (accessibilityId == AccessibilityNodeInfo.ROOT_ITEM_ID) {
+ return mViewRootImpl.mView;
+ } else {
+ return AccessibilityNodeIdManager.getInstance().findView(accessibilityId);
+ }
+ }
+
+ private void adjustIsVisibleToUserIfNeeded(AccessibilityNodeInfo info,
+ Region interactiveRegion) {
+ if (interactiveRegion == null || info == null) {
+ return;
+ }
+ Rect boundsInScreen = mTempRect;
+ info.getBoundsInScreen(boundsInScreen);
+ if (interactiveRegion.quickReject(boundsInScreen) && !shouldBypassAdjustIsVisible()) {
+ info.setVisibleToUser(false);
+ }
+ }
+
+ private boolean shouldBypassAdjustIsVisible() {
+ final int windowType = mViewRootImpl.mOrigWindowType;
+ if (windowType == TYPE_INPUT_METHOD) {
+ return true;
+ }
+ return false;
+ }
+
+ private void adjustBoundsInScreenIfNeeded(AccessibilityNodeInfo info) {
+ if (info == null || shouldBypassAdjustBoundsInScreen()) {
+ return;
+ }
+ final Rect boundsInScreen = mTempRect;
+ info.getBoundsInScreen(boundsInScreen);
+ boundsInScreen.offset(mViewRootImpl.mAttachInfo.mLocationInParentDisplay.x,
+ mViewRootImpl.mAttachInfo.mLocationInParentDisplay.y);
+ info.setBoundsInScreen(boundsInScreen);
+ }
+
+ private boolean shouldBypassAdjustBoundsInScreen() {
+ return mViewRootImpl.mAttachInfo.mLocationInParentDisplay.equals(0, 0);
+ }
+
+ private void applyScreenMatrixIfNeeded(List<AccessibilityNodeInfo> infos) {
+ if (infos == null || shouldBypassApplyScreenMatrix()) {
+ return;
+ }
+ final int infoCount = infos.size();
+ for (int i = 0; i < infoCount; i++) {
+ final AccessibilityNodeInfo info = infos.get(i);
+ applyScreenMatrixIfNeeded(info);
+ }
+ }
+
+ private void applyScreenMatrixIfNeeded(AccessibilityNodeInfo info) {
+ if (info == null || shouldBypassApplyScreenMatrix()) {
+ return;
+ }
+ final Rect boundsInScreen = mTempRect;
+ final RectF transformedBounds = mTempRectF;
+ final Matrix screenMatrix = mViewRootImpl.mAttachInfo.mScreenMatrixInEmbeddedHierarchy;
+
+ info.getBoundsInScreen(boundsInScreen);
+ transformedBounds.set(boundsInScreen);
+ screenMatrix.mapRect(transformedBounds);
+ boundsInScreen.set((int) transformedBounds.left, (int) transformedBounds.top,
+ (int) transformedBounds.right, (int) transformedBounds.bottom);
+ info.setBoundsInScreen(boundsInScreen);
+ }
+
+ private boolean shouldBypassApplyScreenMatrix() {
+ final Matrix screenMatrix = mViewRootImpl.mAttachInfo.mScreenMatrixInEmbeddedHierarchy;
+ return screenMatrix == null || screenMatrix.isIdentity();
+ }
+
+ private void associateLeashedParentIfNeeded(AccessibilityNodeInfo info) {
+ if (info == null || shouldBypassAssociateLeashedParent()) {
+ return;
+ }
+ // The node id of root node in embedded maybe not be ROOT_NODE_ID so we compare the id
+ // with root view.
+ if (mViewRootImpl.mView.getAccessibilityViewId()
+ != AccessibilityNodeInfo.getAccessibilityViewId(info.getSourceNodeId())) {
+ return;
+ }
+ info.setLeashedParent(mViewRootImpl.mAttachInfo.mLeashedParentToken,
+ mViewRootImpl.mAttachInfo.mLeashedParentAccessibilityViewId);
+ }
+
+ private boolean shouldBypassAssociateLeashedParent() {
+ return (mViewRootImpl.mAttachInfo.mLeashedParentToken == null
+ && mViewRootImpl.mAttachInfo.mLeashedParentAccessibilityViewId == View.NO_ID);
+ }
+
+ private void applyAppScaleAndMagnificationSpecIfNeeded(AccessibilityNodeInfo info,
+ MagnificationSpec spec) {
+ if (info == null) {
+ return;
+ }
+
+ final float applicationScale = mViewRootImpl.mAttachInfo.mApplicationScale;
+ if (!shouldApplyAppScaleAndMagnificationSpec(applicationScale, spec)) {
+ return;
+ }
+
+ Rect boundsInParent = mTempRect;
+ Rect boundsInScreen = mTempRect1;
+
+ info.getBoundsInParent(boundsInParent);
+ info.getBoundsInScreen(boundsInScreen);
+ if (applicationScale != 1.0f) {
+ boundsInParent.scale(applicationScale);
+ boundsInScreen.scale(applicationScale);
+ }
+ if (spec != null) {
+ boundsInParent.scale(spec.scale);
+ // boundsInParent must not be offset.
+ boundsInScreen.scale(spec.scale);
+ boundsInScreen.offset((int) spec.offsetX, (int) spec.offsetY);
+ }
+ info.setBoundsInParent(boundsInParent);
+ info.setBoundsInScreen(boundsInScreen);
+
+ // Scale text locations if they are present
+ if (info.hasExtras()) {
+ Bundle extras = info.getExtras();
+ Parcelable[] textLocations =
+ extras.getParcelableArray(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY);
+ if (textLocations != null) {
+ for (int i = 0; i < textLocations.length; i++) {
+ // Unchecked cast - an app that puts other objects in this bundle with this
+ // key will crash.
+ RectF textLocation = ((RectF) textLocations[i]);
+ textLocation.scale(applicationScale);
+ if (spec != null) {
+ textLocation.scale(spec.scale);
+ textLocation.offset(spec.offsetX, spec.offsetY);
+ }
+ }
+ }
+ }
+ }
+
+ private boolean shouldApplyAppScaleAndMagnificationSpec(float appScale,
+ MagnificationSpec spec) {
+ return (appScale != 1.0f || (spec != null && !spec.isNop()));
+ }
+
+ private void updateInfosForViewPort(List<AccessibilityNodeInfo> infos, MagnificationSpec spec,
+ Region interactiveRegion) {
+ for (int i = 0; i < infos.size(); i++) {
+ updateInfoForViewPort(infos.get(i), spec, interactiveRegion);
+ }
+ }
+
+ private void updateInfoForViewPort(AccessibilityNodeInfo info, MagnificationSpec spec,
+ Region interactiveRegion) {
+ associateLeashedParentIfNeeded(info);
+ applyScreenMatrixIfNeeded(info);
+ adjustBoundsInScreenIfNeeded(info);
+ // To avoid applyAppScaleAndMagnificationSpecIfNeeded changing the bounds of node,
+ // then impact the visibility result, we need to adjust visibility before apply scale.
+ adjustIsVisibleToUserIfNeeded(info, interactiveRegion);
+ applyAppScaleAndMagnificationSpecIfNeeded(info, spec);
+ }
+
+ private void updateInfosForViewportAndReturnFindNodeResult(List<AccessibilityNodeInfo> infos,
+ IAccessibilityInteractionConnectionCallback callback, int interactionId,
+ MagnificationSpec spec, Region interactiveRegion) {
+ if (infos != null) {
+ updateInfosForViewPort(infos, spec, interactiveRegion);
+ }
+ returnFindNodesResult(infos, callback, interactionId);
+ }
+
+ private void returnFindNodeResult(AccessibilityNodeInfo info,
+ IAccessibilityInteractionConnectionCallback callback,
+ int interactionId) {
+ try {
+ callback.setFindAccessibilityNodeInfoResult(info, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - the other side will time out */
+ }
+ }
+
+ private void returnFindNodeResult(SatisfiedFindAccessibilityNodeByAccessibilityIdRequest
+ satisfiedRequest) {
+ try {
+ final AccessibilityNodeInfo info = satisfiedRequest.mSatisfiedRequestNode;
+ final IAccessibilityInteractionConnectionCallback callback =
+ satisfiedRequest.mSatisfiedRequestCallback;
+ final int interactionId = satisfiedRequest.mSatisfiedRequestInteractionId;
+ callback.setFindAccessibilityNodeInfoResult(info, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - the other side will time out */
+ }
+ }
+
+ private void returnFindNodesResult(List<AccessibilityNodeInfo> infos,
+ IAccessibilityInteractionConnectionCallback callback, int interactionId) {
+ try {
+ callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
+ if (infos != null) {
+ infos.clear();
+ }
+ } catch (RemoteException re) {
+ /* ignore - the other side will time out */
+ }
+ }
+
+ private SatisfiedFindAccessibilityNodeByAccessibilityIdRequest getSatisfiedRequestInPrefetch(
+ AccessibilityNodeInfo requestedNode, List<AccessibilityNodeInfo> infos, int flags) {
+ SatisfiedFindAccessibilityNodeByAccessibilityIdRequest satisfiedRequest = null;
+ synchronized (mLock) {
+ for (int i = 0; i < mPendingFindNodeByIdMessages.size(); i++) {
+ final Message pendingMessage = mPendingFindNodeByIdMessages.get(i);
+ final int pendingFlags = pendingMessage.arg1;
+ if ((pendingFlags & FLAGS_AFFECTING_REPORTED_DATA)
+ != (flags & FLAGS_AFFECTING_REPORTED_DATA)) {
+ continue;
+ }
+ SomeArgs args = (SomeArgs) pendingMessage.obj;
+ final int accessibilityViewId = args.argi1;
+ final int virtualDescendantId = args.argi2;
+
+ final AccessibilityNodeInfo satisfiedRequestNode = nodeWithIdFromList(requestedNode,
+ infos, AccessibilityNodeInfo.makeNodeId(
+ accessibilityViewId, virtualDescendantId));
+
+ if (satisfiedRequestNode != null) {
+ mHandler.removeMessages(
+ PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID,
+ pendingMessage.obj);
+ final IAccessibilityInteractionConnectionCallback satisfiedRequestCallback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ final int satisfiedRequestInteractionId = args.argi3;
+ satisfiedRequest = new SatisfiedFindAccessibilityNodeByAccessibilityIdRequest(
+ satisfiedRequestNode, satisfiedRequestCallback,
+ satisfiedRequestInteractionId);
+ args.recycle();
+ break;
+ }
+ }
+ mPendingFindNodeByIdMessages.clear();
+ return satisfiedRequest;
+ }
+ }
+
+ private AccessibilityNodeInfo nodeWithIdFromList(AccessibilityNodeInfo requestedNode,
+ List<AccessibilityNodeInfo> infos, long nodeId) {
+ if (requestedNode != null && requestedNode.getSourceNodeId() == nodeId) {
+ return requestedNode;
+ }
+ for (int j = 0; j < infos.size(); j++) {
+ AccessibilityNodeInfo info = infos.get(j);
+ if (info.getSourceNodeId() == nodeId) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ private void returnPrefetchResult(int interactionId, List<AccessibilityNodeInfo> infos,
+ IAccessibilityInteractionConnectionCallback callback) {
+ if (infos.size() > 0) {
+ try {
+ callback.setPrefetchAccessibilityNodeInfoResult(infos, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - other side isn't too bothered if this doesn't arrive */
+ }
+ }
+ }
+
+ private void updateInfoForViewportAndReturnFindNodeResult(AccessibilityNodeInfo info,
+ IAccessibilityInteractionConnectionCallback callback, int interactionId,
+ MagnificationSpec spec, Region interactiveRegion) {
+ updateInfoForViewPort(info, spec, interactiveRegion);
+ returnFindNodeResult(info, callback, interactionId);
+ }
+
+ private boolean handleClickableSpanActionUiThread(
+ View view, int virtualDescendantId, Bundle arguments) {
+ Parcelable span = arguments.getParcelable(ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN);
+ if (!(span instanceof AccessibilityClickableSpan)) {
+ return false;
+ }
+
+ // Find the original ClickableSpan if it's still on the screen
+ AccessibilityNodeInfo infoWithSpan = null;
+ AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
+ if (provider != null) {
+ infoWithSpan = provider.createAccessibilityNodeInfo(virtualDescendantId);
+ } else if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ infoWithSpan = view.createAccessibilityNodeInfo();
+ }
+ if (infoWithSpan == null) {
+ return false;
+ }
+
+ // Click on the corresponding span
+ ClickableSpan clickableSpan = ((AccessibilityClickableSpan) span).findClickableSpan(
+ infoWithSpan.getOriginalText());
+ if (clickableSpan != null) {
+ clickableSpan.onClick(view);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This class encapsulates a prefetching strategy for the accessibility APIs for
+ * querying window content. It is responsible to prefetch a batch of
+ * AccessibilityNodeInfos in addition to the one for a requested node.
+ */
+ private class AccessibilityNodePrefetcher {
+
+ private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50;
+
+ private final ArrayList<View> mTempViewList = new ArrayList<View>();
+
+ public void prefetchAccessibilityNodeInfos(View view, AccessibilityNodeInfo root,
+ int virtualViewId, int fetchFlags, List<AccessibilityNodeInfo> outInfos) {
+ if (root == null) {
+ return;
+ }
+ AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
+ if (provider == null) {
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
+ prefetchPredecessorsOfRealNode(view, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
+ prefetchSiblingsOfRealNode(view, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
+ prefetchDescendantsOfRealNode(view, outInfos);
+ }
+ } else {
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
+ prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
+ prefetchSiblingsOfVirtualNode(root, view, provider, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
+ prefetchDescendantsOfVirtualNode(root, provider, outInfos);
+ }
+ }
+ if (ENFORCE_NODE_TREE_CONSISTENT) {
+ enforceNodeTreeConsistent(root, outInfos);
+ }
+ }
+
+ private boolean shouldStopPrefetching(List prefetchededInfos) {
+ return mHandler.hasUserInteractiveMessagesWaiting()
+ || prefetchededInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE;
+ }
+
+ private void enforceNodeTreeConsistent(
+ AccessibilityNodeInfo root, List<AccessibilityNodeInfo> nodes) {
+ LongSparseArray<AccessibilityNodeInfo> nodeMap =
+ new LongSparseArray<AccessibilityNodeInfo>();
+ final int nodeCount = nodes.size();
+ for (int i = 0; i < nodeCount; i++) {
+ AccessibilityNodeInfo node = nodes.get(i);
+ nodeMap.put(node.getSourceNodeId(), node);
+ }
+
+ // If the nodes are a tree it does not matter from
+ // which node we start to search for the root.
+ AccessibilityNodeInfo parent = root;
+ while (parent != null) {
+ root = parent;
+ parent = nodeMap.get(parent.getParentNodeId());
+ }
+
+ // Traverse the tree and do some checks.
+ AccessibilityNodeInfo accessFocus = null;
+ AccessibilityNodeInfo inputFocus = null;
+ HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
+ Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
+ fringe.add(root);
+
+ while (!fringe.isEmpty()) {
+ AccessibilityNodeInfo current = fringe.poll();
+
+ // Check for duplicates
+ if (!seen.add(current)) {
+ throw new IllegalStateException("Duplicate node: "
+ + current + " in window:"
+ + mViewRootImpl.mAttachInfo.mAccessibilityWindowId);
+ }
+
+ // Check for one accessibility focus.
+ if (current.isAccessibilityFocused()) {
+ if (accessFocus != null) {
+ throw new IllegalStateException("Duplicate accessibility focus:"
+ + current
+ + " in window:" + mViewRootImpl.mAttachInfo.mAccessibilityWindowId);
+ } else {
+ accessFocus = current;
+ }
+ }
+
+ // Check for one input focus.
+ if (current.isFocused()) {
+ if (inputFocus != null) {
+ throw new IllegalStateException("Duplicate input focus: "
+ + current + " in window:"
+ + mViewRootImpl.mAttachInfo.mAccessibilityWindowId);
+ } else {
+ inputFocus = current;
+ }
+ }
+
+ final int childCount = current.getChildCount();
+ for (int j = 0; j < childCount; j++) {
+ final long childId = current.getChildId(j);
+ final AccessibilityNodeInfo child = nodeMap.get(childId);
+ if (child != null) {
+ fringe.add(child);
+ }
+ }
+ }
+
+ // Check for disconnected nodes.
+ for (int j = nodeMap.size() - 1; j >= 0; j--) {
+ AccessibilityNodeInfo info = nodeMap.valueAt(j);
+ if (!seen.contains(info)) {
+ throw new IllegalStateException("Disconnected node: " + info);
+ }
+ }
+ }
+
+ private void prefetchPredecessorsOfRealNode(View view,
+ List<AccessibilityNodeInfo> outInfos) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ ViewParent parent = view.getParentForAccessibility();
+ while (parent instanceof View && !shouldStopPrefetching(outInfos)) {
+ View parentView = (View) parent;
+ AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo();
+ if (info != null) {
+ outInfos.add(info);
+ }
+ parent = parent.getParentForAccessibility();
+ }
+ }
+
+ private void prefetchSiblingsOfRealNode(View current,
+ List<AccessibilityNodeInfo> outInfos) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ ViewParent parent = current.getParentForAccessibility();
+ if (parent instanceof ViewGroup) {
+ ViewGroup parentGroup = (ViewGroup) parent;
+ ArrayList<View> children = mTempViewList;
+ children.clear();
+ try {
+ parentGroup.addChildrenForAccessibility(children);
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ View child = children.get(i);
+ if (child.getAccessibilityViewId() != current.getAccessibilityViewId()
+ && isShown(child)) {
+ AccessibilityNodeInfo info = null;
+ AccessibilityNodeProvider provider =
+ child.getAccessibilityNodeProvider();
+ if (provider == null) {
+ info = child.createAccessibilityNodeInfo();
+ } else {
+ info = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+ if (info != null) {
+ outInfos.add(info);
+ }
+ }
+ }
+ } finally {
+ children.clear();
+ }
+ }
+ }
+
+ private void prefetchDescendantsOfRealNode(View root,
+ List<AccessibilityNodeInfo> outInfos) {
+ if (shouldStopPrefetching(outInfos) || !(root instanceof ViewGroup)) {
+ return;
+ }
+ HashMap<View, AccessibilityNodeInfo> addedChildren =
+ new HashMap<View, AccessibilityNodeInfo>();
+ ArrayList<View> children = mTempViewList;
+ children.clear();
+ try {
+ root.addChildrenForAccessibility(children);
+ final int childCount = children.size();
+ for (int i = 0; i < childCount; i++) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ View child = children.get(i);
+ if (isShown(child)) {
+ AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider();
+ if (provider == null) {
+ AccessibilityNodeInfo info = child.createAccessibilityNodeInfo();
+ if (info != null) {
+ outInfos.add(info);
+ addedChildren.put(child, null);
+ }
+ } else {
+ AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+ if (info != null) {
+ outInfos.add(info);
+ addedChildren.put(child, info);
+ }
+ }
+ }
+ }
+ } finally {
+ children.clear();
+ }
+ if (!shouldStopPrefetching(outInfos)) {
+ for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) {
+ View addedChild = entry.getKey();
+ AccessibilityNodeInfo virtualRoot = entry.getValue();
+ if (virtualRoot == null) {
+ prefetchDescendantsOfRealNode(addedChild, outInfos);
+ } else {
+ AccessibilityNodeProvider provider =
+ addedChild.getAccessibilityNodeProvider();
+ prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos);
+ }
+ }
+ }
+ }
+
+ private void prefetchPredecessorsOfVirtualNode(AccessibilityNodeInfo root,
+ View providerHost, AccessibilityNodeProvider provider,
+ List<AccessibilityNodeInfo> outInfos) {
+ final int initialResultSize = outInfos.size();
+ long parentNodeId = root.getParentNodeId();
+ int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId);
+ while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ final int virtualDescendantId =
+ AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId);
+ if (virtualDescendantId != AccessibilityNodeProvider.HOST_VIEW_ID
+ || accessibilityViewId == providerHost.getAccessibilityViewId()) {
+ final AccessibilityNodeInfo parent;
+ parent = provider.createAccessibilityNodeInfo(virtualDescendantId);
+ if (parent == null) {
+ // Going up the parent relation we found a null predecessor,
+ // so remove these disconnected nodes form the result.
+ final int currentResultSize = outInfos.size();
+ for (int i = currentResultSize - 1; i >= initialResultSize; i--) {
+ outInfos.remove(i);
+ }
+ // Couldn't obtain the parent, which means we have a
+ // disconnected sub-tree. Abort prefetch immediately.
+ return;
+ }
+ outInfos.add(parent);
+ parentNodeId = parent.getParentNodeId();
+ accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(
+ parentNodeId);
+ } else {
+ prefetchPredecessorsOfRealNode(providerHost, outInfos);
+ return;
+ }
+ }
+ }
+
+ private void prefetchSiblingsOfVirtualNode(AccessibilityNodeInfo current, View providerHost,
+ AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) {
+ final long parentNodeId = current.getParentNodeId();
+ final int parentAccessibilityViewId =
+ AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId);
+ final int parentVirtualDescendantId =
+ AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId);
+ if (parentVirtualDescendantId != AccessibilityNodeProvider.HOST_VIEW_ID
+ || parentAccessibilityViewId == providerHost.getAccessibilityViewId()) {
+ final AccessibilityNodeInfo parent =
+ provider.createAccessibilityNodeInfo(parentVirtualDescendantId);
+ if (parent != null) {
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ final long childNodeId = parent.getChildId(i);
+ if (childNodeId != current.getSourceNodeId()) {
+ final int childVirtualDescendantId =
+ AccessibilityNodeInfo.getVirtualDescendantId(childNodeId);
+ AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(
+ childVirtualDescendantId);
+ if (child != null) {
+ outInfos.add(child);
+ }
+ }
+ }
+ }
+ } else {
+ prefetchSiblingsOfRealNode(providerHost, outInfos);
+ }
+ }
+
+ private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root,
+ AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) {
+ final int initialOutInfosSize = outInfos.size();
+ final int childCount = root.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
+ final long childNodeId = root.getChildId(i);
+ AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeInfo.getVirtualDescendantId(childNodeId));
+ if (child != null) {
+ outInfos.add(child);
+ }
+ }
+ if (!shouldStopPrefetching(outInfos)) {
+ final int addedChildCount = outInfos.size() - initialOutInfosSize;
+ for (int i = 0; i < addedChildCount; i++) {
+ AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i);
+ prefetchDescendantsOfVirtualNode(child, provider, outInfos);
+ }
+ }
+ }
+ }
+
+ private class PrivateHandler extends Handler {
+ private static final int MSG_PERFORM_ACCESSIBILITY_ACTION = 1;
+ private static final int MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2;
+ private static final int MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID = 3;
+ private static final int MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT = 4;
+ private static final int MSG_FIND_FOCUS = 5;
+ private static final int MSG_FOCUS_SEARCH = 6;
+ private static final int MSG_PREPARE_FOR_EXTRA_DATA_REQUEST = 7;
+ private static final int MSG_APP_PREPARATION_FINISHED = 8;
+ private static final int MSG_APP_PREPARATION_TIMEOUT = 9;
+
+ // Uses FIRST_NO_ACCESSIBILITY_CALLBACK_MSG for messages that don't need to call back
+ // results to interrogating client.
+ private static final int FIRST_NO_ACCESSIBILITY_CALLBACK_MSG = 100;
+ private static final int MSG_CLEAR_ACCESSIBILITY_FOCUS =
+ FIRST_NO_ACCESSIBILITY_CALLBACK_MSG + 1;
+ private static final int MSG_NOTIFY_OUTSIDE_TOUCH =
+ FIRST_NO_ACCESSIBILITY_CALLBACK_MSG + 2;
+
+ public PrivateHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public String getMessageName(Message message) {
+ final int type = message.what;
+ switch (type) {
+ case MSG_PERFORM_ACCESSIBILITY_ACTION:
+ return "MSG_PERFORM_ACCESSIBILITY_ACTION";
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID:
+ return "MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID";
+ case MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID:
+ return "MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID";
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT:
+ return "MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT";
+ case MSG_FIND_FOCUS:
+ return "MSG_FIND_FOCUS";
+ case MSG_FOCUS_SEARCH:
+ return "MSG_FOCUS_SEARCH";
+ case MSG_PREPARE_FOR_EXTRA_DATA_REQUEST:
+ return "MSG_PREPARE_FOR_EXTRA_DATA_REQUEST";
+ case MSG_APP_PREPARATION_FINISHED:
+ return "MSG_APP_PREPARATION_FINISHED";
+ case MSG_APP_PREPARATION_TIMEOUT:
+ return "MSG_APP_PREPARATION_TIMEOUT";
+ case MSG_CLEAR_ACCESSIBILITY_FOCUS:
+ return "MSG_CLEAR_ACCESSIBILITY_FOCUS";
+ case MSG_NOTIFY_OUTSIDE_TOUCH:
+ return "MSG_NOTIFY_OUTSIDE_TOUCH";
+ default:
+ throw new IllegalArgumentException("Unknown message type: " + type);
+ }
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ final int type = message.what;
+ switch (type) {
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID: {
+ findAccessibilityNodeInfoByAccessibilityIdUiThread(message);
+ } break;
+ case MSG_PERFORM_ACCESSIBILITY_ACTION: {
+ performAccessibilityActionUiThread(message);
+ } break;
+ case MSG_FIND_ACCESSIBILITY_NODE_INFOS_BY_VIEW_ID: {
+ findAccessibilityNodeInfosByViewIdUiThread(message);
+ } break;
+ case MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_TEXT: {
+ findAccessibilityNodeInfosByTextUiThread(message);
+ } break;
+ case MSG_FIND_FOCUS: {
+ findFocusUiThread(message);
+ } break;
+ case MSG_FOCUS_SEARCH: {
+ focusSearchUiThread(message);
+ } break;
+ case MSG_PREPARE_FOR_EXTRA_DATA_REQUEST: {
+ prepareForExtraDataRequestUiThread(message);
+ } break;
+ case MSG_APP_PREPARATION_FINISHED: {
+ requestPreparerDoneUiThread(message);
+ } break;
+ case MSG_APP_PREPARATION_TIMEOUT: {
+ requestPreparerTimeoutUiThread();
+ } break;
+ case MSG_CLEAR_ACCESSIBILITY_FOCUS: {
+ clearAccessibilityFocusUiThread();
+ } break;
+ case MSG_NOTIFY_OUTSIDE_TOUCH: {
+ notifyOutsideTouchUiThread();
+ } break;
+ default:
+ throw new IllegalArgumentException("Unknown message type: " + type);
+ }
+ }
+
+ boolean hasAccessibilityCallback(Message message) {
+ return message.what < FIRST_NO_ACCESSIBILITY_CALLBACK_MSG ? true : false;
+ }
+
+ boolean hasUserInteractiveMessagesWaiting() {
+ return hasMessagesOrCallbacks();
+ }
+ }
+
+ private final class AddNodeInfosForViewId implements Predicate<View> {
+ private int mViewId = View.NO_ID;
+ private List<AccessibilityNodeInfo> mInfos;
+
+ public void init(int viewId, List<AccessibilityNodeInfo> infos) {
+ mViewId = viewId;
+ mInfos = infos;
+ }
+
+ public void reset() {
+ mViewId = View.NO_ID;
+ mInfos = null;
+ }
+
+ @Override
+ public boolean test(View view) {
+ if (view.getId() == mViewId && isShown(view)) {
+ mInfos.add(view.createAccessibilityNodeInfo());
+ }
+ return false;
+ }
+ }
+
+ private static final class MessageHolder {
+ final Message mMessage;
+ final int mInterrogatingPid;
+ final long mInterrogatingTid;
+
+ MessageHolder(Message message, int interrogatingPid, long interrogatingTid) {
+ mMessage = message;
+ mInterrogatingPid = interrogatingPid;
+ mInterrogatingTid = interrogatingTid;
+ }
+ }
+
+ private static class SatisfiedFindAccessibilityNodeByAccessibilityIdRequest {
+ final AccessibilityNodeInfo mSatisfiedRequestNode;
+ final IAccessibilityInteractionConnectionCallback mSatisfiedRequestCallback;
+ final int mSatisfiedRequestInteractionId;
+
+ SatisfiedFindAccessibilityNodeByAccessibilityIdRequest(
+ AccessibilityNodeInfo satisfiedRequestNode,
+ IAccessibilityInteractionConnectionCallback satisfiedRequestCallback,
+ int satisfiedRequestInteractionId) {
+ mSatisfiedRequestNode = satisfiedRequestNode;
+ mSatisfiedRequestCallback = satisfiedRequestCallback;
+ mSatisfiedRequestInteractionId = satisfiedRequestInteractionId;
+ }
+ }
+}
diff --git a/android/view/AccessibilityIterators.java b/android/view/AccessibilityIterators.java
new file mode 100644
index 0000000..c41b3cf
--- /dev/null
+++ b/android/view/AccessibilityIterators.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.res.Configuration;
+import android.os.Build;
+
+import java.text.BreakIterator;
+import java.util.Locale;
+
+/**
+ * This class contains the implementation of text segment iterators
+ * for accessibility support.
+ *
+ * Note: Such iterators are needed in the view package since we want
+ * to be able to iterator over content description of any view.
+ *
+ * @hide
+ */
+public final class AccessibilityIterators {
+
+ /**
+ * @hide
+ */
+ public static interface TextSegmentIterator {
+ public int[] following(int current);
+ public int[] preceding(int current);
+ }
+
+ /**
+ * @hide
+ */
+ public static abstract class AbstractTextSegmentIterator implements TextSegmentIterator {
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public AbstractTextSegmentIterator() {
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected String mText;
+
+ private final int[] mSegment = new int[2];
+
+ public void initialize(String text) {
+ mText = text;
+ }
+
+ protected int[] getRange(int start, int end) {
+ if (start < 0 || end < 0 || start == end) {
+ return null;
+ }
+ mSegment[0] = start;
+ mSegment[1] = end;
+ return mSegment;
+ }
+ }
+
+ static class CharacterTextSegmentIterator extends AbstractTextSegmentIterator
+ implements ViewRootImpl.ConfigChangedCallback {
+ private static CharacterTextSegmentIterator sInstance;
+
+ private Locale mLocale;
+
+ protected BreakIterator mImpl;
+
+ public static CharacterTextSegmentIterator getInstance(Locale locale) {
+ if (sInstance == null) {
+ sInstance = new CharacterTextSegmentIterator(locale);
+ }
+ return sInstance;
+ }
+
+ private CharacterTextSegmentIterator(Locale locale) {
+ mLocale = locale;
+ onLocaleChanged(locale);
+ ViewRootImpl.addConfigCallback(this);
+ }
+
+ @Override
+ public void initialize(String text) {
+ super.initialize(text);
+ mImpl.setText(text);
+ }
+
+ @Override
+ public int[] following(int offset) {
+ final int textLegth = mText.length();
+ if (textLegth <= 0) {
+ return null;
+ }
+ if (offset >= textLegth) {
+ return null;
+ }
+ int start = offset;
+ if (start < 0) {
+ start = 0;
+ }
+ while (!mImpl.isBoundary(start)) {
+ start = mImpl.following(start);
+ if (start == BreakIterator.DONE) {
+ return null;
+ }
+ }
+ final int end = mImpl.following(start);
+ if (end == BreakIterator.DONE) {
+ return null;
+ }
+ return getRange(start, end);
+ }
+
+ @Override
+ public int[] preceding(int offset) {
+ final int textLegth = mText.length();
+ if (textLegth <= 0) {
+ return null;
+ }
+ if (offset <= 0) {
+ return null;
+ }
+ int end = offset;
+ if (end > textLegth) {
+ end = textLegth;
+ }
+ while (!mImpl.isBoundary(end)) {
+ end = mImpl.preceding(end);
+ if (end == BreakIterator.DONE) {
+ return null;
+ }
+ }
+ final int start = mImpl.preceding(end);
+ if (start == BreakIterator.DONE) {
+ return null;
+ }
+ return getRange(start, end);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration globalConfig) {
+ final Locale locale = globalConfig.getLocales().get(0);
+ if (locale == null) {
+ return;
+ }
+ if (!mLocale.equals(locale)) {
+ mLocale = locale;
+ onLocaleChanged(locale);
+ }
+ }
+
+ protected void onLocaleChanged(Locale locale) {
+ mImpl = BreakIterator.getCharacterInstance(locale);
+ }
+ }
+
+ static class WordTextSegmentIterator extends CharacterTextSegmentIterator {
+ private static WordTextSegmentIterator sInstance;
+
+ public static WordTextSegmentIterator getInstance(Locale locale) {
+ if (sInstance == null) {
+ sInstance = new WordTextSegmentIterator(locale);
+ }
+ return sInstance;
+ }
+
+ private WordTextSegmentIterator(Locale locale) {
+ super(locale);
+ }
+
+ @Override
+ protected void onLocaleChanged(Locale locale) {
+ mImpl = BreakIterator.getWordInstance(locale);
+ }
+
+ @Override
+ public int[] following(int offset) {
+ final int textLegth = mText.length();
+ if (textLegth <= 0) {
+ return null;
+ }
+ if (offset >= mText.length()) {
+ return null;
+ }
+ int start = offset;
+ if (start < 0) {
+ start = 0;
+ }
+ while (!isLetterOrDigit(start) && !isStartBoundary(start)) {
+ start = mImpl.following(start);
+ if (start == BreakIterator.DONE) {
+ return null;
+ }
+ }
+ final int end = mImpl.following(start);
+ if (end == BreakIterator.DONE || !isEndBoundary(end)) {
+ return null;
+ }
+ return getRange(start, end);
+ }
+
+ @Override
+ public int[] preceding(int offset) {
+ final int textLegth = mText.length();
+ if (textLegth <= 0) {
+ return null;
+ }
+ if (offset <= 0) {
+ return null;
+ }
+ int end = offset;
+ if (end > textLegth) {
+ end = textLegth;
+ }
+ while (end > 0 && !isLetterOrDigit(end - 1) && !isEndBoundary(end)) {
+ end = mImpl.preceding(end);
+ if (end == BreakIterator.DONE) {
+ return null;
+ }
+ }
+ final int start = mImpl.preceding(end);
+ if (start == BreakIterator.DONE || !isStartBoundary(start)) {
+ return null;
+ }
+ return getRange(start, end);
+ }
+
+ private boolean isStartBoundary(int index) {
+ return isLetterOrDigit(index)
+ && (index == 0 || !isLetterOrDigit(index - 1));
+ }
+
+ private boolean isEndBoundary(int index) {
+ return (index > 0 && isLetterOrDigit(index - 1))
+ && (index == mText.length() || !isLetterOrDigit(index));
+ }
+
+ private boolean isLetterOrDigit(int index) {
+ if (index >= 0 && index < mText.length()) {
+ final int codePoint = mText.codePointAt(index);
+ return Character.isLetterOrDigit(codePoint);
+ }
+ return false;
+ }
+ }
+
+ static class ParagraphTextSegmentIterator extends AbstractTextSegmentIterator {
+ private static ParagraphTextSegmentIterator sInstance;
+
+ public static ParagraphTextSegmentIterator getInstance() {
+ if (sInstance == null) {
+ sInstance = new ParagraphTextSegmentIterator();
+ }
+ return sInstance;
+ }
+
+ @Override
+ public int[] following(int offset) {
+ final int textLength = mText.length();
+ if (textLength <= 0) {
+ return null;
+ }
+ if (offset >= textLength) {
+ return null;
+ }
+ int start = offset;
+ if (start < 0) {
+ start = 0;
+ }
+ while (start < textLength && mText.charAt(start) == '\n'
+ && !isStartBoundary(start)) {
+ start++;
+ }
+ if (start >= textLength) {
+ return null;
+ }
+ int end = start + 1;
+ while (end < textLength && !isEndBoundary(end)) {
+ end++;
+ }
+ return getRange(start, end);
+ }
+
+ @Override
+ public int[] preceding(int offset) {
+ final int textLength = mText.length();
+ if (textLength <= 0) {
+ return null;
+ }
+ if (offset <= 0) {
+ return null;
+ }
+ int end = offset;
+ if (end > textLength) {
+ end = textLength;
+ }
+ while(end > 0 && mText.charAt(end - 1) == '\n' && !isEndBoundary(end)) {
+ end--;
+ }
+ if (end <= 0) {
+ return null;
+ }
+ int start = end - 1;
+ while (start > 0 && !isStartBoundary(start)) {
+ start--;
+ }
+ return getRange(start, end);
+ }
+
+ private boolean isStartBoundary(int index) {
+ return (mText.charAt(index) != '\n'
+ && (index == 0 || mText.charAt(index - 1) == '\n'));
+ }
+
+ private boolean isEndBoundary(int index) {
+ return (index > 0 && mText.charAt(index - 1) != '\n'
+ && (index == mText.length() || mText.charAt(index) == '\n'));
+ }
+ }
+}
diff --git a/android/view/ActionMode.java b/android/view/ActionMode.java
new file mode 100644
index 0000000..d9f9d03
--- /dev/null
+++ b/android/view/ActionMode.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+
+import android.annotation.StringRes;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Rect;
+
+/**
+ * Represents a contextual mode of the user interface. Action modes can be used to provide
+ * alternative interaction modes and replace parts of the normal UI until finished.
+ * Examples of good action modes include text selection and contextual actions.
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about how to provide contextual actions with {@code ActionMode},
+ * read the <a href="{@docRoot}guide/topics/ui/menus.html#context-menu">Menus</a>
+ * developer guide.</p>
+ * </div>
+ */
+public abstract class ActionMode {
+
+ /**
+ * The action mode is treated as a Primary mode. This is the default.
+ * Use with {@link #setType}.
+ */
+ public static final int TYPE_PRIMARY = 0;
+ /**
+ * The action mode is treated as a Floating Toolbar.
+ * Use with {@link #setType}.
+ */
+ public static final int TYPE_FLOATING = 1;
+
+ /**
+ * Default value to hide the action mode for
+ * {@link ViewConfiguration#getDefaultActionModeHideDuration()}.
+ */
+ public static final int DEFAULT_HIDE_DURATION = -1;
+
+ private Object mTag;
+ private boolean mTitleOptionalHint;
+ private int mType = TYPE_PRIMARY;
+
+ /**
+ * Set a tag object associated with this ActionMode.
+ *
+ * <p>Like the tag available to views, this allows applications to associate arbitrary
+ * data with an ActionMode for later reference.
+ *
+ * @param tag Tag to associate with this ActionMode
+ *
+ * @see #getTag()
+ */
+ public void setTag(Object tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Retrieve the tag object associated with this ActionMode.
+ *
+ * <p>Like the tag available to views, this allows applications to associate arbitrary
+ * data with an ActionMode for later reference.
+ *
+ * @return Tag associated with this ActionMode
+ *
+ * @see #setTag(Object)
+ */
+ public Object getTag() {
+ return mTag;
+ }
+
+ /**
+ * Set the title of the action mode. This method will have no visible effect if
+ * a custom view has been set.
+ *
+ * @param title Title string to set
+ *
+ * @see #setTitle(int)
+ * @see #setCustomView(View)
+ */
+ public abstract void setTitle(CharSequence title);
+
+ /**
+ * Set the title of the action mode. This method will have no visible effect if
+ * a custom view has been set.
+ *
+ * @param resId Resource ID of a string to set as the title
+ *
+ * @see #setTitle(CharSequence)
+ * @see #setCustomView(View)
+ */
+ public abstract void setTitle(@StringRes int resId);
+
+ /**
+ * Set the subtitle of the action mode. This method will have no visible effect if
+ * a custom view has been set.
+ *
+ * @param subtitle Subtitle string to set
+ *
+ * @see #setSubtitle(int)
+ * @see #setCustomView(View)
+ */
+ public abstract void setSubtitle(CharSequence subtitle);
+
+ /**
+ * Set the subtitle of the action mode. This method will have no visible effect if
+ * a custom view has been set.
+ *
+ * @param resId Resource ID of a string to set as the subtitle
+ *
+ * @see #setSubtitle(CharSequence)
+ * @see #setCustomView(View)
+ */
+ public abstract void setSubtitle(@StringRes int resId);
+
+ /**
+ * Set whether or not the title/subtitle display for this action mode
+ * is optional.
+ *
+ * <p>In many cases the supplied title for an action mode is merely
+ * meant to add context and is not strictly required for the action
+ * mode to be useful. If the title is optional, the system may choose
+ * to hide the title entirely rather than truncate it due to a lack
+ * of available space.</p>
+ *
+ * <p>Note that this is merely a hint; the underlying implementation
+ * may choose to ignore this setting under some circumstances.</p>
+ *
+ * @param titleOptional true if the title only presents optional information.
+ */
+ public void setTitleOptionalHint(boolean titleOptional) {
+ mTitleOptionalHint = titleOptional;
+ }
+
+ /**
+ * @return true if this action mode has been given a hint to consider the
+ * title/subtitle display to be optional.
+ *
+ * @see #setTitleOptionalHint(boolean)
+ * @see #isTitleOptional()
+ */
+ public boolean getTitleOptionalHint() {
+ return mTitleOptionalHint;
+ }
+
+ /**
+ * @return true if this action mode considers the title and subtitle fields
+ * as optional. Optional titles may not be displayed to the user.
+ */
+ public boolean isTitleOptional() {
+ return false;
+ }
+
+ /**
+ * Set a custom view for this action mode. The custom view will take the place of
+ * the title and subtitle. Useful for things like search boxes.
+ *
+ * @param view Custom view to use in place of the title/subtitle.
+ *
+ * @see #setTitle(CharSequence)
+ * @see #setSubtitle(CharSequence)
+ */
+ public abstract void setCustomView(View view);
+
+ /**
+ * Set a type for this action mode. This will affect how the system renders the action mode if
+ * it has to.
+ *
+ * @param type One of {@link #TYPE_PRIMARY} or {@link #TYPE_FLOATING}.
+ */
+ public void setType(int type) {
+ mType = type;
+ }
+
+ /**
+ * Returns the type for this action mode.
+ *
+ * @return One of {@link #TYPE_PRIMARY} or {@link #TYPE_FLOATING}.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Invalidate the action mode and refresh menu content. The mode's
+ * {@link ActionMode.Callback} will have its
+ * {@link Callback#onPrepareActionMode(ActionMode, Menu)} method called.
+ * If it returns true the menu will be scanned for updated content and any relevant changes
+ * will be reflected to the user.
+ */
+ public abstract void invalidate();
+
+ /**
+ * Invalidate the content rect associated to this ActionMode. This only makes sense for
+ * action modes that support dynamic positioning on the screen, and provides a more efficient
+ * way to reposition it without invalidating the whole action mode.
+ *
+ * @see Callback2#onGetContentRect(ActionMode, View, Rect) .
+ */
+ public void invalidateContentRect() {}
+
+ /**
+ * Hide the action mode view from obstructing the content below for a short duration.
+ * This only makes sense for action modes that support dynamic positioning on the screen.
+ * If this method is called again before the hide duration expires, the later hide call will
+ * cancel the former and then take effect.
+ * NOTE that there is an internal limit to how long the mode can be hidden for. It's typically
+ * about a few seconds.
+ *
+ * @param duration The number of milliseconds to hide for.
+ * @see #DEFAULT_HIDE_DURATION
+ */
+ public void hide(long duration) {}
+
+ /**
+ * Finish and close this action mode. The action mode's {@link ActionMode.Callback} will
+ * have its {@link Callback#onDestroyActionMode(ActionMode)} method called.
+ */
+ public abstract void finish();
+
+ /**
+ * Returns the menu of actions that this action mode presents.
+ * @return The action mode's menu.
+ */
+ public abstract Menu getMenu();
+
+ /**
+ * Returns the current title of this action mode.
+ * @return Title text
+ */
+ public abstract CharSequence getTitle();
+
+ /**
+ * Returns the current subtitle of this action mode.
+ * @return Subtitle text
+ */
+ public abstract CharSequence getSubtitle();
+
+ /**
+ * Returns the current custom view for this action mode.
+ * @return The current custom view
+ */
+ public abstract View getCustomView();
+
+ /**
+ * Returns a {@link MenuInflater} with the ActionMode's context.
+ */
+ public abstract MenuInflater getMenuInflater();
+
+ /**
+ * Called when the window containing the view that started this action mode gains or loses
+ * focus.
+ *
+ * @param hasWindowFocus True if the window containing the view that started this action mode
+ * now has focus, false otherwise.
+ *
+ */
+ public void onWindowFocusChanged(boolean hasWindowFocus) {}
+
+ /**
+ * Returns whether the UI presenting this action mode can take focus or not.
+ * This is used by internal components within the framework that would otherwise
+ * present an action mode UI that requires focus, such as an EditText as a custom view.
+ *
+ * @return true if the UI used to show this action mode can take focus
+ * @hide Internal use only
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public boolean isUiFocusable() {
+ return true;
+ }
+
+ /**
+ * Callback interface for action modes. Supplied to
+ * {@link View#startActionMode(Callback)}, a Callback
+ * configures and handles events raised by a user's interaction with an action mode.
+ *
+ * <p>An action mode's lifecycle is as follows:
+ * <ul>
+ * <li>{@link Callback#onCreateActionMode(ActionMode, Menu)} once on initial
+ * creation</li>
+ * <li>{@link Callback#onPrepareActionMode(ActionMode, Menu)} after creation
+ * and any time the {@link ActionMode} is invalidated</li>
+ * <li>{@link Callback#onActionItemClicked(ActionMode, MenuItem)} any time a
+ * contextual action button is clicked</li>
+ * <li>{@link Callback#onDestroyActionMode(ActionMode)} when the action mode
+ * is closed</li>
+ * </ul>
+ */
+ public interface Callback {
+ /**
+ * Called when action mode is first created. The menu supplied will be used to
+ * generate action buttons for the action mode.
+ *
+ * @param mode ActionMode being created
+ * @param menu Menu used to populate action buttons
+ * @return true if the action mode should be created, false if entering this
+ * mode should be aborted.
+ */
+ public boolean onCreateActionMode(ActionMode mode, Menu menu);
+
+ /**
+ * Called to refresh an action mode's action menu whenever it is invalidated.
+ *
+ * @param mode ActionMode being prepared
+ * @param menu Menu used to populate action buttons
+ * @return true if the menu or action mode was updated, false otherwise.
+ */
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu);
+
+ /**
+ * Called to report a user click on an action button.
+ *
+ * @param mode The current ActionMode
+ * @param item The item that was clicked
+ * @return true if this callback handled the event, false if the standard MenuItem
+ * invocation should continue.
+ */
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item);
+
+ /**
+ * Called when an action mode is about to be exited and destroyed.
+ *
+ * @param mode The current ActionMode being destroyed
+ */
+ public void onDestroyActionMode(ActionMode mode);
+ }
+
+ /**
+ * Extension of {@link ActionMode.Callback} to provide content rect information. This is
+ * required for ActionModes with dynamic positioning such as the ones with type
+ * {@link ActionMode#TYPE_FLOATING} to ensure the positioning doesn't obscure app content. If
+ * an app fails to provide a subclass of this class, a default implementation will be used.
+ */
+ public static abstract class Callback2 implements ActionMode.Callback {
+
+ /**
+ * Called when an ActionMode needs to be positioned on screen, potentially occluding view
+ * content. Note this may be called on a per-frame basis.
+ *
+ * @param mode The ActionMode that requires positioning.
+ * @param view The View that originated the ActionMode, in whose coordinates the Rect should
+ * be provided.
+ * @param outRect The Rect to be populated with the content position. Use this to specify
+ * where the content in your app lives within the given view. This will be used
+ * to avoid occluding the given content Rect with the created ActionMode.
+ */
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+ if (view != null) {
+ outRect.set(0, 0, view.getWidth(), view.getHeight());
+ } else {
+ outRect.set(0, 0, 0, 0);
+ }
+ }
+
+ }
+}
diff --git a/android/view/ActionProvider.java b/android/view/ActionProvider.java
new file mode 100644
index 0000000..e1be0fe
--- /dev/null
+++ b/android/view/ActionProvider.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * An ActionProvider defines rich menu interaction in a single component.
+ * ActionProvider can generate action views for use in the action bar,
+ * dynamically populate submenus of a MenuItem, and handle default menu
+ * item invocations.
+ *
+ * <p>An ActionProvider can be optionally specified for a {@link MenuItem} and will be
+ * responsible for creating the action view that appears in the {@link android.app.ActionBar}
+ * in place of a simple button in the bar. When the menu item is presented in a way that
+ * does not allow custom action views, (e.g. in an overflow menu,) the ActionProvider
+ * can perform a default action.</p>
+ *
+ * <p>There are two ways to use an action provider:
+ * <ul>
+ * <li>
+ * Set the action provider on a {@link MenuItem} directly by calling
+ * {@link MenuItem#setActionProvider(ActionProvider)}.
+ * </li>
+ * <li>
+ * Declare the action provider in an XML menu resource. For example:
+ * <pre>
+ * <code>
+ * <item android:id="@+id/my_menu_item"
+ * android:title="Title"
+ * android:icon="@drawable/my_menu_item_icon"
+ * android:showAsAction="ifRoom"
+ * android:actionProviderClass="foo.bar.SomeActionProvider" />
+ * </code>
+ * </pre>
+ * </li>
+ * </ul>
+ * </p>
+ *
+ * @see MenuItem#setActionProvider(ActionProvider)
+ * @see MenuItem#getActionProvider()
+ */
+public abstract class ActionProvider {
+ private static final String TAG = "ActionProvider";
+ private SubUiVisibilityListener mSubUiVisibilityListener;
+ private VisibilityListener mVisibilityListener;
+
+ /**
+ * Creates a new instance. ActionProvider classes should always implement a
+ * constructor that takes a single Context parameter for inflating from menu XML.
+ *
+ * @param context Context for accessing resources.
+ */
+ public ActionProvider(Context context) {
+ }
+
+ /**
+ * Factory method called by the Android framework to create new action views.
+ *
+ * <p>This method has been deprecated in favor of {@link #onCreateActionView(MenuItem)}.
+ * Newer apps that wish to support platform versions prior to API 16 should also
+ * implement this method to return a valid action view.</p>
+ *
+ * @return A new action view.
+ *
+ * @deprecated use {@link #onCreateActionView(MenuItem)}
+ */
+ @Deprecated
+ public abstract View onCreateActionView();
+
+ /**
+ * Factory method called by the Android framework to create new action views.
+ * This method returns a new action view for the given MenuItem.
+ *
+ * <p>If your ActionProvider implementation overrides the deprecated no-argument overload
+ * {@link #onCreateActionView()}, overriding this method for devices running API 16 or later
+ * is recommended but optional. The default implementation calls {@link #onCreateActionView()}
+ * for compatibility with applications written for older platform versions.</p>
+ *
+ * @param forItem MenuItem to create the action view for
+ * @return the new action view
+ */
+ public View onCreateActionView(MenuItem forItem) {
+ return onCreateActionView();
+ }
+
+ /**
+ * The result of this method determines whether or not {@link #isVisible()} will be used
+ * by the {@link MenuItem} this ActionProvider is bound to help determine its visibility.
+ *
+ * @return true if this ActionProvider overrides the visibility of the MenuItem
+ * it is bound to, false otherwise. The default implementation returns false.
+ * @see #isVisible()
+ */
+ public boolean overridesItemVisibility() {
+ return false;
+ }
+
+ /**
+ * If {@link #overridesItemVisibility()} returns true, the return value of this method
+ * will help determine the visibility of the {@link MenuItem} this ActionProvider is bound to.
+ *
+ * <p>If the MenuItem's visibility is explicitly set to false by the application,
+ * the MenuItem will not be shown, even if this method returns true.</p>
+ *
+ * @return true if the MenuItem this ActionProvider is bound to is visible, false if
+ * it is invisible. The default implementation returns true.
+ */
+ public boolean isVisible() {
+ return true;
+ }
+
+ /**
+ * If this ActionProvider is associated with an item in a menu,
+ * refresh the visibility of the item based on {@link #overridesItemVisibility()} and
+ * {@link #isVisible()}. If {@link #overridesItemVisibility()} returns false, this call
+ * will have no effect.
+ */
+ public void refreshVisibility() {
+ if (mVisibilityListener != null && overridesItemVisibility()) {
+ mVisibilityListener.onActionProviderVisibilityChanged(isVisible());
+ }
+ }
+
+ /**
+ * Performs an optional default action.
+ * <p>
+ * For the case of an action provider placed in a menu item not shown as an action this
+ * method is invoked if previous callbacks for processing menu selection has handled
+ * the event.
+ * </p>
+ * <p>
+ * A menu item selection is processed in the following order:
+ * <ul>
+ * <li>
+ * Receiving a call to {@link MenuItem.OnMenuItemClickListener#onMenuItemClick
+ * MenuItem.OnMenuItemClickListener.onMenuItemClick}.
+ * </li>
+ * <li>
+ * Receiving a call to {@link android.app.Activity#onOptionsItemSelected(MenuItem)
+ * Activity.onOptionsItemSelected(MenuItem)}
+ * </li>
+ * <li>
+ * Receiving a call to {@link android.app.Fragment#onOptionsItemSelected(MenuItem)
+ * Fragment.onOptionsItemSelected(MenuItem)}
+ * </li>
+ * <li>
+ * Launching the {@link android.content.Intent} set via
+ * {@link MenuItem#setIntent(android.content.Intent) MenuItem.setIntent(android.content.Intent)}
+ * </li>
+ * <li>
+ * Invoking this method.
+ * </li>
+ * </ul>
+ * </p>
+ * <p>
+ * The default implementation does not perform any action and returns false.
+ * </p>
+ */
+ public boolean onPerformDefaultAction() {
+ return false;
+ }
+
+ /**
+ * Determines if this ActionProvider has a submenu associated with it.
+ *
+ * <p>Associated submenus will be shown when an action view is not. This
+ * provider instance will receive a call to {@link #onPrepareSubMenu(SubMenu)}
+ * after the call to {@link #onPerformDefaultAction()} and before a submenu is
+ * displayed to the user.
+ *
+ * @return true if the item backed by this provider should have an associated submenu
+ */
+ public boolean hasSubMenu() {
+ return false;
+ }
+
+ /**
+ * Called to prepare an associated submenu for the menu item backed by this ActionProvider.
+ *
+ * <p>if {@link #hasSubMenu()} returns true, this method will be called when the
+ * menu item is selected to prepare the submenu for presentation to the user. Apps
+ * may use this to create or alter submenu content right before display.
+ *
+ * @param subMenu Submenu that will be displayed
+ */
+ public void onPrepareSubMenu(SubMenu subMenu) {
+ }
+
+ /**
+ * Notify the system that the visibility of an action view's sub-UI such as
+ * an anchored popup has changed. This will affect how other system
+ * visibility notifications occur.
+ *
+ * @hide Pending future API approval
+ */
+ public void subUiVisibilityChanged(boolean isVisible) {
+ if (mSubUiVisibilityListener != null) {
+ mSubUiVisibilityListener.onSubUiVisibilityChanged(isVisible);
+ }
+ }
+
+ /**
+ * @hide Internal use only
+ */
+ @UnsupportedAppUsage
+ public void setSubUiVisibilityListener(SubUiVisibilityListener listener) {
+ mSubUiVisibilityListener = listener;
+ }
+
+ /**
+ * Set a listener to be notified when this ActionProvider's overridden visibility changes.
+ * This should only be used by MenuItem implementations.
+ *
+ * @param listener listener to set
+ */
+ public void setVisibilityListener(VisibilityListener listener) {
+ if (mVisibilityListener != null) {
+ Log.w(TAG, "setVisibilityListener: Setting a new ActionProvider.VisibilityListener " +
+ "when one is already set. Are you reusing this " + getClass().getSimpleName() +
+ " instance while it is still in use somewhere else?");
+ }
+ mVisibilityListener = listener;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void reset() {
+ mVisibilityListener = null;
+ mSubUiVisibilityListener = null;
+ }
+
+ /**
+ * @hide Internal use only
+ */
+ public interface SubUiVisibilityListener {
+ public void onSubUiVisibilityChanged(boolean isVisible);
+ }
+
+ /**
+ * Listens to changes in visibility as reported by {@link ActionProvider#refreshVisibility()}.
+ *
+ * @see ActionProvider#overridesItemVisibility()
+ * @see ActionProvider#isVisible()
+ */
+ public interface VisibilityListener {
+ public void onActionProviderVisibilityChanged(boolean isVisible);
+ }
+}
diff --git a/android/view/AppTransitionAnimationSpec.java b/android/view/AppTransitionAnimationSpec.java
new file mode 100644
index 0000000..3215f2b
--- /dev/null
+++ b/android/view/AppTransitionAnimationSpec.java
@@ -0,0 +1,62 @@
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Holds information about how the next app transition animation should be executed.
+ *
+ * This class is intended to be used with IWindowManager.overridePendingAppTransition* methods when
+ * simple arguments are not enough to describe the animation.
+ *
+ * @hide
+ */
+public class AppTransitionAnimationSpec implements Parcelable {
+ public final int taskId;
+ public final HardwareBuffer buffer;
+ public final Rect rect;
+
+ @UnsupportedAppUsage
+ public AppTransitionAnimationSpec(int taskId, HardwareBuffer buffer, Rect rect) {
+ this.taskId = taskId;
+ this.rect = rect;
+ this.buffer = buffer;
+ }
+
+ public AppTransitionAnimationSpec(Parcel in) {
+ taskId = in.readInt();
+ rect = in.readTypedObject(Rect.CREATOR);
+ buffer = in.readTypedObject(HardwareBuffer.CREATOR);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(taskId);
+ dest.writeTypedObject(rect, 0 /* flags */);
+ dest.writeTypedObject(buffer, 0 /* flags */);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AppTransitionAnimationSpec> CREATOR
+ = new Parcelable.Creator<AppTransitionAnimationSpec>() {
+ public AppTransitionAnimationSpec createFromParcel(Parcel in) {
+ return new AppTransitionAnimationSpec(in);
+ }
+
+ public AppTransitionAnimationSpec[] newArray(int size) {
+ return new AppTransitionAnimationSpec[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "{taskId: " + taskId + ", buffer: " + buffer + ", rect: " + rect + "}";
+ }
+}
diff --git a/android/view/AttachInfo_Accessor.java b/android/view/AttachInfo_Accessor.java
new file mode 100644
index 0000000..60c13c0
--- /dev/null
+++ b/android/view/AttachInfo_Accessor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.os.Handler;
+import android.view.View.AttachInfo;
+
+import com.android.layoutlib.bridge.util.ReflectionUtils;
+
+/**
+ * Class allowing access to package-protected methods/fields.
+ */
+public class AttachInfo_Accessor {
+
+ public static void setAttachInfo(View view) {
+ Context context = view.getContext();
+ WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ ViewRootImpl root = new ViewRootImpl(context, display);
+ AttachInfo info = new AttachInfo(ReflectionUtils.createProxy(IWindowSession.class),
+ ReflectionUtils.createProxy(IWindow.class), display, root, new Handler(), null,
+ context);
+ info.mHasWindowFocus = true;
+ info.mWindowVisibility = View.VISIBLE;
+ info.mInTouchMode = false; // this is so that we can display selections.
+ info.mHardwareAccelerated = false;
+ view.dispatchAttachedToWindow(info, 0);
+ }
+
+ public static void dispatchOnPreDraw(View view) {
+ view.mAttachInfo.mTreeObserver.dispatchOnPreDraw();
+ }
+
+ public static void detachFromWindow(View view) {
+ if (view != null) {
+ view.dispatchDetachedFromWindow();
+ }
+ }
+
+ public static ViewRootImpl getRootView(View view) {
+ return view.mAttachInfo != null ? view.mAttachInfo.mViewRootImpl : null;
+ }
+}
diff --git a/android/view/AttachedSurfaceControl.java b/android/view/AttachedSurfaceControl.java
new file mode 100644
index 0000000..bcc5b56
--- /dev/null
+++ b/android/view/AttachedSurfaceControl.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+
+/**
+ * Provides an interface to the root-Surface of a View Hierarchy or Window. This
+ * is used in combination with the {@link android.view.SurfaceControl} API to enable
+ * attaching app created SurfaceControl to the SurfaceControl hierarchy used
+ * by the app, and enable SurfaceTransactions to be performed in sync with the
+ * View hierarchy drawing.
+ *
+ * This object is obtained from {@link android.view.View#getRootSurfaceControl} and
+ * {@link android.view.Window#getRootSurfaceControl}. It must be used from the UI thread of
+ * the object it was obtained from.
+ */
+@UiThread
+public interface AttachedSurfaceControl {
+ /**
+ * Create a transaction which will reparent {@param child} to the View hierarchy
+ * root SurfaceControl. See
+ * {@link SurfaceControl.Transaction#reparent}. This transacton must be applied
+ * or merged in to another transaction by the caller, otherwise it will have
+ * no effect.
+ *
+ * @param child The SurfaceControl to reparent.
+ * @return A new transaction which performs the reparent operation when applied.
+ */
+ @Nullable SurfaceControl.Transaction buildReparentTransaction(@NonNull SurfaceControl child);
+
+ /**
+ * Consume the passed in transaction, and request the View hierarchy to apply it atomically
+ * with the next draw. This transaction will be merged with the buffer transaction from the
+ * ViewRoot and they will show up on-screen atomically synced.
+ *
+ * This will not cause a draw to be scheduled, and if there are no other changes
+ * to the View hierarchy you may need to call {@link android.view.View#invalidate}
+ */
+ boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t);
+}
diff --git a/android/view/BatchedInputEventReceiver.java b/android/view/BatchedInputEventReceiver.java
new file mode 100644
index 0000000..7023e4b
--- /dev/null
+++ b/android/view/BatchedInputEventReceiver.java
@@ -0,0 +1,103 @@
+/*
+ * 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.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Looper;
+
+/**
+ * Similar to {@link InputEventReceiver}, but batches events to vsync boundaries when possible.
+ * @hide
+ */
+public class BatchedInputEventReceiver extends InputEventReceiver {
+ private Choreographer mChoreographer;
+ private boolean mBatchingEnabled;
+ private boolean mBatchedInputScheduled;
+
+ @UnsupportedAppUsage
+ public BatchedInputEventReceiver(
+ InputChannel inputChannel, Looper looper, Choreographer choreographer) {
+ super(inputChannel, looper);
+ mChoreographer = choreographer;
+ mBatchingEnabled = true;
+ }
+
+ @Override
+ public void onBatchedInputEventPending(int source) {
+ if (mBatchingEnabled) {
+ scheduleBatchedInput();
+ } else {
+ consumeBatchedInputEvents(-1);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ unscheduleBatchedInput();
+ consumeBatchedInputEvents(-1);
+ super.dispose();
+ }
+
+ /**
+ * Sets whether to enable batching on this input event receiver.
+ * @hide
+ */
+ public void setBatchingEnabled(boolean batchingEnabled) {
+ mBatchingEnabled = batchingEnabled;
+ if (!batchingEnabled) {
+ unscheduleBatchedInput();
+ consumeBatchedInputEvents(-1);
+ }
+ }
+
+ void doConsumeBatchedInput(long frameTimeNanos) {
+ if (mBatchedInputScheduled) {
+ mBatchedInputScheduled = false;
+ if (consumeBatchedInputEvents(frameTimeNanos) && frameTimeNanos != -1) {
+ // If we consumed a batch here, we want to go ahead and schedule the
+ // consumption of batched input events on the next frame. Otherwise, we would
+ // wait until we have more input events pending and might get starved by other
+ // things occurring in the process. If the frame time is -1, however, then
+ // we're in a non-batching mode, so there's no need to schedule this.
+ scheduleBatchedInput();
+ }
+ }
+ }
+
+ private void scheduleBatchedInput() {
+ if (!mBatchedInputScheduled) {
+ mBatchedInputScheduled = true;
+ mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mBatchedInputRunnable, null);
+ }
+ }
+
+ private void unscheduleBatchedInput() {
+ if (mBatchedInputScheduled) {
+ mBatchedInputScheduled = false;
+ mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_INPUT, mBatchedInputRunnable, null);
+ }
+ }
+
+ private final class BatchedInputRunnable implements Runnable {
+ @Override
+ public void run() {
+ doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
+ }
+ }
+ private final BatchedInputRunnable mBatchedInputRunnable = new BatchedInputRunnable();
+}
diff --git a/android/view/BridgeInflater.java b/android/view/BridgeInflater.java
new file mode 100644
index 0000000..dfb46f2
--- /dev/null
+++ b/android/view/BridgeInflater.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.SdkConstants;
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.ide.common.rendering.api.LayoutlibCallback;
+import com.android.ide.common.rendering.api.MergeCookie;
+import com.android.ide.common.rendering.api.ResourceNamespace;
+import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.BridgeConstants;
+import com.android.layoutlib.bridge.MockView;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
+import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil;
+import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
+import com.android.layoutlib.bridge.impl.ParserFactory;
+import com.android.layoutlib.bridge.util.ReflectionUtils;
+import com.android.tools.layoutlib.annotations.NotNull;
+import com.android.tools.layoutlib.annotations.Nullable;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.ResolvingAttributeSet;
+import android.view.View.OnAttachStateChangeListener;
+import android.widget.ImageView;
+import android.widget.NumberPicker;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
+
+/**
+ * Custom implementation of {@link LayoutInflater} to handle custom views.
+ */
+public final class BridgeInflater extends LayoutInflater {
+ private static final String INFLATER_CLASS_ATTR_NAME = "viewInflaterClass";
+ private static final ResourceReference RES_AUTO_INFLATER_CLASS_ATTR =
+ ResourceReference.attr(ResourceNamespace.RES_AUTO, INFLATER_CLASS_ATTR_NAME);
+ private static final ResourceReference LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR =
+ ResourceReference.attr(ResourceNamespace.APPCOMPAT_LEGACY, INFLATER_CLASS_ATTR_NAME);
+ private static final ResourceReference ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR =
+ ResourceReference.attr(ResourceNamespace.APPCOMPAT, INFLATER_CLASS_ATTR_NAME);
+ private static final String LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME =
+ "android.support.v7.app.AppCompatViewInflater";
+ private static final String ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME =
+ "androidx.appcompat.app.AppCompatViewInflater";
+ private final LayoutlibCallback mLayoutlibCallback;
+
+ private boolean mIsInMerge = false;
+ private ResourceReference mResourceReference;
+ private Map<View, String> mOpenDrawerLayouts;
+
+ // Keep in sync with the same value in LayoutInflater.
+ private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
+
+ /**
+ * List of class prefixes which are tried first by default.
+ * <p/>
+ * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
+ */
+ private static final String[] sClassPrefixList = {
+ "android.widget.",
+ "android.webkit.",
+ "android.app."
+ };
+ private BiFunction<String, AttributeSet, View> mCustomInflater;
+
+ public static String[] getClassPrefixList() {
+ return sClassPrefixList;
+ }
+
+ private BridgeInflater(LayoutInflater original, Context newContext) {
+ super(original, newContext);
+ newContext = getBaseContext(newContext);
+ mLayoutlibCallback = (newContext instanceof BridgeContext) ?
+ ((BridgeContext) newContext).getLayoutlibCallback() :
+ null;
+ }
+
+ /**
+ * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object.
+ *
+ * @param context The Android application context.
+ * @param layoutlibCallback the {@link LayoutlibCallback} object.
+ */
+ public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
+ super(context);
+ mLayoutlibCallback = layoutlibCallback;
+ mConstructorArgs[0] = context;
+ }
+
+ @Override
+ public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
+ View view = createViewFromCustomInflater(name, attrs);
+
+ if (view == null) {
+ try {
+ // First try to find a class using the default Android prefixes
+ for (String prefix : sClassPrefixList) {
+ try {
+ view = createView(name, prefix, attrs);
+ if (view != null) {
+ break;
+ }
+ } catch (ClassNotFoundException e) {
+ // Ignore. We'll try again using the base class below.
+ }
+ }
+
+ // Next try using the parent loader. This will most likely only work for
+ // fully-qualified class names.
+ try {
+ if (view == null) {
+ view = super.onCreateView(name, attrs);
+ }
+ } catch (ClassNotFoundException e) {
+ // Ignore. We'll try again using the custom view loader below.
+ }
+
+ // Finally try again using the custom view loader
+ if (view == null) {
+ view = loadCustomView(name, attrs);
+ }
+ } catch (InflateException e) {
+ // Don't catch the InflateException below as that results in hiding the real cause.
+ throw e;
+ } catch (Exception e) {
+ // Wrap the real exception in a ClassNotFoundException, so that the calling method
+ // can deal with it.
+ throw new ClassNotFoundException("onCreateView", e);
+ }
+ }
+
+ setupViewInContext(view, attrs);
+
+ return view;
+ }
+
+ /**
+ * Finds the createView method in the given customInflaterClass. Since createView is
+ * currently package protected, it will show in the declared class so we iterate up the
+ * hierarchy and return the first instance we find.
+ * The returned method will be accessible.
+ */
+ @NotNull
+ private static Method getCreateViewMethod(Class<?> customInflaterClass) throws NoSuchMethodException {
+ Class<?> current = customInflaterClass;
+ do {
+ try {
+ Method method = current.getDeclaredMethod("createView", View.class, String.class,
+ Context.class, AttributeSet.class, boolean.class, boolean.class,
+ boolean.class, boolean.class);
+ method.setAccessible(true);
+ return method;
+ } catch (NoSuchMethodException ignore) {
+ }
+ current = current.getSuperclass();
+ } while (current != null && current != Object.class);
+
+ throw new NoSuchMethodException();
+ }
+
+ /**
+ * Finds the custom inflater class. If it's defined in the theme, we'll use that one (if the
+ * class does not exist, null is returned).
+ * If {@code viewInflaterClass} is not defined in the theme, we'll try to instantiate
+ * {@code android.support.v7.app.AppCompatViewInflater}
+ */
+ @Nullable
+ private static Class<?> findCustomInflater(@NotNull BridgeContext bc,
+ @NotNull LayoutlibCallback layoutlibCallback) {
+ ResourceReference attrRef;
+ if (layoutlibCallback.isResourceNamespacingRequired()) {
+ if (layoutlibCallback.hasLegacyAppCompat()) {
+ attrRef = LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR;
+ } else if (layoutlibCallback.hasAndroidXAppCompat()) {
+ attrRef = ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR;
+ } else {
+ return null;
+ }
+ } else {
+ attrRef = RES_AUTO_INFLATER_CLASS_ATTR;
+ }
+ ResourceValue value = bc.getRenderResources().findItemInTheme(attrRef);
+ String inflaterName = value != null ? value.getValue() : null;
+
+ if (inflaterName != null) {
+ try {
+ return layoutlibCallback.findClass(inflaterName);
+ } catch (ClassNotFoundException ignore) {
+ }
+
+ // viewInflaterClass was defined but we couldn't find the class.
+ } else if (bc.isAppCompatTheme()) {
+ // Older versions of AppCompat do not define the viewInflaterClass so try to get it
+ // manually.
+ try {
+ if (layoutlibCallback.hasLegacyAppCompat()) {
+ return layoutlibCallback.findClass(LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME);
+ } else if (layoutlibCallback.hasAndroidXAppCompat()) {
+ return layoutlibCallback.findClass(ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME);
+ }
+ } catch (ClassNotFoundException ignore) {
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if there is a custom inflater and, when present, tries to instantiate the view
+ * using it.
+ */
+ @Nullable
+ private View createViewFromCustomInflater(@NotNull String name, @NotNull AttributeSet attrs) {
+ if (mCustomInflater == null) {
+ Context context = getContext();
+ context = getBaseContext(context);
+ if (context instanceof BridgeContext) {
+ BridgeContext bc = (BridgeContext) context;
+ Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback);
+
+ if (inflaterClass != null) {
+ try {
+ Constructor<?> constructor = inflaterClass.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Object inflater = constructor.newInstance();
+ Method method = getCreateViewMethod(inflaterClass);
+ mCustomInflater = (viewName, attributeSet) -> {
+ try {
+ return (View) method.invoke(inflater, null, viewName,
+ mConstructorArgs[0],
+ attributeSet,
+ false,
+ false /*readAndroidTheme*/, // No need after L
+ true /*readAppTheme*/,
+ true /*wrapContext*/);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, e.getMessage(), e,
+ null, null);
+ }
+ return null;
+ };
+ } catch (InvocationTargetException | IllegalAccessException |
+ NoSuchMethodException | InstantiationException ignore) {
+ }
+ }
+ }
+
+ if (mCustomInflater == null) {
+ // There is no custom inflater. We'll create a nop custom inflater to avoid the
+ // penalty of trying to instantiate again
+ mCustomInflater = (s, attributeSet) -> null;
+ }
+ }
+
+ return mCustomInflater.apply(name, attrs);
+ }
+
+ @Override
+ public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
+ boolean ignoreThemeAttr) {
+ View view = null;
+ if (name.equals("view")) {
+ // This is usually done by the superclass but this allows us catching the error and
+ // reporting something useful.
+ name = attrs.getAttributeValue(null, "class");
+
+ if (name == null) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "Unable to inflate view tag without " +
+ "class attribute", null, null);
+ // We weren't able to resolve the view so we just pass a mock View to be able to
+ // continue rendering.
+ view = new MockView(context, attrs);
+ ((MockView) view).setText("view");
+ }
+ }
+
+ try {
+ if (view == null) {
+ view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr);
+ }
+ } catch (InflateException e) {
+ // Creation of ContextThemeWrapper code is same as in the super method.
+ // Apply a theme wrapper, if allowed and one is specified.
+ if (!ignoreThemeAttr) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
+ final int themeResId = ta.getResourceId(0, 0);
+ if (themeResId != 0) {
+ context = new ContextThemeWrapper(context, themeResId);
+ }
+ ta.recycle();
+ }
+ if (!(e.getCause() instanceof ClassNotFoundException)) {
+ // There is some unknown inflation exception in inflating a View that was found.
+ view = new MockView(context, attrs);
+ ((MockView) view).setText(name);
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, e.getMessage(), e, null, null);
+ } else {
+ final Object lastContext = mConstructorArgs[0];
+ mConstructorArgs[0] = context;
+ // try to load the class from using the custom view loader
+ try {
+ view = loadCustomView(name, attrs);
+ } catch (Exception e2) {
+ // Wrap the real exception in an InflateException so that the calling
+ // method can deal with it.
+ InflateException exception = new InflateException();
+ if (!e2.getClass().equals(ClassNotFoundException.class)) {
+ exception.initCause(e2);
+ } else {
+ exception.initCause(e);
+ }
+ throw exception;
+ } finally {
+ mConstructorArgs[0] = lastContext;
+ }
+ }
+ }
+
+ setupViewInContext(view, attrs);
+
+ return view;
+ }
+
+ @Override
+ public View inflate(int resource, ViewGroup root) {
+ Context context = getContext();
+ context = getBaseContext(context);
+ if (context instanceof BridgeContext) {
+ BridgeContext bridgeContext = (BridgeContext)context;
+
+ ResourceValue value = null;
+
+ ResourceReference layoutInfo = Bridge.resolveResourceId(resource);
+ if (layoutInfo == null) {
+ layoutInfo = mLayoutlibCallback.resolveResourceId(resource);
+
+ }
+ if (layoutInfo != null) {
+ value = bridgeContext.getRenderResources().getResolvedResource(layoutInfo);
+ }
+
+ if (value != null) {
+ String path = value.getValue();
+ try {
+ XmlPullParser parser = ParserFactory.create(path, true);
+ if (parser == null) {
+ return null;
+ }
+
+ BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
+ parser, bridgeContext, value.getNamespace());
+
+ return inflate(bridgeParser, root);
+ } catch (Exception e) {
+ Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ,
+ "Failed to parse file " + path, e, null, null);
+
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Instantiates the given view name and returns the instance. If the view doesn't exist, a
+ * MockView or null might be returned.
+ * @param name the custom view name
+ * @param attrs the {@link AttributeSet} to be passed to the view constructor
+ * @param silent if true, errors while loading the view won't be reported and, if the view
+ * doesn't exist, null will be returned.
+ */
+ private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception {
+ if (mLayoutlibCallback != null) {
+ // first get the classname in case it's not the node name
+ if (name.equals("view")) {
+ name = attrs.getAttributeValue(null, "class");
+ if (name == null) {
+ return null;
+ }
+ }
+
+ mConstructorArgs[1] = attrs;
+
+ Object customView = silent ?
+ mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs)
+ : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs);
+
+ if (customView instanceof View) {
+ return (View)customView;
+ }
+ }
+
+ return null;
+ }
+
+ private View loadCustomView(String name, AttributeSet attrs) throws Exception {
+ return loadCustomView(name, attrs, false);
+ }
+
+ private void setupViewInContext(View view, AttributeSet attrs) {
+ Context context = getContext();
+ context = getBaseContext(context);
+ if (!(context instanceof BridgeContext)) {
+ return;
+ }
+
+ BridgeContext bc = (BridgeContext) context;
+ // get the view key
+ Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge);
+ if (viewKey != null) {
+ bc.addViewKey(view, viewKey);
+ }
+ String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX");
+ if (scrollPosX != null && scrollPosX.endsWith("px")) {
+ int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2));
+ bc.setScrollXPos(view, value);
+ }
+ String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY");
+ if (scrollPosY != null && scrollPosY.endsWith("px")) {
+ int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2));
+ bc.setScrollYPos(view, value);
+ }
+ if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
+ int resourceId = 0;
+ int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI,
+ BridgeConstants.ATTR_ITEM_COUNT, -1);
+ if (attrs instanceof ResolvingAttributeSet) {
+ ResourceValue attrListItemValue =
+ ((ResolvingAttributeSet) attrs).getResolvedAttributeValue(
+ BridgeConstants.NS_TOOLS_URI, BridgeConstants.ATTR_LIST_ITEM);
+ if (attrListItemValue != null) {
+ resourceId = bc.getResourceId(attrListItemValue.asReference(), 0);
+ }
+ }
+ RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue);
+ } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) {
+ String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
+ BridgeConstants.ATTR_OPEN_DRAWER);
+ if (attrVal != null) {
+ getDrawerLayoutMap().put(view, attrVal);
+ }
+ }
+ else if (view instanceof NumberPicker) {
+ NumberPicker numberPicker = (NumberPicker) view;
+ String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue");
+ if (minValue != null) {
+ numberPicker.setMinValue(Integer.parseInt(minValue));
+ }
+ String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue");
+ if (maxValue != null) {
+ numberPicker.setMaxValue(Integer.parseInt(maxValue));
+ }
+ }
+ else if (view instanceof ImageView) {
+ ImageView img = (ImageView) view;
+ Drawable drawable = img.getDrawable();
+ if (drawable instanceof Animatable) {
+ if (!((Animatable) drawable).isRunning()) {
+ ((Animatable) drawable).start();
+ }
+ }
+ }
+ else if (view instanceof ViewStub) {
+ // By default, ViewStub will be set to GONE and won't be inflate. If the XML has the
+ // tools:visibility attribute we'll workaround that behavior.
+ String visibility = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
+ SdkConstants.ATTR_VISIBILITY);
+
+ boolean isVisible = "visible".equals(visibility);
+ if (isVisible || "invisible".equals(visibility)) {
+ // We can not inflate the view until is attached to its parent so we need to delay
+ // the setVisible call until after that happens.
+ final int visibilityValue = isVisible ? View.VISIBLE : View.INVISIBLE;
+ view.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ v.removeOnAttachStateChangeListener(this);
+ view.setVisibility(visibilityValue);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {}
+ });
+ }
+ }
+
+ }
+
+ public void setIsInMerge(boolean isInMerge) {
+ mIsInMerge = isInMerge;
+ }
+
+ public void setResourceReference(ResourceReference reference) {
+ mResourceReference = reference;
+ }
+
+ @Override
+ public LayoutInflater cloneInContext(Context newContext) {
+ return new BridgeInflater(this, newContext);
+ }
+
+ /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc,
+ ResourceReference resourceReference, boolean isInMerge) {
+
+ if (!(attrs instanceof BridgeXmlBlockParser)) {
+ return null;
+ }
+ BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs);
+
+ // get the view key
+ Object viewKey = parser.getViewCookie();
+
+ if (viewKey == null) {
+ int currentDepth = parser.getDepth();
+
+ // test whether we are in an included file or in a adapter binding view.
+ BridgeXmlBlockParser previousParser = bc.getPreviousParser();
+ if (previousParser != null) {
+ // looks like we are inside an embedded layout.
+ // only apply the cookie of the calling node (<include>) if we are at the
+ // top level of the embedded layout. If there is a merge tag, then
+ // skip it and look for the 2nd level
+ int testDepth = isInMerge ? 2 : 1;
+ if (currentDepth == testDepth) {
+ viewKey = previousParser.getViewCookie();
+ // if we are in a merge, wrap the cookie in a MergeCookie.
+ if (viewKey != null && isInMerge) {
+ viewKey = new MergeCookie(viewKey);
+ }
+ }
+ } else if (resourceReference != null && currentDepth == 1) {
+ // else if there's a resource reference, this means we are in an adapter
+ // binding case. Set the resource ref as the view cookie only for the top
+ // level view.
+ viewKey = resourceReference;
+ }
+ }
+
+ return viewKey;
+ }
+
+ public void postInflateProcess(View view) {
+ if (mOpenDrawerLayouts != null) {
+ String gravity = mOpenDrawerLayouts.get(view);
+ if (gravity != null) {
+ DrawerLayoutUtil.openDrawer(view, gravity);
+ }
+ mOpenDrawerLayouts.remove(view);
+ }
+ }
+
+ @NonNull
+ private Map<View, String> getDrawerLayoutMap() {
+ if (mOpenDrawerLayouts == null) {
+ mOpenDrawerLayouts = new HashMap<>(4);
+ }
+ return mOpenDrawerLayouts;
+ }
+
+ public void onDoneInflation() {
+ if (mOpenDrawerLayouts != null) {
+ mOpenDrawerLayouts.clear();
+ }
+ }
+}
diff --git a/android/view/Choreographer.java b/android/view/Choreographer.java
new file mode 100644
index 0000000..be172f7
--- /dev/null
+++ b/android/view/Choreographer.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.DisplayEventReceiver.VSYNC_SOURCE_APP;
+import static android.view.DisplayEventReceiver.VSYNC_SOURCE_SURFACE_FLINGER;
+
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.FrameInfo;
+import android.graphics.Insets;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.util.Log;
+import android.util.TimeUtils;
+import android.view.animation.AnimationUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Coordinates the timing of animations, input and drawing.
+ * <p>
+ * The choreographer receives timing pulses (such as vertical synchronization)
+ * from the display subsystem then schedules work to occur as part of rendering
+ * the next display frame.
+ * </p><p>
+ * Applications typically interact with the choreographer indirectly using
+ * higher level abstractions in the animation framework or the view hierarchy.
+ * Here are some examples of things you can do using the higher-level APIs.
+ * </p>
+ * <ul>
+ * <li>To post an animation to be processed on a regular time basis synchronized with
+ * display frame rendering, use {@link android.animation.ValueAnimator#start}.</li>
+ * <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
+ * frame, use {@link View#postOnAnimation}.</li>
+ * <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
+ * frame after a delay, use {@link View#postOnAnimationDelayed}.</li>
+ * <li>To post a call to {@link View#invalidate()} to occur once at the beginning of the
+ * next display frame, use {@link View#postInvalidateOnAnimation()} or
+ * {@link View#postInvalidateOnAnimation(int, int, int, int)}.</li>
+ * <li>To ensure that the contents of a {@link View} scroll smoothly and are drawn in
+ * sync with display frame rendering, do nothing. This already happens automatically.
+ * {@link View#onDraw} will be called at the appropriate time.</li>
+ * </ul>
+ * <p>
+ * However, there are a few cases where you might want to use the functions of the
+ * choreographer directly in your application. Here are some examples.
+ * </p>
+ * <ul>
+ * <li>If your application does its rendering in a different thread, possibly using GL,
+ * or does not use the animation framework or view hierarchy at all
+ * and you want to ensure that it is appropriately synchronized with the display, then use
+ * {@link Choreographer#postFrameCallback}.</li>
+ * <li>... and that's about it.</li>
+ * </ul>
+ * <p>
+ * Each {@link Looper} thread has its own choreographer. Other threads can
+ * post callbacks to run on the choreographer but they will run on the {@link Looper}
+ * to which the choreographer belongs.
+ * </p>
+ */
+public final class Choreographer {
+ private static final String TAG = "Choreographer";
+
+ // Prints debug messages about jank which was detected (low volume).
+ private static final boolean DEBUG_JANK = false;
+
+ // Prints debug messages about every frame and callback registered (high volume).
+ private static final boolean DEBUG_FRAMES = false;
+
+ // The default amount of time in ms between animation frames.
+ // When vsync is not enabled, we want to have some idea of how long we should
+ // wait before posting the next animation message. It is important that the
+ // default value be less than the true inter-frame delay on all devices to avoid
+ // situations where we might skip frames by waiting too long (we must compensate
+ // for jitter and hardware variations). Regardless of this value, the animation
+ // and display loop is ultimately rate-limited by how fast new graphics buffers can
+ // be dequeued.
+ private static final long DEFAULT_FRAME_DELAY = 10;
+
+ // The number of milliseconds between animation frames.
+ private static volatile long sFrameDelay = DEFAULT_FRAME_DELAY;
+
+ // Thread local storage for the choreographer.
+ private static final ThreadLocal<Choreographer> sThreadInstance =
+ new ThreadLocal<Choreographer>() {
+ @Override
+ protected Choreographer initialValue() {
+ Looper looper = Looper.myLooper();
+ if (looper == null) {
+ throw new IllegalStateException("The current thread must have a looper!");
+ }
+ Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
+ if (looper == Looper.getMainLooper()) {
+ mMainInstance = choreographer;
+ }
+ return choreographer;
+ }
+ };
+
+ private static volatile Choreographer mMainInstance;
+
+ // Thread local storage for the SF choreographer.
+ private static final ThreadLocal<Choreographer> sSfThreadInstance =
+ new ThreadLocal<Choreographer>() {
+ @Override
+ protected Choreographer initialValue() {
+ Looper looper = Looper.myLooper();
+ if (looper == null) {
+ throw new IllegalStateException("The current thread must have a looper!");
+ }
+ return new Choreographer(looper, VSYNC_SOURCE_SURFACE_FLINGER);
+ }
+ };
+
+ // Enable/disable vsync for animations and drawing.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769497)
+ private static final boolean USE_VSYNC = SystemProperties.getBoolean(
+ "debug.choreographer.vsync", true);
+
+ // Enable/disable using the frame time instead of returning now.
+ private static final boolean USE_FRAME_TIME = SystemProperties.getBoolean(
+ "debug.choreographer.frametime", true);
+
+ // Set a limit to warn about skipped frames.
+ // Skipped frames imply jank.
+ private static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt(
+ "debug.choreographer.skipwarning", 30);
+
+ private static final int MSG_DO_FRAME = 0;
+ private static final int MSG_DO_SCHEDULE_VSYNC = 1;
+ private static final int MSG_DO_SCHEDULE_CALLBACK = 2;
+
+ // All frame callbacks posted by applications have this token.
+ private static final Object FRAME_CALLBACK_TOKEN = new Object() {
+ public String toString() { return "FRAME_CALLBACK_TOKEN"; }
+ };
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final Object mLock = new Object();
+
+ private final Looper mLooper;
+ private final FrameHandler mHandler;
+
+ // The display event receiver can only be accessed by the looper thread to which
+ // it is attached. We take care to ensure that we post message to the looper
+ // if appropriate when interacting with the display event receiver.
+ @UnsupportedAppUsage
+ private final FrameDisplayEventReceiver mDisplayEventReceiver;
+
+ private CallbackRecord mCallbackPool;
+
+ @UnsupportedAppUsage
+ private final CallbackQueue[] mCallbackQueues;
+
+ private boolean mFrameScheduled;
+ private boolean mCallbacksRunning;
+ @UnsupportedAppUsage
+ private long mLastFrameTimeNanos;
+
+ /** DO NOT USE since this will not updated when screen refresh changes. */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R,
+ publicAlternatives = "Use {@link android.view.Display#getRefreshRate} instead")
+ @Deprecated
+ private long mFrameIntervalNanos;
+ private long mLastFrameIntervalNanos;
+
+ private boolean mDebugPrintNextFrameTimeDelta;
+ private int mFPSDivisor = 1;
+ private DisplayEventReceiver.VsyncEventData mLastVsyncEventData =
+ new DisplayEventReceiver.VsyncEventData();
+
+ /**
+ * Contains information about the current frame for jank-tracking,
+ * mainly timings of key events along with a bit of metadata about
+ * view tree state
+ *
+ * TODO: Is there a better home for this? Currently Choreographer
+ * is the only one with CALLBACK_ANIMATION start time, hence why this
+ * resides here.
+ *
+ * @hide
+ */
+ FrameInfo mFrameInfo = new FrameInfo();
+
+ /**
+ * Must be kept in sync with CALLBACK_* ints below, used to index into this array.
+ * @hide
+ */
+ private static final String[] CALLBACK_TRACE_TITLES = {
+ "input", "animation", "insets_animation", "traversal", "commit"
+ };
+
+ /**
+ * Callback type: Input callback. Runs first.
+ * @hide
+ */
+ public static final int CALLBACK_INPUT = 0;
+
+ /**
+ * Callback type: Animation callback. Runs before {@link #CALLBACK_INSETS_ANIMATION}.
+ * @hide
+ */
+ @TestApi
+ public static final int CALLBACK_ANIMATION = 1;
+
+ /**
+ * Callback type: Animation callback to handle inset updates. This is separate from
+ * {@link #CALLBACK_ANIMATION} as we need to "gather" all inset animation updates via
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)} for multiple
+ * ongoing animations but then update the whole view system with a single callback to
+ * {@link View#dispatchWindowInsetsAnimationProgress} that contains all the combined updated
+ * insets.
+ * <p>
+ * Both input and animation may change insets, so we need to run this after these callbacks, but
+ * before traversals.
+ * <p>
+ * Runs before traversals.
+ * @hide
+ */
+ public static final int CALLBACK_INSETS_ANIMATION = 2;
+
+ /**
+ * Callback type: Traversal callback. Handles layout and draw. Runs
+ * after all other asynchronous messages have been handled.
+ * @hide
+ */
+ public static final int CALLBACK_TRAVERSAL = 3;
+
+ /**
+ * Callback type: Commit callback. Handles post-draw operations for the frame.
+ * Runs after traversal completes. The {@link #getFrameTime() frame time} reported
+ * during this callback may be updated to reflect delays that occurred while
+ * traversals were in progress in case heavy layout operations caused some frames
+ * to be skipped. The frame time reported during this callback provides a better
+ * estimate of the start time of the frame in which animations (and other updates
+ * to the view hierarchy state) actually took effect.
+ * @hide
+ */
+ public static final int CALLBACK_COMMIT = 4;
+
+ private static final int CALLBACK_LAST = CALLBACK_COMMIT;
+
+ private Choreographer(Looper looper, int vsyncSource) {
+ mLooper = looper;
+ mHandler = new FrameHandler(looper);
+ mDisplayEventReceiver = USE_VSYNC
+ ? new FrameDisplayEventReceiver(looper, vsyncSource)
+ : null;
+ mLastFrameTimeNanos = Long.MIN_VALUE;
+
+ mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
+
+ mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
+ for (int i = 0; i <= CALLBACK_LAST; i++) {
+ mCallbackQueues[i] = new CallbackQueue();
+ }
+ // b/68769804: For low FPS experiments.
+ setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
+ }
+
+ private static float getRefreshRate() {
+ DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
+ Display.DEFAULT_DISPLAY);
+ return di.getRefreshRate();
+ }
+
+ /**
+ * Gets the choreographer for the calling thread. Must be called from
+ * a thread that already has a {@link android.os.Looper} associated with it.
+ *
+ * @return The choreographer for this thread.
+ * @throws IllegalStateException if the thread does not have a looper.
+ */
+ public static Choreographer getInstance() {
+ return sThreadInstance.get();
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static Choreographer getSfInstance() {
+ return sSfThreadInstance.get();
+ }
+
+ /**
+ * @return The Choreographer of the main thread, if it exists, or {@code null} otherwise.
+ * @hide
+ */
+ public static Choreographer getMainThreadInstance() {
+ return mMainInstance;
+ }
+
+ /** Destroys the calling thread's choreographer
+ * @hide
+ */
+ public static void releaseInstance() {
+ Choreographer old = sThreadInstance.get();
+ sThreadInstance.remove();
+ old.dispose();
+ }
+
+ private void dispose() {
+ mDisplayEventReceiver.dispose();
+ }
+
+ /**
+ * The amount of time, in milliseconds, between each frame of the animation.
+ * <p>
+ * This is a requested time that the animation will attempt to honor, but the actual delay
+ * between frames may be different, depending on system load and capabilities. This is a static
+ * function because the same delay will be applied to all animations, since they are all
+ * run off of a single timing loop.
+ * </p><p>
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ * </p>
+ *
+ * @return the requested time between frames, in milliseconds
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public static long getFrameDelay() {
+ return sFrameDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, between each frame of the animation.
+ * <p>
+ * This is a requested time that the animation will attempt to honor, but the actual delay
+ * between frames may be different, depending on system load and capabilities. This is a static
+ * function because the same delay will be applied to all animations, since they are all
+ * run off of a single timing loop.
+ * </p><p>
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ * </p>
+ *
+ * @param frameDelay the requested time between frames, in milliseconds
+ * @hide
+ */
+ @TestApi
+ public static void setFrameDelay(long frameDelay) {
+ sFrameDelay = frameDelay;
+ }
+
+ /**
+ * Subtracts typical frame delay time from a delay interval in milliseconds.
+ * <p>
+ * This method can be used to compensate for animation delay times that have baked
+ * in assumptions about the frame delay. For example, it's quite common for code to
+ * assume a 60Hz frame time and bake in a 16ms delay. When we call
+ * {@link #postAnimationCallbackDelayed} we want to know how long to wait before
+ * posting the animation callback but let the animation timer take care of the remaining
+ * frame delay time.
+ * </p><p>
+ * This method is somewhat conservative about how much of the frame delay it
+ * subtracts. It uses the same value returned by {@link #getFrameDelay} which by
+ * default is 10ms even though many parts of the system assume 16ms. Consequently,
+ * we might still wait 6ms before posting an animation callback that we want to run
+ * on the next frame, but this is much better than waiting a whole 16ms and likely
+ * missing the deadline.
+ * </p>
+ *
+ * @param delayMillis The original delay time including an assumed frame delay.
+ * @return The adjusted delay time with the assumed frame delay subtracted out.
+ * @hide
+ */
+ public static long subtractFrameDelay(long delayMillis) {
+ final long frameDelay = sFrameDelay;
+ return delayMillis <= frameDelay ? 0 : delayMillis - frameDelay;
+ }
+
+ /**
+ * @return The refresh rate as the nanoseconds between frames
+ * @hide
+ */
+ public long getFrameIntervalNanos() {
+ synchronized (mLock) {
+ return mLastFrameIntervalNanos;
+ }
+ }
+
+ void dump(String prefix, PrintWriter writer) {
+ String innerPrefix = prefix + " ";
+ writer.print(prefix); writer.println("Choreographer:");
+ writer.print(innerPrefix); writer.print("mFrameScheduled=");
+ writer.println(mFrameScheduled);
+ writer.print(innerPrefix); writer.print("mLastFrameTime=");
+ writer.println(TimeUtils.formatUptime(mLastFrameTimeNanos / 1000000));
+ }
+
+ /**
+ * Posts a callback to run on the next frame.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
+ *
+ * @param callbackType The callback type.
+ * @param action The callback action to run during the next frame.
+ * @param token The callback token, or null if none.
+ *
+ * @see #removeCallbacks
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public void postCallback(int callbackType, Runnable action, Object token) {
+ postCallbackDelayed(callbackType, action, token, 0);
+ }
+
+ /**
+ * Posts a callback to run on the next frame after the specified delay.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
+ *
+ * @param callbackType The callback type.
+ * @param action The callback action to run during the next frame after the specified delay.
+ * @param token The callback token, or null if none.
+ * @param delayMillis The delay time in milliseconds.
+ *
+ * @see #removeCallback
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public void postCallbackDelayed(int callbackType,
+ Runnable action, Object token, long delayMillis) {
+ if (action == null) {
+ throw new IllegalArgumentException("action must not be null");
+ }
+ if (callbackType < 0 || callbackType > CALLBACK_LAST) {
+ throw new IllegalArgumentException("callbackType is invalid");
+ }
+
+ postCallbackDelayedInternal(callbackType, action, token, delayMillis);
+ }
+
+ private void postCallbackDelayedInternal(int callbackType,
+ Object action, Object token, long delayMillis) {
+ if (DEBUG_FRAMES) {
+ Log.d(TAG, "PostCallback: type=" + callbackType
+ + ", action=" + action + ", token=" + token
+ + ", delayMillis=" + delayMillis);
+ }
+
+ synchronized (mLock) {
+ final long now = SystemClock.uptimeMillis();
+ final long dueTime = now + delayMillis;
+ mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
+
+ if (dueTime <= now) {
+ scheduleFrameLocked(now);
+ } else {
+ Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
+ msg.arg1 = callbackType;
+ msg.setAsynchronous(true);
+ mHandler.sendMessageAtTime(msg, dueTime);
+ }
+ }
+ }
+
+ /**
+ * Removes callbacks that have the specified action and token.
+ *
+ * @param callbackType The callback type.
+ * @param action The action property of the callbacks to remove, or null to remove
+ * callbacks with any action.
+ * @param token The token property of the callbacks to remove, or null to remove
+ * callbacks with any token.
+ *
+ * @see #postCallback
+ * @see #postCallbackDelayed
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public void removeCallbacks(int callbackType, Runnable action, Object token) {
+ if (callbackType < 0 || callbackType > CALLBACK_LAST) {
+ throw new IllegalArgumentException("callbackType is invalid");
+ }
+
+ removeCallbacksInternal(callbackType, action, token);
+ }
+
+ private void removeCallbacksInternal(int callbackType, Object action, Object token) {
+ if (DEBUG_FRAMES) {
+ Log.d(TAG, "RemoveCallbacks: type=" + callbackType
+ + ", action=" + action + ", token=" + token);
+ }
+
+ synchronized (mLock) {
+ mCallbackQueues[callbackType].removeCallbacksLocked(action, token);
+ if (action != null && token == null) {
+ mHandler.removeMessages(MSG_DO_SCHEDULE_CALLBACK, action);
+ }
+ }
+ }
+
+ /**
+ * Posts a frame callback to run on the next frame.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
+ *
+ * @param callback The frame callback to run during the next frame.
+ *
+ * @see #postFrameCallbackDelayed
+ * @see #removeFrameCallback
+ */
+ public void postFrameCallback(FrameCallback callback) {
+ postFrameCallbackDelayed(callback, 0);
+ }
+
+ /**
+ * Posts a frame callback to run on the next frame after the specified delay.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
+ *
+ * @param callback The frame callback to run during the next frame.
+ * @param delayMillis The delay time in milliseconds.
+ *
+ * @see #postFrameCallback
+ * @see #removeFrameCallback
+ */
+ public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+
+ postCallbackDelayedInternal(CALLBACK_ANIMATION,
+ callback, FRAME_CALLBACK_TOKEN, delayMillis);
+ }
+
+ /**
+ * Removes a previously posted frame callback.
+ *
+ * @param callback The frame callback to remove.
+ *
+ * @see #postFrameCallback
+ * @see #postFrameCallbackDelayed
+ */
+ public void removeFrameCallback(FrameCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+
+ removeCallbacksInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN);
+ }
+
+ /**
+ * Gets the time when the current frame started.
+ * <p>
+ * This method provides the time in milliseconds when the frame started being rendered.
+ * The frame time provides a stable time base for synchronizing animations
+ * and drawing. It should be used instead of {@link SystemClock#uptimeMillis()}
+ * or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame
+ * time helps to reduce inter-frame jitter because the frame time is fixed at the time
+ * the frame was scheduled to start, regardless of when the animations or drawing
+ * callback actually runs. All callbacks that run as part of rendering a frame will
+ * observe the same frame time so using the frame time also helps to synchronize effects
+ * that are performed by different callbacks.
+ * </p><p>
+ * Please note that the framework already takes care to process animations and
+ * drawing using the frame time as a stable time base. Most applications should
+ * not need to use the frame time information directly.
+ * </p><p>
+ * This method should only be called from within a callback.
+ * </p>
+ *
+ * @return The frame start time, in the {@link SystemClock#uptimeMillis()} time base.
+ *
+ * @throws IllegalStateException if no frame is in progress.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public long getFrameTime() {
+ return getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
+ }
+
+ /**
+ * Same as {@link #getFrameTime()} but with nanosecond precision.
+ *
+ * @return The frame start time, in the {@link System#nanoTime()} time base.
+ *
+ * @throws IllegalStateException if no frame is in progress.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public long getFrameTimeNanos() {
+ synchronized (mLock) {
+ if (!mCallbacksRunning) {
+ throw new IllegalStateException("This method must only be called as "
+ + "part of a callback while a frame is in progress.");
+ }
+ return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime();
+ }
+ }
+
+ /**
+ * Like {@link #getLastFrameTimeNanos}, but always returns the last frame time, not matter
+ * whether callbacks are currently running.
+ * @return The frame start time of the last frame, in the {@link System#nanoTime()} time base.
+ * @hide
+ */
+ public long getLastFrameTimeNanos() {
+ synchronized (mLock) {
+ return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime();
+ }
+ }
+
+ private void scheduleFrameLocked(long now) {
+ if (!mFrameScheduled) {
+ mFrameScheduled = true;
+ if (USE_VSYNC) {
+ if (DEBUG_FRAMES) {
+ Log.d(TAG, "Scheduling next frame on vsync.");
+ }
+
+ // If running on the Looper thread, then schedule the vsync immediately,
+ // otherwise post a message to schedule the vsync from the UI thread
+ // as soon as possible.
+ if (isRunningOnLooperThreadLocked()) {
+ scheduleVsyncLocked();
+ } else {
+ Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageAtFrontOfQueue(msg);
+ }
+ } else {
+ final long nextFrameTime = Math.max(
+ mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
+ if (DEBUG_FRAMES) {
+ Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
+ }
+ Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageAtTime(msg, nextFrameTime);
+ }
+ }
+ }
+
+ /**
+ * Returns the vsync id of the last frame callback. Client are expected to call
+ * this function from their frame callback function to get the vsyncId and pass
+ * it together with a buffer or transaction to the Surface Composer. Calling
+ * this function from anywhere else will return an undefined value.
+ *
+ * @hide
+ */
+ public long getVsyncId() {
+ return mLastVsyncEventData.id;
+ }
+
+ /**
+ * Returns the frame deadline in {@link System#nanoTime()} timebase that it is allotted for the
+ * frame to be completed. Client are expected to call this function from their frame callback
+ * function. Calling this function from anywhere else will return an undefined value.
+ *
+ * @hide
+ */
+ public long getFrameDeadline() {
+ return mLastVsyncEventData.frameDeadline;
+ }
+
+ void setFPSDivisor(int divisor) {
+ if (divisor <= 0) divisor = 1;
+ mFPSDivisor = divisor;
+ ThreadedRenderer.setFPSDivisor(divisor);
+ }
+
+ private void traceMessage(String msg) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, msg);
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ void doFrame(long frameTimeNanos, int frame,
+ DisplayEventReceiver.VsyncEventData vsyncEventData) {
+ final long startNanos;
+ final long frameIntervalNanos = vsyncEventData.frameInterval;
+ try {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "Choreographer#doFrame " + vsyncEventData.id);
+ }
+ synchronized (mLock) {
+ if (!mFrameScheduled) {
+ traceMessage("Frame not scheduled");
+ return; // no work to do
+ }
+
+ if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
+ mDebugPrintNextFrameTimeDelta = false;
+ Log.d(TAG, "Frame time delta: "
+ + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
+ }
+
+ long intendedFrameTimeNanos = frameTimeNanos;
+ startNanos = System.nanoTime();
+ final long jitterNanos = startNanos - frameTimeNanos;
+ if (jitterNanos >= frameIntervalNanos) {
+ final long skippedFrames = jitterNanos / frameIntervalNanos;
+ if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
+ Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ + "The application may be doing too much work on its main thread.");
+ }
+ final long lastFrameOffset = jitterNanos % frameIntervalNanos;
+ if (DEBUG_JANK) {
+ Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ + "which is more than the frame interval of "
+ + (frameIntervalNanos * 0.000001f) + " ms! "
+ + "Skipping " + skippedFrames + " frames and setting frame "
+ + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
+ }
+ frameTimeNanos = startNanos - lastFrameOffset;
+ }
+
+ if (frameTimeNanos < mLastFrameTimeNanos) {
+ if (DEBUG_JANK) {
+ Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ + "previously skipped frame. Waiting for next vsync.");
+ }
+ traceMessage("Frame time goes backward");
+ scheduleVsyncLocked();
+ return;
+ }
+
+ if (mFPSDivisor > 1) {
+ long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
+ if (timeSinceVsync < (frameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
+ traceMessage("Frame skipped due to FPSDivisor");
+ scheduleVsyncLocked();
+ return;
+ }
+ }
+
+ mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos, vsyncEventData.id,
+ vsyncEventData.frameDeadline, startNanos, vsyncEventData.frameInterval);
+ mFrameScheduled = false;
+ mLastFrameTimeNanos = frameTimeNanos;
+ mLastFrameIntervalNanos = frameIntervalNanos;
+ mLastVsyncEventData = vsyncEventData;
+ }
+
+ AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
+
+ mFrameInfo.markInputHandlingStart();
+ doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, frameIntervalNanos);
+
+ mFrameInfo.markAnimationsStart();
+ doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, frameIntervalNanos);
+ doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos,
+ frameIntervalNanos);
+
+ mFrameInfo.markPerformTraversalsStart();
+ doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);
+
+ doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, frameIntervalNanos);
+ } finally {
+ AnimationUtils.unlockAnimationClock();
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ if (DEBUG_FRAMES) {
+ final long endNanos = System.nanoTime();
+ Log.d(TAG, "Frame " + frame + ": Finished, took "
+ + (endNanos - startNanos) * 0.000001f + " ms, latency "
+ + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
+ }
+ }
+
+ void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
+ CallbackRecord callbacks;
+ synchronized (mLock) {
+ // We use "now" to determine when callbacks become due because it's possible
+ // for earlier processing phases in a frame to post callbacks that should run
+ // in a following phase, such as an input event that causes an animation to start.
+ final long now = System.nanoTime();
+ callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
+ now / TimeUtils.NANOS_PER_MS);
+ if (callbacks == null) {
+ return;
+ }
+ mCallbacksRunning = true;
+
+ // Update the frame time if necessary when committing the frame.
+ // We only update the frame time if we are more than 2 frames late reaching
+ // the commit phase. This ensures that the frame time which is observed by the
+ // callbacks will always increase from one frame to the next and never repeat.
+ // We never want the next frame's starting frame time to end up being less than
+ // or equal to the previous frame's commit frame time. Keep in mind that the
+ // next frame has most likely already been scheduled by now so we play it
+ // safe by ensuring the commit time is always at least one frame behind.
+ if (callbackType == Choreographer.CALLBACK_COMMIT) {
+ final long jitterNanos = now - frameTimeNanos;
+ Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
+ if (jitterNanos >= 2 * frameIntervalNanos) {
+ final long lastFrameOffset = jitterNanos % frameIntervalNanos
+ + frameIntervalNanos;
+ if (DEBUG_JANK) {
+ Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
+ + " ms which is more than twice the frame interval of "
+ + (frameIntervalNanos * 0.000001f) + " ms! "
+ + "Setting frame time to " + (lastFrameOffset * 0.000001f)
+ + " ms in the past.");
+ mDebugPrintNextFrameTimeDelta = true;
+ }
+ frameTimeNanos = now - lastFrameOffset;
+ mLastFrameTimeNanos = frameTimeNanos;
+ }
+ }
+ }
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
+ for (CallbackRecord c = callbacks; c != null; c = c.next) {
+ if (DEBUG_FRAMES) {
+ Log.d(TAG, "RunCallback: type=" + callbackType
+ + ", action=" + c.action + ", token=" + c.token
+ + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
+ }
+ c.run(frameTimeNanos);
+ }
+ } finally {
+ synchronized (mLock) {
+ mCallbacksRunning = false;
+ do {
+ final CallbackRecord next = callbacks.next;
+ recycleCallbackLocked(callbacks);
+ callbacks = next;
+ } while (callbacks != null);
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ void doScheduleVsync() {
+ synchronized (mLock) {
+ if (mFrameScheduled) {
+ scheduleVsyncLocked();
+ }
+ }
+ }
+
+ void doScheduleCallback(int callbackType) {
+ synchronized (mLock) {
+ if (!mFrameScheduled) {
+ final long now = SystemClock.uptimeMillis();
+ if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
+ scheduleFrameLocked(now);
+ }
+ }
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void scheduleVsyncLocked() {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#scheduleVsyncLocked");
+ mDisplayEventReceiver.scheduleVsync();
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ private boolean isRunningOnLooperThreadLocked() {
+ return Looper.myLooper() == mLooper;
+ }
+
+ private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
+ CallbackRecord callback = mCallbackPool;
+ if (callback == null) {
+ callback = new CallbackRecord();
+ } else {
+ mCallbackPool = callback.next;
+ callback.next = null;
+ }
+ callback.dueTime = dueTime;
+ callback.action = action;
+ callback.token = token;
+ return callback;
+ }
+
+ private void recycleCallbackLocked(CallbackRecord callback) {
+ callback.action = null;
+ callback.token = null;
+ callback.next = mCallbackPool;
+ mCallbackPool = callback;
+ }
+
+ /**
+ * Implement this interface to receive a callback when a new display frame is
+ * being rendered. The callback is invoked on the {@link Looper} thread to
+ * which the {@link Choreographer} is attached.
+ */
+ public interface FrameCallback {
+ /**
+ * Called when a new display frame is being rendered.
+ * <p>
+ * This method provides the time in nanoseconds when the frame started being rendered.
+ * The frame time provides a stable time base for synchronizing animations
+ * and drawing. It should be used instead of {@link SystemClock#uptimeMillis()}
+ * or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame
+ * time helps to reduce inter-frame jitter because the frame time is fixed at the time
+ * the frame was scheduled to start, regardless of when the animations or drawing
+ * callback actually runs. All callbacks that run as part of rendering a frame will
+ * observe the same frame time so using the frame time also helps to synchronize effects
+ * that are performed by different callbacks.
+ * </p><p>
+ * Please note that the framework already takes care to process animations and
+ * drawing using the frame time as a stable time base. Most applications should
+ * not need to use the frame time information directly.
+ * </p>
+ *
+ * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
+ * in the {@link System#nanoTime()} timebase. Divide this value by {@code 1000000}
+ * to convert it to the {@link SystemClock#uptimeMillis()} time base.
+ */
+ public void doFrame(long frameTimeNanos);
+ }
+
+ private final class FrameHandler extends Handler {
+ public FrameHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DO_FRAME:
+ doFrame(System.nanoTime(), 0, new DisplayEventReceiver.VsyncEventData());
+ break;
+ case MSG_DO_SCHEDULE_VSYNC:
+ doScheduleVsync();
+ break;
+ case MSG_DO_SCHEDULE_CALLBACK:
+ doScheduleCallback(msg.arg1);
+ break;
+ }
+ }
+ }
+
+ private final class FrameDisplayEventReceiver extends DisplayEventReceiver
+ implements Runnable {
+ private boolean mHavePendingVsync;
+ private long mTimestampNanos;
+ private int mFrame;
+ private VsyncEventData mLastVsyncEventData = new VsyncEventData();
+
+ public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
+ super(looper, vsyncSource, 0);
+ }
+
+ // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
+ // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
+ // for the internal display implicitly.
+ @Override
+ public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
+ VsyncEventData vsyncEventData) {
+ try {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "Choreographer#onVsync " + vsyncEventData.id);
+ }
+ // Post the vsync event to the Handler.
+ // The idea is to prevent incoming vsync events from completely starving
+ // the message queue. If there are no messages in the queue with timestamps
+ // earlier than the frame time, then the vsync event will be processed immediately.
+ // Otherwise, messages that predate the vsync event will be handled first.
+ long now = System.nanoTime();
+ if (timestampNanos > now) {
+ Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ + " ms in the future! Check that graphics HAL is generating vsync "
+ + "timestamps using the correct timebase.");
+ timestampNanos = now;
+ }
+
+ if (mHavePendingVsync) {
+ Log.w(TAG, "Already have a pending vsync event. There should only be "
+ + "one at a time.");
+ } else {
+ mHavePendingVsync = true;
+ }
+
+ mTimestampNanos = timestampNanos;
+ mFrame = frame;
+ mLastVsyncEventData = vsyncEventData;
+ Message msg = Message.obtain(mHandler, this);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ @Override
+ public void run() {
+ mHavePendingVsync = false;
+ doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
+ }
+ }
+
+ private static final class CallbackRecord {
+ public CallbackRecord next;
+ public long dueTime;
+ public Object action; // Runnable or FrameCallback
+ public Object token;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void run(long frameTimeNanos) {
+ if (token == FRAME_CALLBACK_TOKEN) {
+ ((FrameCallback)action).doFrame(frameTimeNanos);
+ } else {
+ ((Runnable)action).run();
+ }
+ }
+ }
+
+ private final class CallbackQueue {
+ private CallbackRecord mHead;
+
+ public boolean hasDueCallbacksLocked(long now) {
+ return mHead != null && mHead.dueTime <= now;
+ }
+
+ public CallbackRecord extractDueCallbacksLocked(long now) {
+ CallbackRecord callbacks = mHead;
+ if (callbacks == null || callbacks.dueTime > now) {
+ return null;
+ }
+
+ CallbackRecord last = callbacks;
+ CallbackRecord next = last.next;
+ while (next != null) {
+ if (next.dueTime > now) {
+ last.next = null;
+ break;
+ }
+ last = next;
+ next = next.next;
+ }
+ mHead = next;
+ return callbacks;
+ }
+
+ @UnsupportedAppUsage
+ public void addCallbackLocked(long dueTime, Object action, Object token) {
+ CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
+ CallbackRecord entry = mHead;
+ if (entry == null) {
+ mHead = callback;
+ return;
+ }
+ if (dueTime < entry.dueTime) {
+ callback.next = entry;
+ mHead = callback;
+ return;
+ }
+ while (entry.next != null) {
+ if (dueTime < entry.next.dueTime) {
+ callback.next = entry.next;
+ break;
+ }
+ entry = entry.next;
+ }
+ entry.next = callback;
+ }
+
+ public void removeCallbacksLocked(Object action, Object token) {
+ CallbackRecord predecessor = null;
+ for (CallbackRecord callback = mHead; callback != null;) {
+ final CallbackRecord next = callback.next;
+ if ((action == null || callback.action == action)
+ && (token == null || callback.token == token)) {
+ if (predecessor != null) {
+ predecessor.next = next;
+ } else {
+ mHead = next;
+ }
+ recycleCallbackLocked(callback);
+ } else {
+ predecessor = callback;
+ }
+ callback = next;
+ }
+ }
+ }
+}
diff --git a/android/view/Choreographer_Delegate.java b/android/view/Choreographer_Delegate.java
new file mode 100644
index 0000000..3a8839f
--- /dev/null
+++ b/android/view/Choreographer_Delegate.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.animation.AnimationHandler;
+import android.util.TimeUtils;
+import android.view.animation.AnimationUtils;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link Choreographer}
+ *
+ * Through the layoutlib_create tool, the original methods of Choreographer have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ */
+public class Choreographer_Delegate {
+ private static final AtomicReference<Choreographer> mInstance = new AtomicReference<Choreographer>();
+
+ private static final int MS_16 = 16000000;
+
+ @LayoutlibDelegate
+ public static Choreographer getInstance() {
+ if (mInstance.get() == null) {
+ mInstance.compareAndSet(null, Choreographer.getInstance_Original());
+ }
+
+ return mInstance.get();
+ }
+
+ @LayoutlibDelegate
+ public static float getRefreshRate() {
+ return 60.f;
+ }
+
+ @LayoutlibDelegate
+ static void scheduleVsyncLocked(Choreographer thisChoreographer) {
+ // do nothing
+ }
+
+ public static void doFrame(long frameTimeNanos) {
+ Choreographer thisChoreographer = Choreographer.getInstance();
+
+ AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
+
+ try {
+ thisChoreographer.mLastFrameTimeNanos = frameTimeNanos - thisChoreographer.getFrameIntervalNanos();
+ thisChoreographer.mFrameInfo.markInputHandlingStart();
+ thisChoreographer.doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, MS_16);
+
+ thisChoreographer.mFrameInfo.markAnimationsStart();
+ thisChoreographer.doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, MS_16);
+
+ thisChoreographer.mFrameInfo.markPerformTraversalsStart();
+ thisChoreographer.doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, MS_16);
+
+ thisChoreographer.doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, MS_16);
+ } finally {
+ AnimationUtils.unlockAnimationClock();
+ }
+ }
+
+ public static void clearFrames() {
+ Choreographer thisChoreographer = Choreographer.getInstance();
+
+ thisChoreographer.removeCallbacks(Choreographer.CALLBACK_INPUT, null, null);
+ thisChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, null, null);
+ thisChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, null, null);
+ thisChoreographer.removeCallbacks(Choreographer.CALLBACK_COMMIT, null, null);
+
+ // Release animation handler instance since it holds references to the callbacks
+ AnimationHandler.sAnimatorHandler.set(null);
+ }
+
+ public static void dispose() {
+ clearFrames();
+ Choreographer.releaseInstance();
+ }
+}
diff --git a/android/view/CollapsibleActionView.java b/android/view/CollapsibleActionView.java
new file mode 100644
index 0000000..ab2365e
--- /dev/null
+++ b/android/view/CollapsibleActionView.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.view.MenuItem.OnActionExpandListener;
+
+/**
+ * When a {@link View} implements this interface it will receive callbacks
+ * when expanded or collapsed as an action view alongside the optional,
+ * app-specified callbacks to {@link OnActionExpandListener}.
+ *
+ * <p>See {@link MenuItem} for more information about action views.
+ * See {@link android.app.ActionBar} for more information about the action bar.
+ */
+public interface CollapsibleActionView {
+ /**
+ * Called when this view is expanded as an action view.
+ * See {@link MenuItem#expandActionView()}.
+ */
+ public void onActionViewExpanded();
+
+ /**
+ * Called when this view is collapsed as an action view.
+ * See {@link MenuItem#collapseActionView()}.
+ */
+ public void onActionViewCollapsed();
+}
diff --git a/android/view/CompositionSamplingListener.java b/android/view/CompositionSamplingListener.java
new file mode 100644
index 0000000..677a559
--- /dev/null
+++ b/android/view/CompositionSamplingListener.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Listener for sampling the result of the screen composition.
+ * {@hide}
+ */
+public abstract class CompositionSamplingListener {
+
+ private long mNativeListener;
+ private final Executor mExecutor;
+
+ public CompositionSamplingListener(Executor executor) {
+ mExecutor = executor;
+ mNativeListener = nativeCreate(this);
+ }
+
+ public void destroy() {
+ if (mNativeListener == 0) {
+ return;
+ }
+ unregister(this);
+ nativeDestroy(mNativeListener);
+ mNativeListener = 0;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Reports a luma sample from the registered region.
+ */
+ public abstract void onSampleCollected(float medianLuma);
+
+ /**
+ * Registers a sampling listener.
+ */
+ public static void register(CompositionSamplingListener listener,
+ int displayId, SurfaceControl stopLayer, Rect samplingArea) {
+ if (listener.mNativeListener == 0) {
+ return;
+ }
+ Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
+ "default display only for now");
+ long nativeStopLayerObject = stopLayer != null ? stopLayer.mNativeObject : 0;
+ nativeRegister(listener.mNativeListener, nativeStopLayerObject, samplingArea.left,
+ samplingArea.top, samplingArea.right, samplingArea.bottom);
+ }
+
+ /**
+ * Unregisters a sampling listener.
+ */
+ public static void unregister(CompositionSamplingListener listener) {
+ if (listener.mNativeListener == 0) {
+ return;
+ }
+ nativeUnregister(listener.mNativeListener);
+ }
+
+ /**
+ * Dispatch the collected sample.
+ *
+ * Called from native code on a binder thread.
+ */
+ private static void dispatchOnSampleCollected(CompositionSamplingListener listener,
+ float medianLuma) {
+ listener.mExecutor.execute(() -> listener.onSampleCollected(medianLuma));
+ }
+
+ private static native long nativeCreate(CompositionSamplingListener thiz);
+ private static native void nativeDestroy(long ptr);
+ private static native void nativeRegister(long ptr, long stopLayerObject,
+ int samplingAreaLeft, int top, int right, int bottom);
+ private static native void nativeUnregister(long ptr);
+}
diff --git a/android/view/ContentInfo.java b/android/view/ContentInfo.java
new file mode 100644
index 0000000..b1b0e6b
--- /dev/null
+++ b/android/view/ContentInfo.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import android.view.inputmethod.InputContentInfo;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Holds all the relevant data for a request to {@link View#performReceiveContent}.
+ */
+public final class ContentInfo implements Parcelable {
+
+ /**
+ * Specifies the UI through which content is being inserted. Future versions of Android may
+ * support additional values.
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD,
+ SOURCE_DRAG_AND_DROP, SOURCE_AUTOFILL, SOURCE_PROCESS_TEXT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Source {}
+
+ /**
+ * Specifies that the operation was triggered by the app that contains the target view.
+ */
+ public static final int SOURCE_APP = 0;
+
+ /**
+ * Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or
+ * "Paste as plain text" action in the insertion/selection menu).
+ */
+ public static final int SOURCE_CLIPBOARD = 1;
+
+ /**
+ * Specifies that the operation was triggered from the soft keyboard (also known as input
+ * method editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard
+ * for more info.
+ */
+ public static final int SOURCE_INPUT_METHOD = 2;
+
+ /**
+ * Specifies that the operation was triggered by the drag/drop framework. See
+ * https://developer.android.com/guide/topics/ui/drag-drop for more info.
+ */
+ public static final int SOURCE_DRAG_AND_DROP = 3;
+
+ /**
+ * Specifies that the operation was triggered by the autofill framework. See
+ * https://developer.android.com/guide/topics/text/autofill for more info.
+ */
+ public static final int SOURCE_AUTOFILL = 4;
+
+ /**
+ * Specifies that the operation was triggered by a result from a
+ * {@link android.content.Intent#ACTION_PROCESS_TEXT PROCESS_TEXT} action in the selection
+ * menu.
+ */
+ public static final int SOURCE_PROCESS_TEXT = 5;
+
+ /**
+ * Returns the symbolic name of the given source.
+ *
+ * @hide
+ */
+ static String sourceToString(@Source int source) {
+ switch (source) {
+ case SOURCE_APP: return "SOURCE_APP";
+ case SOURCE_CLIPBOARD: return "SOURCE_CLIPBOARD";
+ case SOURCE_INPUT_METHOD: return "SOURCE_INPUT_METHOD";
+ case SOURCE_DRAG_AND_DROP: return "SOURCE_DRAG_AND_DROP";
+ case SOURCE_AUTOFILL: return "SOURCE_AUTOFILL";
+ case SOURCE_PROCESS_TEXT: return "SOURCE_PROCESS_TEXT";
+ }
+ return String.valueOf(source);
+ }
+
+ /**
+ * Flags to configure the insertion behavior.
+ *
+ * @hide
+ */
+ @IntDef(flag = true, prefix = {"FLAG_"}, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Flags {}
+
+ /**
+ * Flag requesting that the content should be converted to plain text prior to inserting.
+ */
+ public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0;
+
+ /**
+ * Returns the symbolic names of the set flags or {@code "0"} if no flags are set.
+ *
+ * @hide
+ */
+ static String flagsToString(@Flags int flags) {
+ if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
+ return "FLAG_CONVERT_TO_PLAIN_TEXT";
+ }
+ return String.valueOf(flags);
+ }
+
+ @NonNull
+ private final ClipData mClip;
+ @Source
+ private final int mSource;
+ @Flags
+ private final int mFlags;
+ @Nullable
+ private final Uri mLinkUri;
+ @Nullable
+ private final Bundle mExtras;
+ @Nullable
+ private final InputContentInfo mInputContentInfo;
+ @Nullable
+ private final DragAndDropPermissions mDragAndDropPermissions;
+
+ private ContentInfo(Builder b) {
+ this.mClip = Objects.requireNonNull(b.mClip);
+ this.mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_PROCESS_TEXT,
+ "source");
+ this.mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
+ this.mLinkUri = b.mLinkUri;
+ this.mExtras = b.mExtras;
+ this.mInputContentInfo = b.mInputContentInfo;
+ this.mDragAndDropPermissions = b.mDragAndDropPermissions;
+ }
+
+ /**
+ * If the content came from a source that supports proactive release of URI permissions
+ * (e.g. IME), releases permissions; otherwise a no-op.
+ *
+ * @hide
+ */
+ @TestApi
+ public void releasePermissions() {
+ if (mInputContentInfo != null) {
+ mInputContentInfo.releasePermission();
+ }
+ if (mDragAndDropPermissions != null) {
+ mDragAndDropPermissions.release();
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ContentInfo{"
+ + "clip=" + mClip
+ + ", source=" + sourceToString(mSource)
+ + ", flags=" + flagsToString(mFlags)
+ + ", linkUri=" + mLinkUri
+ + ", extras=" + mExtras
+ + "}";
+ }
+
+ /**
+ * The data to be inserted.
+ */
+ @NonNull
+ public ClipData getClip() {
+ return mClip;
+ }
+
+ /**
+ * The source of the operation. See {@code SOURCE_} constants. Future versions of Android
+ * may pass additional values.
+ */
+ @Source
+ public int getSource() {
+ return mSource;
+ }
+
+ /**
+ * Optional flags that control the insertion behavior. See {@code FLAG_} constants.
+ */
+ @Flags
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Optional http/https URI for the content that may be provided by the IME. This is only
+ * populated if the source is {@link #SOURCE_INPUT_METHOD} and if a non-empty
+ * {@link android.view.inputmethod.InputContentInfo#getLinkUri linkUri} was passed by the
+ * IME.
+ */
+ @Nullable
+ public Uri getLinkUri() {
+ return mLinkUri;
+ }
+
+ /**
+ * Optional additional metadata. If the source is {@link #SOURCE_INPUT_METHOD}, this will
+ * include the {@link android.view.inputmethod.InputConnection#commitContent opts} passed by
+ * the IME.
+ */
+ @Nullable
+ @SuppressLint("NullableCollection")
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Partitions this content based on the given predicate.
+ *
+ * <p>This function classifies the content and organizes it into a pair, grouping the items
+ * that matched vs didn't match the predicate.
+ *
+ * <p>Except for the {@link ClipData} items, the returned objects will contain all the same
+ * metadata as this {@link ContentInfo}.
+ *
+ * @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which
+ * partition to place it into.
+ * @return A pair containing the partitioned content. The pair's first object will have the
+ * content that matched the predicate, or null if none of the items matched. The pair's
+ * second object will have the content that didn't match the predicate, or null if all of
+ * the items matched.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public Pair<ContentInfo, ContentInfo> partition(
+ @NonNull Predicate<ClipData.Item> itemPredicate) {
+ if (mClip.getItemCount() == 1) {
+ boolean matched = itemPredicate.test(mClip.getItemAt(0));
+ return Pair.create(matched ? this : null, matched ? null : this);
+ }
+ ArrayList<ClipData.Item> acceptedItems = new ArrayList<>();
+ ArrayList<ClipData.Item> remainingItems = new ArrayList<>();
+ for (int i = 0; i < mClip.getItemCount(); i++) {
+ ClipData.Item item = mClip.getItemAt(i);
+ if (itemPredicate.test(item)) {
+ acceptedItems.add(item);
+ } else {
+ remainingItems.add(item);
+ }
+ }
+ if (acceptedItems.isEmpty()) {
+ return Pair.create(null, this);
+ }
+ if (remainingItems.isEmpty()) {
+ return Pair.create(this, null);
+ }
+ ContentInfo accepted = new Builder(this)
+ .setClip(new ClipData(new ClipDescription(mClip.getDescription()), acceptedItems))
+ .build();
+ ContentInfo remaining = new Builder(this)
+ .setClip(new ClipData(new ClipDescription(mClip.getDescription()), remainingItems))
+ .build();
+ return Pair.create(accepted, remaining);
+ }
+
+ /**
+ * Builder for {@link ContentInfo}.
+ */
+ public static final class Builder {
+ @NonNull
+ private ClipData mClip;
+ @Source
+ private int mSource;
+ @Flags
+ private int mFlags;
+ @Nullable
+ private Uri mLinkUri;
+ @Nullable
+ private Bundle mExtras;
+ @Nullable
+ private InputContentInfo mInputContentInfo;
+ @Nullable
+ private DragAndDropPermissions mDragAndDropPermissions;
+
+ /**
+ * Creates a new builder initialized with the data from the given builder.
+ */
+ public Builder(@NonNull ContentInfo other) {
+ mClip = other.mClip;
+ mSource = other.mSource;
+ mFlags = other.mFlags;
+ mLinkUri = other.mLinkUri;
+ mExtras = other.mExtras;
+ mInputContentInfo = other.mInputContentInfo;
+ mDragAndDropPermissions = other.mDragAndDropPermissions;
+ }
+
+ /**
+ * Creates a new builder.
+ * @param clip The data to insert.
+ * @param source The source of the operation. See {@code SOURCE_} constants.
+ */
+ public Builder(@NonNull ClipData clip, @Source int source) {
+ mClip = clip;
+ mSource = source;
+ }
+
+ /**
+ * Sets the data to be inserted.
+ * @param clip The data to insert.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setClip(@NonNull ClipData clip) {
+ mClip = clip;
+ return this;
+ }
+
+ /**
+ * Sets the source of the operation.
+ * @param source The source of the operation. See {@code SOURCE_} constants.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setSource(@Source int source) {
+ mSource = source;
+ return this;
+ }
+
+ /**
+ * Sets flags that control content insertion behavior.
+ * @param flags Optional flags to configure the insertion behavior. Use 0 for default
+ * behavior. See {@code FLAG_} constants.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setFlags(@Flags int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets the http/https URI for the content. See
+ * {@link android.view.inputmethod.InputContentInfo#getLinkUri} for more info.
+ * @param linkUri Optional http/https URI for the content.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setLinkUri(@Nullable Uri linkUri) {
+ mLinkUri = linkUri;
+ return this;
+ }
+
+ /**
+ * Sets additional metadata.
+ * @param extras Optional bundle with additional metadata.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setExtras(@SuppressLint("NullableCollection") @Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Set the {@link InputContentInfo} object if the content is coming from the IME. This can
+ * be used for proactive cleanup of permissions.
+ *
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setInputContentInfo(@Nullable InputContentInfo inputContentInfo) {
+ mInputContentInfo = inputContentInfo;
+ return this;
+ }
+
+ /**
+ * Set the {@link DragAndDropPermissions} object if the content is coming via drag-and-drop.
+ * This can be used for proactive cleanup of permissions.
+ *
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setDragAndDropPermissions(@Nullable DragAndDropPermissions permissions) {
+ mDragAndDropPermissions = permissions;
+ return this;
+ }
+
+
+ /**
+ * @return A new {@link ContentInfo} instance with the data from this builder.
+ */
+ @NonNull
+ public ContentInfo build() {
+ return new ContentInfo(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Writes this object into the given parcel.
+ *
+ * @param dest The parcel to write into.
+ * @param flags The flags to use for parceling.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ mClip.writeToParcel(dest, flags);
+ dest.writeInt(mSource);
+ dest.writeInt(mFlags);
+ Uri.writeToParcel(dest, mLinkUri);
+ dest.writeBundle(mExtras);
+ if (mInputContentInfo == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mInputContentInfo.writeToParcel(dest, flags);
+ }
+ if (mDragAndDropPermissions == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mDragAndDropPermissions.writeToParcel(dest, flags);
+ }
+ }
+
+ /**
+ * Creates {@link ContentInfo} instances from parcels.
+ */
+ @NonNull
+ public static final Parcelable.Creator<ContentInfo> CREATOR =
+ new Parcelable.Creator<ContentInfo>() {
+ @Override
+ public ContentInfo createFromParcel(Parcel parcel) {
+ ClipData clip = ClipData.CREATOR.createFromParcel(parcel);
+ int source = parcel.readInt();
+ int flags = parcel.readInt();
+ Uri linkUri = Uri.CREATOR.createFromParcel(parcel);
+ Bundle extras = parcel.readBundle();
+ InputContentInfo inputContentInfo = null;
+ if (parcel.readInt() != 0) {
+ inputContentInfo = InputContentInfo.CREATOR.createFromParcel(parcel);
+ }
+ DragAndDropPermissions dragAndDropPermissions = null;
+ if (parcel.readInt() != 0) {
+ dragAndDropPermissions = DragAndDropPermissions.CREATOR.createFromParcel(parcel);
+ }
+ return new ContentInfo.Builder(clip, source)
+ .setFlags(flags)
+ .setLinkUri(linkUri)
+ .setExtras(extras)
+ .setInputContentInfo(inputContentInfo)
+ .setDragAndDropPermissions(dragAndDropPermissions)
+ .build();
+ }
+
+ @Override
+ public ContentInfo[] newArray(int size) {
+ return new ContentInfo[size];
+ }
+ };
+}
diff --git a/android/view/ContextMenu.java b/android/view/ContextMenu.java
new file mode 100644
index 0000000..85fe421
--- /dev/null
+++ b/android/view/ContextMenu.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.widget.AdapterView;
+
+/**
+ * Extension of {@link Menu} for context menus providing functionality to modify
+ * the header of the context menu.
+ * <p>
+ * Context menus do not support item shortcuts and item icons.
+ * <p>
+ * To show a context menu on long click, most clients will want to call
+ * {@link Activity#registerForContextMenu} and override
+ * {@link Activity#onCreateContextMenu}.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about creating menus, read the
+ * <a href="{@docRoot}guide/topics/ui/menus.html">Menus</a> developer guide.</p>
+ * </div>
+ */
+public interface ContextMenu extends Menu {
+ /**
+ * Sets the context menu header's title to the title given in <var>titleRes</var>
+ * resource identifier.
+ *
+ * @param titleRes The string resource identifier used for the title.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderTitle(@StringRes int titleRes);
+
+ /**
+ * Sets the context menu header's title to the title given in <var>title</var>.
+ *
+ * @param title The character sequence used for the title.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderTitle(CharSequence title);
+
+ /**
+ * Sets the context menu header's icon to the icon given in <var>iconRes</var>
+ * resource id.
+ *
+ * @param iconRes The resource identifier used for the icon.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderIcon(@DrawableRes int iconRes);
+
+ /**
+ * Sets the context menu header's icon to the icon given in <var>icon</var>
+ * {@link Drawable}.
+ *
+ * @param icon The {@link Drawable} used for the icon.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderIcon(Drawable icon);
+
+ /**
+ * Sets the header of the context menu to the {@link View} given in
+ * <var>view</var>. This replaces the header title and icon (and those
+ * replace this).
+ *
+ * @param view The {@link View} used for the header.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderView(View view);
+
+ /**
+ * Clears the header of the context menu.
+ */
+ public void clearHeader();
+
+ /**
+ * Additional information regarding the creation of the context menu. For example,
+ * {@link AdapterView}s use this to pass the exact item position within the adapter
+ * that initiated the context menu.
+ */
+ public interface ContextMenuInfo {
+ }
+}
diff --git a/android/view/ContextThemeWrapper.java b/android/view/ContextThemeWrapper.java
new file mode 100644
index 0000000..876331b
--- /dev/null
+++ b/android/view/ContextThemeWrapper.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+
+/**
+ * A context wrapper that allows you to modify or replace the theme of the
+ * wrapped context.
+ */
+public class ContextThemeWrapper extends ContextWrapper {
+ @UnsupportedAppUsage
+ private int mThemeResource;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768723)
+ private Resources.Theme mTheme;
+ @UnsupportedAppUsage
+ private LayoutInflater mInflater;
+ private Configuration mOverrideConfiguration;
+ @UnsupportedAppUsage
+ private Resources mResources;
+
+ /**
+ * Creates a new context wrapper with no theme and no base context.
+ * <p class="note">
+ * <strong>Note:</strong> A base context <strong>must</strong> be attached
+ * using {@link #attachBaseContext(Context)} before calling any other
+ * method on the newly constructed context wrapper.
+ */
+ public ContextThemeWrapper() {
+ super(null);
+ }
+
+ /**
+ * Creates a new context wrapper with the specified theme.
+ * <p>
+ * The specified theme will be applied on top of the base context's theme.
+ * Any attributes not explicitly defined in the theme identified by
+ * <var>themeResId</var> will retain their original values.
+ *
+ * @param base the base context
+ * @param themeResId the resource ID of the theme to be applied on top of
+ * the base context's theme
+ */
+ public ContextThemeWrapper(Context base, @StyleRes int themeResId) {
+ super(base);
+ mThemeResource = themeResId;
+ }
+
+ /**
+ * Creates a new context wrapper with the specified theme.
+ * <p>
+ * Unlike {@link #ContextThemeWrapper(Context, int)}, the theme passed to
+ * this constructor will completely replace the base context's theme.
+ *
+ * @param base the base context
+ * @param theme the theme against which resources should be inflated
+ */
+ public ContextThemeWrapper(Context base, Resources.Theme theme) {
+ super(base);
+ mTheme = theme;
+ }
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(newBase);
+ }
+
+ /**
+ * Call to set an "override configuration" on this context -- this is
+ * a configuration that replies one or more values of the standard
+ * configuration that is applied to the context. See
+ * {@link Context#createConfigurationContext(Configuration)} for more
+ * information.
+ *
+ * <p>This method can only be called once, and must be called before any
+ * calls to {@link #getResources()} or {@link #getAssets()} are made.
+ */
+ public void applyOverrideConfiguration(Configuration overrideConfiguration) {
+ if (mResources != null) {
+ throw new IllegalStateException(
+ "getResources() or getAssets() has already been called");
+ }
+ if (mOverrideConfiguration != null) {
+ throw new IllegalStateException("Override configuration has already been set");
+ }
+ mOverrideConfiguration = new Configuration(overrideConfiguration);
+ }
+
+ /**
+ * Used by ActivityThread to apply the overridden configuration to onConfigurationChange
+ * callbacks.
+ * @hide
+ */
+ public Configuration getOverrideConfiguration() {
+ return mOverrideConfiguration;
+ }
+
+ @Override
+ public AssetManager getAssets() {
+ // Ensure we're returning assets with the correct configuration.
+ return getResourcesInternal().getAssets();
+ }
+
+ @Override
+ public Resources getResources() {
+ return getResourcesInternal();
+ }
+
+ private Resources getResourcesInternal() {
+ if (mResources == null) {
+ if (mOverrideConfiguration == null) {
+ mResources = super.getResources();
+ } else {
+ final Context resContext = createConfigurationContext(mOverrideConfiguration);
+ mResources = resContext.getResources();
+ }
+ }
+ return mResources;
+ }
+
+ @Override
+ public void setTheme(int resid) {
+ if (mThemeResource != resid) {
+ mThemeResource = resid;
+ initializeTheme();
+ }
+ }
+
+ /**
+ * Set the configure the current theme. If null is provided then the default Theme is returned
+ * on the next call to {@link #getTheme()}
+ * @param theme Theme to consume in the wrapper, a value of null resets the theme to the default
+ */
+ public void setTheme(@Nullable Resources.Theme theme) {
+ mTheme = theme;
+ }
+
+ /** @hide */
+ @Override
+ @UnsupportedAppUsage
+ public int getThemeResId() {
+ return mThemeResource;
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ if (mTheme != null) {
+ return mTheme;
+ }
+
+ mThemeResource = Resources.selectDefaultTheme(mThemeResource,
+ getApplicationInfo().targetSdkVersion);
+ initializeTheme();
+
+ return mTheme;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (LAYOUT_INFLATER_SERVICE.equals(name)) {
+ if (mInflater == null) {
+ mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
+ }
+ return mInflater;
+ }
+ return getBaseContext().getSystemService(name);
+ }
+
+ /**
+ * Called by {@link #setTheme} and {@link #getTheme} to apply a theme
+ * resource to the current Theme object. May be overridden to change the
+ * default (simple) behavior. This method will not be called in multiple
+ * threads simultaneously.
+ *
+ * @param theme the theme being modified
+ * @param resId the style resource being applied to <var>theme</var>
+ * @param first {@code true} if this is the first time a style is being
+ * applied to <var>theme</var>
+ */
+ protected void onApplyThemeResource(Resources.Theme theme, int resId, boolean first) {
+ theme.applyStyle(resId, true);
+ }
+
+ @UnsupportedAppUsage
+ private void initializeTheme() {
+ final boolean first = mTheme == null;
+ if (first) {
+ mTheme = getResources().newTheme();
+ final Resources.Theme theme = getBaseContext().getTheme();
+ if (theme != null) {
+ mTheme.setTo(theme);
+ }
+ }
+ onApplyThemeResource(mTheme, mThemeResource, first);
+ }
+}
+
diff --git a/android/view/CrossWindowBlurListeners.java b/android/view/CrossWindowBlurListeners.java
new file mode 100644
index 0000000..761a2b8
--- /dev/null
+++ b/android/view/CrossWindowBlurListeners.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Class that holds all registered {@link CrossWindowBlurEnabledListener}s. It listens
+ * for updates from the WindowManagerService and updates all registered listeners.
+ * @hide
+ */
+public final class CrossWindowBlurListeners {
+ private static final String TAG = "CrossWindowBlurListeners";
+
+ // property for background blur support in surface flinger
+ private static final String BLUR_PROPERTY = "ro.surface_flinger.supports_background_blur";
+ public static final boolean CROSS_WINDOW_BLUR_SUPPORTED =
+ SystemProperties.getBoolean(BLUR_PROPERTY, false);
+
+ private static volatile CrossWindowBlurListeners sInstance;
+ private static final Object sLock = new Object();
+
+ private final BlurEnabledListenerInternal mListenerInternal = new BlurEnabledListenerInternal();
+ private final ArrayMap<Consumer<Boolean>, Executor> mListeners = new ArrayMap();
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+ private boolean mInternalListenerAttached = false;
+ private boolean mCrossWindowBlurEnabled;
+
+ private CrossWindowBlurListeners() {}
+
+ /**
+ * Returns a CrossWindowBlurListeners instance
+ */
+ public static CrossWindowBlurListeners getInstance() {
+ CrossWindowBlurListeners instance = sInstance;
+ if (instance == null) {
+
+ synchronized (sLock) {
+ instance = sInstance;
+ if (instance == null) {
+ instance = new CrossWindowBlurListeners();
+ sInstance = instance;
+ }
+ }
+ }
+ return instance;
+ }
+
+ public boolean isCrossWindowBlurEnabled() {
+ synchronized (sLock) {
+ attachInternalListenerIfNeededLocked();
+ return mCrossWindowBlurEnabled;
+ }
+ }
+
+ public void addListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<Boolean> listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ Preconditions.checkNotNull(executor, "executor cannot be null");
+
+ synchronized (sLock) {
+ attachInternalListenerIfNeededLocked();
+
+ mListeners.put(listener, executor);
+ notifyListener(listener, executor, mCrossWindowBlurEnabled);
+ }
+ }
+
+
+ public void removeListener(Consumer<Boolean> listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+
+ synchronized (sLock) {
+ mListeners.remove(listener);
+
+ if (mInternalListenerAttached && mListeners.size() == 0) {
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .unregisterCrossWindowBlurEnabledListener(mListenerInternal);
+ mInternalListenerAttached = false;
+ } catch (RemoteException e) {
+ Log.d(TAG, "Could not unregister ICrossWindowBlurEnabledListener");
+ }
+ }
+ }
+ }
+
+ private void attachInternalListenerIfNeededLocked() {
+ if (!mInternalListenerAttached) {
+ try {
+ mCrossWindowBlurEnabled = WindowManagerGlobal.getWindowManagerService()
+ .registerCrossWindowBlurEnabledListener(mListenerInternal);
+ mInternalListenerAttached = true;
+ } catch (RemoteException e) {
+ Log.d(TAG, "Could not register ICrossWindowBlurEnabledListener");
+ }
+ }
+ }
+
+ private void notifyListener(Consumer<Boolean> listener, Executor executor, boolean enabled) {
+ executor.execute(() -> listener.accept(enabled));
+ }
+
+ private final class BlurEnabledListenerInternal extends ICrossWindowBlurEnabledListener.Stub {
+ @Override
+ public void onCrossWindowBlurEnabledChanged(boolean enabled) {
+ synchronized (sLock) {
+ mCrossWindowBlurEnabled = enabled;
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ for (int i = 0; i < mListeners.size(); i++) {
+ notifyListener(mListeners.keyAt(i), mListeners.valueAt(i), enabled);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+ }
+}
diff --git a/android/view/CutoutSpecification.java b/android/view/CutoutSpecification.java
new file mode 100644
index 0000000..850e9fc
--- /dev/null
+++ b/android/view/CutoutSpecification.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.LEFT;
+import static android.view.Gravity.RIGHT;
+import static android.view.Gravity.TOP;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PathParser;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * In order to accept the cutout specification for all of edges in devices, the specification
+ * parsing method is extracted from
+ * {@link android.view.DisplayCutout#fromResourcesRectApproximation(Resources, int, int)} to be
+ * the specified class for parsing the specification.
+ * BNF definition:
+ * <ul>
+ * <li>Cutouts Specification = ([Cutout Delimiter],Cutout Specification) {...}, [Dp] ; </li>
+ * <li>Cutout Specification = [Vertical Position], (SVG Path Element), [Horizontal Position]
+ * [Bind Cutout] ;</li>
+ * <li>Vertical Position = "@bottom" | "@center_vertical" ;</li>
+ * <li>Horizontal Position = "@left" | "@right" ;</li>
+ * <li>Bind Cutout = "@bind_left_cutout" | "@bind_right_cutout" ;</li>
+ * <li>Cutout Delimiter = "@cutout" ;</li>
+ * <li>Dp = "@dp"</li>
+ * </ul>
+ *
+ * <ul>
+ * <li>Vertical position is top by default if there is neither "@bottom" nor "@center_vertical"
+ * </li>
+ * <li>Horizontal position is center horizontal by default if there is neither "@left" nor
+ * "@right".</li>
+ * <li>@bottom make the cutout piece bind to bottom edge.</li>
+ * <li>both of @bind_left_cutout and @bind_right_cutout are use to claim the cutout belong to
+ * left or right edge cutout.</li>
+ * </ul>
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class CutoutSpecification {
+ private static final String TAG = "CutoutSpecification";
+ private static final boolean DEBUG = false;
+
+ private static final int MINIMAL_ACCEPTABLE_PATH_LENGTH = "H1V1Z".length();
+
+ private static final char MARKER_START_CHAR = '@';
+ private static final String DP_MARKER = MARKER_START_CHAR + "dp";
+
+ private static final String BOTTOM_MARKER = MARKER_START_CHAR + "bottom";
+ private static final String RIGHT_MARKER = MARKER_START_CHAR + "right";
+ private static final String LEFT_MARKER = MARKER_START_CHAR + "left";
+ private static final String CUTOUT_MARKER = MARKER_START_CHAR + "cutout";
+ private static final String CENTER_VERTICAL_MARKER = MARKER_START_CHAR + "center_vertical";
+
+ /* By default, it's top bound cutout. That's why TOP_BOUND_CUTOUT_MARKER is not defined */
+ private static final String BIND_RIGHT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_right_cutout";
+ private static final String BIND_LEFT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_left_cutout";
+
+ private final Path mPath;
+ private final Rect mLeftBound;
+ private final Rect mTopBound;
+ private final Rect mRightBound;
+ private final Rect mBottomBound;
+ private final Insets mInsets;
+
+ private CutoutSpecification(@NonNull Parser parser) {
+ mPath = parser.mPath;
+ mLeftBound = parser.mLeftBound;
+ mTopBound = parser.mTopBound;
+ mRightBound = parser.mRightBound;
+ mBottomBound = parser.mBottomBound;
+ mInsets = parser.mInsets;
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(Locale.ENGLISH,
+ "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s",
+ mLeftBound != null ? mLeftBound.toString() : "",
+ mTopBound != null ? mTopBound.toString() : "",
+ mRightBound != null ? mRightBound.toString() : "",
+ mBottomBound != null ? mBottomBound.toString() : ""));
+ }
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Path getPath() {
+ return mPath;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getLeftBound() {
+ return mLeftBound;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getTopBound() {
+ return mTopBound;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getRightBound() {
+ return mRightBound;
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ @Nullable
+ public Rect getBottomBound() {
+ return mBottomBound;
+ }
+
+ /**
+ * To count the safe inset according to the cutout bounds and waterfall inset.
+ *
+ * @return the safe inset.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public Rect getSafeInset() {
+ return mInsets.toRect();
+ }
+
+ private static int decideWhichEdge(boolean isTopEdgeShortEdge,
+ boolean isShortEdge, boolean isStart) {
+ return (isTopEdgeShortEdge)
+ ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT))
+ : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM));
+ }
+
+ /**
+ * The CutoutSpecification Parser.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public static class Parser {
+ private final boolean mIsShortEdgeOnTop;
+ private final float mDensity;
+ private final int mDisplayWidth;
+ private final int mDisplayHeight;
+ private final Matrix mMatrix;
+ private Insets mInsets;
+ private int mSafeInsetLeft;
+ private int mSafeInsetTop;
+ private int mSafeInsetRight;
+ private int mSafeInsetBottom;
+
+ private final Rect mTmpRect = new Rect();
+ private final RectF mTmpRectF = new RectF();
+
+ private boolean mInDp;
+
+ private Path mPath;
+ private Rect mLeftBound;
+ private Rect mTopBound;
+ private Rect mRightBound;
+ private Rect mBottomBound;
+
+ private boolean mPositionFromLeft = false;
+ private boolean mPositionFromRight = false;
+ private boolean mPositionFromBottom = false;
+ private boolean mPositionFromCenterVertical = false;
+
+ private boolean mBindLeftCutout = false;
+ private boolean mBindRightCutout = false;
+ private boolean mBindBottomCutout = false;
+
+ private boolean mIsTouchShortEdgeStart;
+ private boolean mIsTouchShortEdgeEnd;
+ private boolean mIsCloserToStartSide;
+
+ /**
+ * The constructor of the CutoutSpecification parser to parse the specification of cutout.
+ * @param density the display density.
+ * @param displayWidth the display width.
+ * @param displayHeight the display height.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public Parser(float density, int displayWidth, int displayHeight) {
+ mDensity = density;
+ mDisplayWidth = displayWidth;
+ mDisplayHeight = displayHeight;
+ mMatrix = new Matrix();
+ mIsShortEdgeOnTop = mDisplayWidth < mDisplayHeight;
+ }
+
+ private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
+ mTmpRectF.setEmpty();
+ p.computeBounds(mTmpRectF, false /* unused */);
+ mTmpRectF.round(inoutRect);
+ inoutRegion.op(inoutRect, Region.Op.UNION);
+ }
+
+ private void resetStatus(StringBuilder sb) {
+ sb.setLength(0);
+ mPositionFromBottom = false;
+ mPositionFromLeft = false;
+ mPositionFromRight = false;
+ mPositionFromCenterVertical = false;
+
+ mBindLeftCutout = false;
+ mBindRightCutout = false;
+ mBindBottomCutout = false;
+ }
+
+ private void translateMatrix() {
+ final float offsetX;
+ if (mPositionFromRight) {
+ offsetX = mDisplayWidth;
+ } else if (mPositionFromLeft) {
+ offsetX = 0;
+ } else {
+ offsetX = mDisplayWidth / 2f;
+ }
+
+ final float offsetY;
+ if (mPositionFromBottom) {
+ offsetY = mDisplayHeight;
+ } else if (mPositionFromCenterVertical) {
+ offsetY = mDisplayHeight / 2f;
+ } else {
+ offsetY = 0;
+ }
+
+ mMatrix.reset();
+ if (mInDp) {
+ mMatrix.postScale(mDensity, mDensity);
+ }
+ mMatrix.postTranslate(offsetX, offsetY);
+ }
+
+ private int computeSafeInsets(int gravity, Rect rect) {
+ if (gravity == LEFT && rect.right > 0 && rect.right < mDisplayWidth) {
+ return rect.right;
+ } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mDisplayHeight) {
+ return rect.bottom;
+ } else if (gravity == RIGHT && rect.left > 0 && rect.left < mDisplayWidth) {
+ return mDisplayWidth - rect.left;
+ } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mDisplayHeight) {
+ return mDisplayHeight - rect.top;
+ }
+ return 0;
+ }
+
+ private void setSafeInset(int gravity, int inset) {
+ if (gravity == LEFT) {
+ mSafeInsetLeft = inset;
+ } else if (gravity == TOP) {
+ mSafeInsetTop = inset;
+ } else if (gravity == RIGHT) {
+ mSafeInsetRight = inset;
+ } else if (gravity == BOTTOM) {
+ mSafeInsetBottom = inset;
+ }
+ }
+
+ private int getSafeInset(int gravity) {
+ if (gravity == LEFT) {
+ return mSafeInsetLeft;
+ } else if (gravity == TOP) {
+ return mSafeInsetTop;
+ } else if (gravity == RIGHT) {
+ return mSafeInsetRight;
+ } else if (gravity == BOTTOM) {
+ return mSafeInsetBottom;
+ }
+ return 0;
+ }
+
+ @NonNull
+ private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) {
+ final int gravity;
+ if (isShortEdge) {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart);
+ } else {
+ if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart);
+ } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, true,
+ mIsCloserToStartSide);
+ } else {
+ gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart);
+ }
+ }
+
+ int oldSafeInset = getSafeInset(gravity);
+ int newSafeInset = computeSafeInsets(gravity, rect);
+ if (oldSafeInset < newSafeInset) {
+ setSafeInset(gravity, newSafeInset);
+ }
+
+ return new Rect(rect);
+ }
+
+ private void setEdgeCutout(@NonNull Path newPath) {
+ if (mBindRightCutout && mRightBound == null) {
+ mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect);
+ } else if (mBindLeftCutout && mLeftBound == null) {
+ mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect);
+ } else if (mBindBottomCutout && mBottomBound == null) {
+ mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect);
+ } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout)
+ && mTopBound == null) {
+ mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect);
+ } else {
+ return;
+ }
+
+ if (mPath != null) {
+ mPath.addPath(newPath);
+ } else {
+ mPath = newPath;
+ }
+ }
+
+ private void parseSvgPathSpec(Region region, String spec) {
+ if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) {
+ Log.e(TAG, "According to SVG definition, it shouldn't happen");
+ return;
+ }
+ spec.trim();
+ translateMatrix();
+
+ final Path newPath = PathParser.createPathFromPathData(spec);
+ newPath.transform(mMatrix);
+ computeBoundsRectAndAddToRegion(newPath, region, mTmpRect);
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(Locale.ENGLISH,
+ "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b",
+ mPositionFromLeft, mPositionFromRight, mPositionFromBottom,
+ mPositionFromCenterVertical));
+ Log.d(TAG, "region = " + region);
+ Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath);
+ }
+
+ if (mTmpRect.isEmpty()) {
+ return;
+ }
+
+ if (mIsShortEdgeOnTop) {
+ mIsTouchShortEdgeStart = mTmpRect.top <= 0;
+ mIsTouchShortEdgeEnd = mTmpRect.bottom >= mDisplayHeight;
+ mIsCloserToStartSide = mTmpRect.centerY() < mDisplayHeight / 2;
+ } else {
+ mIsTouchShortEdgeStart = mTmpRect.left <= 0;
+ mIsTouchShortEdgeEnd = mTmpRect.right >= mDisplayWidth;
+ mIsCloserToStartSide = mTmpRect.centerX() < mDisplayWidth / 2;
+ }
+
+ setEdgeCutout(newPath);
+ }
+
+ private void parseSpecWithoutDp(@NonNull String specWithoutDp) {
+ Region region = Region.obtain();
+ StringBuilder sb = null;
+ int currentIndex = 0;
+ int lastIndex = 0;
+ while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) {
+ if (sb == null) {
+ sb = new StringBuilder(specWithoutDp.length());
+ }
+ sb.append(specWithoutDp, lastIndex, currentIndex);
+
+ if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) {
+ if (!mPositionFromRight) {
+ mPositionFromLeft = true;
+ }
+ currentIndex += LEFT_MARKER.length();
+ } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) {
+ if (!mPositionFromLeft) {
+ mPositionFromRight = true;
+ }
+ currentIndex += RIGHT_MARKER.length();
+ } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) {
+ parseSvgPathSpec(region, sb.toString());
+ currentIndex += BOTTOM_MARKER.length();
+
+ /* prepare to parse the rest path */
+ resetStatus(sb);
+ mBindBottomCutout = true;
+ mPositionFromBottom = true;
+ } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) {
+ parseSvgPathSpec(region, sb.toString());
+ currentIndex += CENTER_VERTICAL_MARKER.length();
+
+ /* prepare to parse the rest path */
+ resetStatus(sb);
+ mPositionFromCenterVertical = true;
+ } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) {
+ parseSvgPathSpec(region, sb.toString());
+ currentIndex += CUTOUT_MARKER.length();
+
+ /* prepare to parse the rest path */
+ resetStatus(sb);
+ } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) {
+ mBindBottomCutout = false;
+ mBindRightCutout = false;
+ mBindLeftCutout = true;
+
+ currentIndex += BIND_LEFT_CUTOUT_MARKER.length();
+ } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) {
+ mBindBottomCutout = false;
+ mBindLeftCutout = false;
+ mBindRightCutout = true;
+
+ currentIndex += BIND_RIGHT_CUTOUT_MARKER.length();
+ } else {
+ currentIndex += 1;
+ }
+
+ lastIndex = currentIndex;
+ }
+
+ if (sb == null) {
+ parseSvgPathSpec(region, specWithoutDp);
+ } else {
+ sb.append(specWithoutDp, lastIndex, specWithoutDp.length());
+ parseSvgPathSpec(region, sb.toString());
+ }
+
+ region.recycle();
+ }
+
+ /**
+ * To parse specification string as the CutoutSpecification.
+ *
+ * @param originalSpec the specification string
+ * @return the CutoutSpecification instance
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public CutoutSpecification parse(@NonNull String originalSpec) {
+ Objects.requireNonNull(originalSpec);
+
+ int dpIndex = originalSpec.lastIndexOf(DP_MARKER);
+ mInDp = (dpIndex != -1);
+ final String spec;
+ if (dpIndex != -1) {
+ spec = originalSpec.substring(0, dpIndex)
+ + originalSpec.substring(dpIndex + DP_MARKER.length());
+ } else {
+ spec = originalSpec;
+ }
+
+ parseSpecWithoutDp(spec);
+
+ mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom);
+ return new CutoutSpecification(this);
+ }
+ }
+}
diff --git a/android/view/CutoutSpecificationBenchmark.java b/android/view/CutoutSpecificationBenchmark.java
new file mode 100644
index 0000000..860c134
--- /dev/null
+++ b/android/view/CutoutSpecificationBenchmark.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PathParser;
+
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class CutoutSpecificationBenchmark {
+ private static final int DISPLAY_WIDTH = 1080;
+ private static final int DISPLAY_HEIGHT = 1920;
+ private static final float DISPLAY_DENSITY = 3.5f;
+
+ private static final String TAG = "CutoutSpecificationBenchmark";
+
+ private static final String BOTTOM_MARKER = "@bottom";
+ private static final String DP_MARKER = "@dp";
+ private static final String RIGHT_MARKER = "@right";
+ private static final String LEFT_MARKER = "@left";
+
+ private static final String DOUBLE_CUTOUT_SPEC = "M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, 20.0595537175\n"
+ + "C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0\n"
+ + "L 56.8, 32.0\n"
+ + "C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@bottom\n"
+ + "M 0,0\n"
+ + "L -72, 0\n"
+ + "L -69.9940446283, -20.0595537175\n"
+ + "C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0\n"
+ + "L 56.8, -32.0\n"
+ + "C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20.0595537175\n"
+ + "L 72, 0\n"
+ + "Z\n"
+ + "@dp";
+ @Rule
+ public BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+
+ private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) {
+ final RectF rectF = new RectF();
+ p.computeBounds(rectF, false /* unused */);
+ rectF.round(inoutRect);
+ inoutRegion.op(inoutRect, Region.Op.UNION);
+ }
+
+ private static void oldMethodParsingSpec(String spec, int displayWidth, int displayHeight,
+ float density) {
+ Path p = null;
+ Rect boundTop = null;
+ Rect boundBottom = null;
+ Rect safeInset = new Rect();
+ String bottomSpec = null;
+ if (!TextUtils.isEmpty(spec)) {
+ spec = spec.trim();
+ final float offsetX;
+ if (spec.endsWith(RIGHT_MARKER)) {
+ offsetX = displayWidth;
+ spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim();
+ } else if (spec.endsWith(LEFT_MARKER)) {
+ offsetX = 0;
+ spec = spec.substring(0, spec.length() - LEFT_MARKER.length()).trim();
+ } else {
+ offsetX = displayWidth / 2f;
+ }
+ final boolean inDp = spec.endsWith(DP_MARKER);
+ if (inDp) {
+ spec = spec.substring(0, spec.length() - DP_MARKER.length());
+ }
+
+ if (spec.contains(BOTTOM_MARKER)) {
+ String[] splits = spec.split(BOTTOM_MARKER, 2);
+ spec = splits[0].trim();
+ bottomSpec = splits[1].trim();
+ }
+
+ final Matrix m = new Matrix();
+ final Region r = Region.obtain();
+ if (!spec.isEmpty()) {
+ try {
+ p = PathParser.createPathFromPathData(spec);
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Could not inflate cutout: ", e);
+ }
+
+ if (p != null) {
+ if (inDp) {
+ m.postScale(density, density);
+ }
+ m.postTranslate(offsetX, 0);
+ p.transform(m);
+
+ boundTop = new Rect();
+ toRectAndAddToRegion(p, r, boundTop);
+ safeInset.top = boundTop.bottom;
+ }
+ }
+
+ if (bottomSpec != null) {
+ int bottomInset = 0;
+ Path bottomPath = null;
+ try {
+ bottomPath = PathParser.createPathFromPathData(bottomSpec);
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Could not inflate bottom cutout: ", e);
+ }
+
+ if (bottomPath != null) {
+ // Keep top transform
+ m.postTranslate(0, displayHeight);
+ bottomPath.transform(m);
+ p.addPath(bottomPath);
+ boundBottom = new Rect();
+ toRectAndAddToRegion(bottomPath, r, boundBottom);
+ bottomInset = displayHeight - boundBottom.top;
+ }
+ safeInset.bottom = bottomInset;
+ }
+ }
+ }
+
+ @Test
+ public void parseByOldMethodForDoubleCutout() {
+ final BenchmarkState state = mBenchmarkRule.getState();
+ while (state.keepRunning()) {
+ oldMethodParsingSpec(DOUBLE_CUTOUT_SPEC, DISPLAY_WIDTH, DISPLAY_HEIGHT,
+ DISPLAY_DENSITY);
+ }
+ }
+
+ @Test
+ public void parseByNewMethodForDoubleCutout() {
+ final BenchmarkState state = mBenchmarkRule.getState();
+ while (state.keepRunning()) {
+ new CutoutSpecification.Parser(DISPLAY_DENSITY, DISPLAY_WIDTH, DISPLAY_HEIGHT)
+ .parse(DOUBLE_CUTOUT_SPEC);
+ }
+ }
+
+ @Test
+ public void parseLongEdgeCutout() {
+ final String spec = "M 0,0\n"
+ + "H 48\n"
+ + "V 48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "H 48\n"
+ + "V 48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@left\n"
+ + "@center_vertical\n"
+ + "M 0,0\n"
+ + "H -48\n"
+ + "V 48\n"
+ + "H 48\n"
+ + "Z\n"
+ + "@right\n"
+ + "@dp";
+
+ final BenchmarkState state = mBenchmarkRule.getState();
+ while (state.keepRunning()) {
+ new CutoutSpecification.Parser(DISPLAY_DENSITY, DISPLAY_WIDTH, DISPLAY_HEIGHT)
+ .parse(spec);
+ }
+ }
+
+ @Test
+ public void parseShortEdgeCutout() {
+ final String spec = "M 0,0\n"
+ + "H 48\n"
+ + "V 48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@bottom\n"
+ + "M 0,0\n"
+ + "H 48\n"
+ + "V -48\n"
+ + "H -48\n"
+ + "Z\n"
+ + "@dp";
+
+ final BenchmarkState state = mBenchmarkRule.getState();
+ while (state.keepRunning()) {
+ new CutoutSpecification.Parser(DISPLAY_DENSITY, DISPLAY_WIDTH, DISPLAY_HEIGHT)
+ .parse(spec);
+ }
+ }
+}
diff --git a/android/view/Display.java b/android/view/Display.java
new file mode 100644
index 0000000..9cb0d1f
--- /dev/null
+++ b/android/view/Display.java
@@ -0,0 +1,2072 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.Manifest.permission.CONFIGURE_DISPLAY_COLOR_MODE;
+import static android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.app.ActivityThread;
+import android.app.KeyguardManager;
+import android.app.WindowConfiguration;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.ColorSpace;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DeviceProductInfo;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Provides information about the size and density of a logical display.
+ * <p>
+ * The display area is described in two different ways.
+ * <ul>
+ * <li>The application display area specifies the part of the display that may contain
+ * an application window, excluding the system decorations. The application display area may
+ * be smaller than the real display area because the system subtracts the space needed
+ * for decor elements such as the status bar. Use {@link WindowMetrics#getBounds()} to query the
+ * application window bounds.</li>
+ * <li>The real display area specifies the part of the display that is accessible to an application
+ * in the current system state. The real display area may be smaller than the physical size of the
+ * display in a few scenarios. Use {@link WindowManager#getCurrentWindowMetrics()} to identify the
+ * current size of the activity window. UI-related work, such as choosing UI layouts, should rely
+ * upon {@link WindowMetrics#getBounds()}. See {@link #getRealSize} / {@link #getRealMetrics} for
+ * details.</li>
+ * </ul>
+ * </p><p>
+ * A logical display does not necessarily represent a particular physical display device
+ * such as the internal display or an external display. The contents of a logical
+ * display may be presented on one or more physical displays according to the devices
+ * that are currently attached and whether mirroring has been enabled.
+ * </p>
+ */
+public final class Display {
+ private static final String TAG = "Display";
+ private static final boolean DEBUG = false;
+
+ private final Object mLock = new Object();
+ private final DisplayManagerGlobal mGlobal;
+ private final int mDisplayId;
+ private final int mFlags;
+ private final int mType;
+ private final int mOwnerUid;
+ private final String mOwnerPackageName;
+ private final Resources mResources;
+ private DisplayAdjustments mDisplayAdjustments;
+
+ @UnsupportedAppUsage
+ private DisplayInfo mDisplayInfo; // never null
+ private boolean mIsValid;
+
+ // Temporary display metrics structure used for compatibility mode.
+ private final DisplayMetrics mTempMetrics = new DisplayMetrics();
+
+ // We cache the app width and height properties briefly between calls
+ // to getHeight() and getWidth() to ensure that applications perceive
+ // consistent results when the size changes (most of the time).
+ // Applications should now be using WindowMetrics instead.
+ private static final int CACHED_APP_SIZE_DURATION_MILLIS = 20;
+ private long mLastCachedAppSizeUpdate;
+ private int mCachedAppWidthCompat;
+ private int mCachedAppHeightCompat;
+
+ /**
+ * Indicates that the application is started in a different rotation than the real display, so
+ * the display information may be adjusted. That ensures the methods {@link #getRotation},
+ * {@link #getRealSize}, {@link #getRealMetrics}, and {@link #getCutout} are consistent with how
+ * the application window is laid out.
+ */
+ private boolean mMayAdjustByFixedRotation;
+
+ /**
+ * Cache if the application is the recents component.
+ * TODO(b/179308296) Remove once Launcher addresses issue
+ */
+ private Optional<Boolean> mIsRecentsComponent = Optional.empty();
+
+ /**
+ * The default Display id, which is the id of the primary display assuming there is one.
+ */
+ public static final int DEFAULT_DISPLAY = 0;
+
+ /**
+ * Invalid display id.
+ */
+ public static final int INVALID_DISPLAY = -1;
+
+ /**
+ * The default display group id, which is the display group id of the primary display assuming
+ * there is one.
+ * @hide
+ */
+ public static final int DEFAULT_DISPLAY_GROUP = 0;
+
+ /**
+ * Invalid display group id.
+ * @hide
+ */
+ public static final int INVALID_DISPLAY_GROUP = -1;
+
+ /**
+ * Display flag: Indicates that the display supports compositing content
+ * that is stored in protected graphics buffers.
+ * <p>
+ * If this flag is set then the display device supports compositing protected buffers.
+ * </p><p>
+ * If this flag is not set then the display device may not support compositing
+ * protected buffers; the user may see a blank region on the screen instead of
+ * the protected content.
+ * </p><p>
+ * Secure (DRM) video decoders may allocate protected graphics buffers to request that
+ * a hardware-protected path be provided between the video decoder and the external
+ * display sink. If a hardware-protected path is not available, then content stored
+ * in protected graphics buffers may not be composited.
+ * </p><p>
+ * An application can use the absence of this flag as a hint that it should not use protected
+ * buffers for this display because the content may not be visible. For example,
+ * if the flag is not set then the application may choose not to show content on this
+ * display, show an informative error message, select an alternate content stream
+ * or adopt a different strategy for decoding content that does not rely on
+ * protected buffers.
+ * </p>
+ *
+ * @see #getFlags
+ */
+ public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 1 << 0;
+
+ /**
+ * Display flag: Indicates that the display has a secure video output and
+ * supports compositing secure surfaces.
+ * <p>
+ * If this flag is set then the display device has a secure video output
+ * and is capable of showing secure surfaces. It may also be capable of
+ * showing {@link #FLAG_SUPPORTS_PROTECTED_BUFFERS protected buffers}.
+ * </p><p>
+ * If this flag is not set then the display device may not have a secure video
+ * output; the user may see a blank region on the screen instead of
+ * the contents of secure surfaces or protected buffers.
+ * </p><p>
+ * Secure surfaces are used to prevent content rendered into those surfaces
+ * by applications from appearing in screenshots or from being viewed
+ * on non-secure displays. Protected buffers are used by secure video decoders
+ * for a similar purpose.
+ * </p><p>
+ * An application creates a window with a secure surface by specifying the
+ * {@link WindowManager.LayoutParams#FLAG_SECURE} window flag.
+ * Likewise, an application creates a {@link SurfaceView} with a secure surface
+ * by calling {@link SurfaceView#setSecure} before attaching the secure view to
+ * its containing window.
+ * </p><p>
+ * An application can use the absence of this flag as a hint that it should not create
+ * secure surfaces or protected buffers on this display because the content may
+ * not be visible. For example, if the flag is not set then the application may
+ * choose not to show content on this display, show an informative error message,
+ * select an alternate content stream or adopt a different strategy for decoding
+ * content that does not rely on secure surfaces or protected buffers.
+ * </p>
+ *
+ * @see #getFlags
+ */
+ public static final int FLAG_SECURE = 1 << 1;
+
+ /**
+ * Display flag: Indicates that the display is private. Only the application that
+ * owns the display and apps that are already on the display can create windows on it.
+ *
+ * @see #getFlags
+ */
+ public static final int FLAG_PRIVATE = 1 << 2;
+
+ /**
+ * Display flag: Indicates that the display is a presentation display.
+ * <p>
+ * This flag identifies secondary displays that are suitable for
+ * use as presentation displays such as external or wireless displays. Applications
+ * may automatically project their content to presentation displays to provide
+ * richer second screen experiences.
+ * </p>
+ *
+ * @see #getFlags
+ */
+ public static final int FLAG_PRESENTATION = 1 << 3;
+
+ /**
+ * Display flag: Indicates that the display has a round shape.
+ * <p>
+ * This flag identifies displays that are circular, elliptical or otherwise
+ * do not permit the user to see all the way to the logical corners of the display.
+ * </p>
+ *
+ * @see #getFlags
+ */
+ public static final int FLAG_ROUND = 1 << 4;
+
+ /**
+ * Display flag: Indicates that the display can show its content when non-secure keyguard is
+ * shown.
+ * <p>
+ * This flag identifies secondary displays that will continue showing content if keyguard can be
+ * dismissed without entering credentials.
+ * </p><p>
+ * An example of usage is a virtual display which content is displayed on external hardware
+ * display that is not visible to the system directly.
+ * </p>
+ *
+ * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD
+ * @see KeyguardManager#isDeviceSecure()
+ * @see KeyguardManager#isDeviceLocked()
+ * @see #getFlags
+ * @hide
+ */
+ // TODO (b/114338689): Remove the flag and use IWindowManager#shouldShowWithInsecureKeyguard
+ public static final int FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD = 1 << 5;
+
+ /**
+ * Display flag: Indicates that the display should show system decorations.
+ * <p>
+ * This flag identifies secondary displays that should show system decorations, such as status
+ * bar, navigation bar, home activity or IME.
+ * </p>
+ * <p>Note that this flag doesn't work without {@link #FLAG_TRUSTED}</p>
+ *
+ * @see #getFlags()
+ * @hide
+ */
+ // TODO (b/114338689): Remove the flag and use IWindowManager#setShouldShowSystemDecors
+ public static final int FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 6;
+
+ /**
+ * Flag: The display is trusted to show system decorations and receive inputs without users'
+ * touch.
+ * @see #FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS
+ *
+ * @see #getFlags()
+ * @hide
+ */
+ @TestApi
+ public static final int FLAG_TRUSTED = 1 << 7;
+
+ /**
+ * Flag: Indicates that the display should not be a part of the default DisplayGroup and
+ * instead be part of a new DisplayGroup.
+ *
+ * @hide
+ * @see #getFlags()
+ */
+ public static final int FLAG_OWN_DISPLAY_GROUP = 1 << 8;
+
+ /**
+ * Display flag: Indicates that the contents of the display should not be scaled
+ * to fit the physical screen dimensions. Used for development only to emulate
+ * devices with smaller physicals screens while preserving density.
+ *
+ * @hide
+ */
+ public static final int FLAG_SCALING_DISABLED = 1 << 30;
+
+ /**
+ * Display type: Unknown display type.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public static final int TYPE_UNKNOWN = 0;
+
+ /**
+ * Display type: Physical display connected through an internal port.
+ * @hide
+ */
+ @TestApi
+ public static final int TYPE_INTERNAL = 1;
+
+ /**
+ * Display type: Physical display connected through an external port.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @TestApi
+ public static final int TYPE_EXTERNAL = 2;
+
+ /**
+ * Display type: WiFi display.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public static final int TYPE_WIFI = 3;
+
+ /**
+ * Display type: Overlay display.
+ * @hide
+ */
+ @TestApi
+ public static final int TYPE_OVERLAY = 4;
+
+ /**
+ * Display type: Virtual display.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public static final int TYPE_VIRTUAL = 5;
+
+ /**
+ * Display state: The display state is unknown.
+ *
+ * @see #getState
+ */
+ public static final int STATE_UNKNOWN = ViewProtoEnums.DISPLAY_STATE_UNKNOWN; // 0
+
+ /**
+ * Display state: The display is off.
+ *
+ * @see #getState
+ */
+ public static final int STATE_OFF = ViewProtoEnums.DISPLAY_STATE_OFF; // 1
+
+ /**
+ * Display state: The display is on.
+ *
+ * @see #getState
+ */
+ public static final int STATE_ON = ViewProtoEnums.DISPLAY_STATE_ON; // 2
+
+ /**
+ * Display state: The display is dozing in a low power state; it is still
+ * on but is optimized for showing system-provided content while the
+ * device is non-interactive.
+ *
+ * @see #getState
+ * @see android.os.PowerManager#isInteractive
+ */
+ public static final int STATE_DOZE = ViewProtoEnums.DISPLAY_STATE_DOZE; // 3
+
+ /**
+ * Display state: The display is dozing in a suspended low power state; it is still
+ * on but the CPU is not updating it. This may be used in one of two ways: to show
+ * static system-provided content while the device is non-interactive, or to allow
+ * a "Sidekick" compute resource to update the display. For this reason, the
+ * CPU must not control the display in this mode.
+ *
+ * @see #getState
+ * @see android.os.PowerManager#isInteractive
+ */
+ public static final int STATE_DOZE_SUSPEND = ViewProtoEnums.DISPLAY_STATE_DOZE_SUSPEND; // 4
+
+ /**
+ * Display state: The display is on and optimized for VR mode.
+ *
+ * @see #getState
+ * @see android.os.PowerManager#isInteractive
+ */
+ public static final int STATE_VR = ViewProtoEnums.DISPLAY_STATE_VR; // 5
+
+ /**
+ * Display state: The display is in a suspended full power state; it is still
+ * on but the CPU is not updating it. This may be used in one of two ways: to show
+ * static system-provided content while the device is non-interactive, or to allow
+ * a "Sidekick" compute resource to update the display. For this reason, the
+ * CPU must not control the display in this mode.
+ *
+ * @see #getState
+ * @see android.os.PowerManager#isInteractive
+ */
+ public static final int STATE_ON_SUSPEND = ViewProtoEnums.DISPLAY_STATE_ON_SUSPEND; // 6
+
+ /* The color mode constants defined below must be kept in sync with the ones in
+ * system/core/include/system/graphics-base.h */
+
+ /**
+ * Display color mode: The current color mode is unknown or invalid.
+ * @hide
+ */
+ public static final int COLOR_MODE_INVALID = -1;
+
+ /**
+ * Display color mode: The default or native gamut of the display.
+ * @hide
+ */
+ public static final int COLOR_MODE_DEFAULT = 0;
+
+ /** @hide */
+ public static final int COLOR_MODE_BT601_625 = 1;
+ /** @hide */
+ public static final int COLOR_MODE_BT601_625_UNADJUSTED = 2;
+ /** @hide */
+ public static final int COLOR_MODE_BT601_525 = 3;
+ /** @hide */
+ public static final int COLOR_MODE_BT601_525_UNADJUSTED = 4;
+ /** @hide */
+ public static final int COLOR_MODE_BT709 = 5;
+ /** @hide */
+ public static final int COLOR_MODE_DCI_P3 = 6;
+ /** @hide */
+ public static final int COLOR_MODE_SRGB = 7;
+ /** @hide */
+ public static final int COLOR_MODE_ADOBE_RGB = 8;
+ /** @hide */
+ public static final int COLOR_MODE_DISPLAY_P3 = 9;
+
+ /** @hide **/
+ @IntDef(prefix = {"COLOR_MODE_"}, value = {
+ COLOR_MODE_INVALID,
+ COLOR_MODE_DEFAULT,
+ COLOR_MODE_BT601_625,
+ COLOR_MODE_BT601_625_UNADJUSTED,
+ COLOR_MODE_BT601_525,
+ COLOR_MODE_BT601_525_UNADJUSTED,
+ COLOR_MODE_BT709,
+ COLOR_MODE_DCI_P3,
+ COLOR_MODE_SRGB,
+ COLOR_MODE_ADOBE_RGB,
+ COLOR_MODE_DISPLAY_P3
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ColorMode {}
+
+ /**
+ * Indicates that when display is removed, all its activities will be moved to the primary
+ * display and the topmost activity should become focused.
+ *
+ * @hide
+ */
+ // TODO (b/114338689): Remove the flag and use WindowManager#REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY
+ public static final int REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY = 0;
+ /**
+ * Indicates that when display is removed, all its stacks and tasks will be removed, all
+ * activities will be destroyed according to the usual lifecycle.
+ *
+ * @hide
+ */
+ // TODO (b/114338689): Remove the flag and use WindowManager#REMOVE_CONTENT_MODE_DESTROY
+ public static final int REMOVE_MODE_DESTROY_CONTENT = 1;
+
+ /** @hide */
+ public static final int DISPLAY_MODE_ID_FOR_FRAME_RATE_OVERRIDE = 0xFF;
+
+ /**
+ * Internal method to create a display.
+ * The display created with this method will have a static {@link DisplayAdjustments} applied.
+ * Applications should use {@link android.content.Context#getDisplay} with
+ * {@link android.app.Activity} or a context associated with a {@link Display} via
+ * {@link android.content.Context#createDisplayContext(Display)}
+ * to get a display object associated with a {@link android.app.Context}, or
+ * {@link android.hardware.display.DisplayManager#getDisplay} to get a display object by id.
+ *
+ * @see android.content.Context#getDisplay()
+ * @see android.content.Context#createDisplayContext(Display)
+ * @hide
+ */
+ public Display(DisplayManagerGlobal global, int displayId, /*@NotNull*/ DisplayInfo displayInfo,
+ DisplayAdjustments daj) {
+ this(global, displayId, displayInfo, daj, null /*res*/);
+ }
+
+ /**
+ * Internal method to create a display.
+ * The display created with this method will be adjusted based on the adjustments in the
+ * supplied {@link Resources}.
+ *
+ * @hide
+ */
+ public Display(DisplayManagerGlobal global, int displayId, /*@NotNull*/ DisplayInfo displayInfo,
+ Resources res) {
+ this(global, displayId, displayInfo, null /*daj*/, res);
+ }
+
+ private Display(DisplayManagerGlobal global, int displayId,
+ /*@NotNull*/ DisplayInfo displayInfo, DisplayAdjustments daj, Resources res) {
+ mGlobal = global;
+ mDisplayId = displayId;
+ mDisplayInfo = displayInfo;
+ mResources = res;
+ mDisplayAdjustments = mResources != null
+ ? new DisplayAdjustments(mResources.getConfiguration())
+ : daj != null ? new DisplayAdjustments(daj) : new DisplayAdjustments();
+ mIsValid = true;
+
+ // Cache properties that cannot change as long as the display is valid.
+ mFlags = displayInfo.flags;
+ mType = displayInfo.type;
+ mOwnerUid = displayInfo.ownerUid;
+ mOwnerPackageName = displayInfo.ownerPackageName;
+ }
+
+ /**
+ * Gets the display id.
+ * <p>
+ * Each logical display has a unique id.
+ * The default display has id {@link #DEFAULT_DISPLAY}.
+ * </p>
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Gets the display unique id.
+ * <p>
+ * Unique id is different from display id because physical displays have stable unique id across
+ * reboots.
+ *
+ * @see com.android.service.display.DisplayDevice#hasStableUniqueId().
+ * @hide
+ */
+ public String getUniqueId() {
+ return mDisplayInfo.uniqueId;
+ }
+
+ /**
+ * Returns true if this display is still valid, false if the display has been removed.
+ *
+ * If the display is invalid, then the methods of this class will
+ * continue to report the most recently observed display information.
+ * However, it is unwise (and rather fruitless) to continue using a
+ * {@link Display} object after the display's demise.
+ *
+ * It's possible for a display that was previously invalid to become
+ * valid again if a display with the same id is reconnected.
+ *
+ * @return True if the display is still valid.
+ */
+ public boolean isValid() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mIsValid;
+ }
+ }
+
+ /**
+ * Gets a full copy of the display information.
+ *
+ * @param outDisplayInfo The object to receive the copy of the display information.
+ * @return True if the display is still valid.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ public boolean getDisplayInfo(DisplayInfo outDisplayInfo) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ outDisplayInfo.copyFrom(mDisplayInfo);
+ return mIsValid;
+ }
+ }
+
+ /**
+ * Gets the display's layer stack.
+ *
+ * Each display has its own independent layer stack upon which surfaces
+ * are placed to be managed by surface flinger.
+ *
+ * @return The display's layer stack number.
+ * @hide
+ */
+ public int getLayerStack() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.layerStack;
+ }
+ }
+
+ /**
+ * Returns a combination of flags that describe the capabilities of the display.
+ *
+ * @return The display flags.
+ *
+ * @see #FLAG_SUPPORTS_PROTECTED_BUFFERS
+ * @see #FLAG_SECURE
+ * @see #FLAG_PRIVATE
+ * @see #FLAG_ROUND
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Gets the display type.
+ *
+ * @return The display type.
+ *
+ * @see #TYPE_UNKNOWN
+ * @see #TYPE_INTERNAL
+ * @see #TYPE_EXTERNAL
+ * @see #TYPE_WIFI
+ * @see #TYPE_OVERLAY
+ * @see #TYPE_VIRTUAL
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Gets the display address, or null if none.
+ * Interpretation varies by display type.
+ *
+ * @return The display address.
+ * @hide
+ */
+ public DisplayAddress getAddress() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.address;
+ }
+ }
+
+ /**
+ * Gets the UID of the application that owns this display, or zero if it is
+ * owned by the system.
+ * <p>
+ * If the display is private, then only the owner can use it.
+ * </p>
+ *
+ * @hide
+ */
+ public int getOwnerUid() {
+ return mOwnerUid;
+ }
+
+ /**
+ * Gets the package name of the application that owns this display, or null if it is
+ * owned by the system.
+ * <p>
+ * If the display is private, then only the owner can use it.
+ * </p>
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public String getOwnerPackageName() {
+ return mOwnerPackageName;
+ }
+
+ /**
+ * Gets the compatibility info used by this display instance.
+ *
+ * @return The display adjustments holder, or null if none is required.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public DisplayAdjustments getDisplayAdjustments() {
+ if (mResources != null) {
+ final DisplayAdjustments currentAdjustments = mResources.getDisplayAdjustments();
+ if (!mDisplayAdjustments.equals(currentAdjustments)) {
+ mDisplayAdjustments = new DisplayAdjustments(currentAdjustments);
+ }
+ }
+
+ return mDisplayAdjustments;
+ }
+
+ /**
+ * Gets the name of the display.
+ * <p>
+ * Note that some displays may be renamed by the user.
+ * </p>
+ *
+ * @return The display's name.
+ */
+ public String getName() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.name;
+ }
+ }
+
+ /**
+ * Gets the default brightness configured for the display.
+ *
+ * @return Default brightness between 0.0-1.0
+ * @hide
+ */
+ public float getBrightnessDefault() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.brightnessDefault;
+ }
+ }
+
+ /**
+ * @return Brightness information about the display.
+ * @hide
+ */
+ @RequiresPermission(CONTROL_DISPLAY_BRIGHTNESS)
+ public @Nullable BrightnessInfo getBrightnessInfo() {
+ return mGlobal.getBrightnessInfo(mDisplayId);
+ }
+
+ /**
+ * Gets the size of the display, in pixels.
+ * Value returned by this method does not necessarily represent the actual raw size
+ * (native resolution) of the display.
+ * <p>
+ * 1. The returned size may be adjusted to exclude certain system decor elements
+ * that are always visible.
+ * </p><p>
+ * 2. It may be scaled to provide compatibility with older applications that
+ * were originally designed for smaller displays.
+ * </p><p>
+ * 3. It can be different depending on the WindowManager to which the display belongs.
+ * </p><p>
+ * - If requested from non-Activity context (e.g. Application context via
+ * {@code (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE)})
+ * it will report the size of the entire display based on current rotation and with subtracted
+ * system decoration areas.
+ * </p><p>
+ * - If requested from activity (either using {@code getWindowManager()} or
+ * {@code (WindowManager) getSystemService(Context.WINDOW_SERVICE)}) resulting size will
+ * correspond to current app window size. In this case it can be smaller than physical size in
+ * multi-window mode.
+ * </p><p>
+ * Typically for the purposes of layout apps should make a request from activity context
+ * to obtain size available for the app content.
+ * </p>
+ *
+ * @param outSize A {@link Point} object to receive the size information.
+ * @deprecated Use {@link WindowManager#getCurrentWindowMetrics()} to obtain an instance of
+ * {@link WindowMetrics} and use {@link WindowMetrics#getBounds()} instead.
+ */
+ @Deprecated
+ public void getSize(Point outSize) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ mDisplayInfo.getAppMetrics(mTempMetrics, getDisplayAdjustments());
+ outSize.x = mTempMetrics.widthPixels;
+ outSize.y = mTempMetrics.heightPixels;
+ }
+ }
+
+ /**
+ * Gets the size of the display as a rectangle, in pixels.
+ *
+ * @param outSize A {@link Rect} object to receive the size information.
+ * @deprecated Use {@link WindowMetrics#getBounds()} to get the dimensions of the application
+ * window area.
+ */
+ @Deprecated
+ public void getRectSize(Rect outSize) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ mDisplayInfo.getAppMetrics(mTempMetrics, getDisplayAdjustments());
+ outSize.set(0, 0, mTempMetrics.widthPixels, mTempMetrics.heightPixels);
+ }
+ }
+
+ /**
+ * Return the range of display sizes an application can expect to encounter
+ * under normal operation, as long as there is no physical change in screen
+ * size. This is basically the sizes you will see as the orientation
+ * changes, taking into account whatever screen decoration there is in
+ * each rotation. For example, the status bar is always at the top of the
+ * screen, so it will reduce the height both in landscape and portrait, and
+ * the smallest height returned here will be the smaller of the two.
+ *
+ * This is intended for applications to get an idea of the range of sizes
+ * they will encounter while going through device rotations, to provide a
+ * stable UI through rotation. The sizes here take into account all standard
+ * system decorations that reduce the size actually available to the
+ * application: the status bar, navigation bar, system bar, etc. It does
+ * <em>not</em> take into account more transient elements like an IME
+ * soft keyboard.
+ *
+ * @param outSmallestSize Filled in with the smallest width and height
+ * that the application will encounter, in pixels (not dp units). The x
+ * (width) dimension here directly corresponds to
+ * {@link android.content.res.Configuration#smallestScreenWidthDp
+ * Configuration.smallestScreenWidthDp}, except the value here is in raw
+ * screen pixels rather than dp units. Your application may of course
+ * still get smaller space yet if, for example, a soft keyboard is
+ * being displayed.
+ * @param outLargestSize Filled in with the largest width and height
+ * that the application will encounter, in pixels (not dp units). Your
+ * application may of course still get larger space than this if,
+ * for example, screen decorations like the status bar are being hidden.
+ */
+ public void getCurrentSizeRange(Point outSmallestSize, Point outLargestSize) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ outSmallestSize.x = mDisplayInfo.smallestNominalAppWidth;
+ outSmallestSize.y = mDisplayInfo.smallestNominalAppHeight;
+ outLargestSize.x = mDisplayInfo.largestNominalAppWidth;
+ outLargestSize.y = mDisplayInfo.largestNominalAppHeight;
+ }
+ }
+
+ /**
+ * Return the maximum screen size dimension that will happen. This is
+ * mostly for wallpapers.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getMaximumSizeDimension() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return Math.max(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link WindowMetrics#getBounds#width()} instead.
+ */
+ @Deprecated
+ public int getWidth() {
+ synchronized (mLock) {
+ updateCachedAppSizeIfNeededLocked();
+ return mCachedAppWidthCompat;
+ }
+ }
+
+ /**
+ * @deprecated Use {@link WindowMetrics#getBounds()#height()} instead.
+ */
+ @Deprecated
+ public int getHeight() {
+ synchronized (mLock) {
+ updateCachedAppSizeIfNeededLocked();
+ return mCachedAppHeightCompat;
+ }
+ }
+
+ /**
+ * Returns the rotation of the screen from its "natural" orientation.
+ * The returned value may be {@link Surface#ROTATION_0 Surface.ROTATION_0}
+ * (no rotation), {@link Surface#ROTATION_90 Surface.ROTATION_90},
+ * {@link Surface#ROTATION_180 Surface.ROTATION_180}, or
+ * {@link Surface#ROTATION_270 Surface.ROTATION_270}. For
+ * example, if a device has a naturally tall screen, and the user has
+ * turned it on its side to go into a landscape orientation, the value
+ * returned here may be either {@link Surface#ROTATION_90 Surface.ROTATION_90}
+ * or {@link Surface#ROTATION_270 Surface.ROTATION_270} depending on
+ * the direction it was turned. The angle is the rotation of the drawn
+ * graphics on the screen, which is the opposite direction of the physical
+ * rotation of the device. For example, if the device is rotated 90
+ * degrees counter-clockwise, to compensate rendering will be rotated by
+ * 90 degrees clockwise and thus the returned value here will be
+ * {@link Surface#ROTATION_90 Surface.ROTATION_90}.
+ */
+ @Surface.Rotation
+ public int getRotation() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mMayAdjustByFixedRotation
+ ? getDisplayAdjustments().getRotation(mDisplayInfo.rotation)
+ : mDisplayInfo.rotation;
+ }
+ }
+
+ /**
+ * @deprecated use {@link #getRotation}
+ * @return orientation of this display.
+ */
+ @Deprecated
+ @Surface.Rotation
+ public int getOrientation() {
+ return getRotation();
+ }
+
+
+ /**
+ * Returns the {@link DisplayCutout}, or {@code null} if there is none.
+ *
+ * @see DisplayCutout
+ */
+ @Nullable
+ public DisplayCutout getCutout() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mMayAdjustByFixedRotation
+ ? getDisplayAdjustments().getDisplayCutout(mDisplayInfo.displayCutout)
+ : mDisplayInfo.displayCutout;
+ }
+ }
+
+ /**
+ * Returns the {@link RoundedCorner} of the given position if there is one.
+ *
+ * @param position the position of the rounded corner on the display.
+ *
+ * @return the rounded corner of the given position. Returns {@code null} if there is none.
+ */
+ @SuppressLint("VisiblySynchronized")
+ @Nullable
+ public RoundedCorner getRoundedCorner(@RoundedCorner.Position int position) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ RoundedCorners roundedCorners;
+ if (mMayAdjustByFixedRotation) {
+ roundedCorners = getDisplayAdjustments().adjustRoundedCorner(
+ mDisplayInfo.roundedCorners,
+ mDisplayInfo.rotation,
+ mDisplayInfo.logicalWidth,
+ mDisplayInfo.logicalHeight);
+ } else {
+ roundedCorners = mDisplayInfo.roundedCorners;
+ }
+ return roundedCorners == null ? null : roundedCorners.getRoundedCorner(position);
+ }
+ }
+
+ /**
+ * Gets the pixel format of the display.
+ * @return One of the constants defined in {@link android.graphics.PixelFormat}.
+ *
+ * @deprecated This method is no longer supported.
+ * The result is always {@link PixelFormat#RGBA_8888}.
+ */
+ @Deprecated
+ public int getPixelFormat() {
+ return PixelFormat.RGBA_8888;
+ }
+
+ /**
+ * Gets the refresh rate of this display in frames per second.
+ */
+ public float getRefreshRate() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.getRefreshRate();
+ }
+ }
+
+ /**
+ * Get the supported refresh rates of this display in frames per second.
+ * <p>
+ * This method only returns refresh rates for the display's default modes. For more options, use
+ * {@link #getSupportedModes()}.
+ *
+ * @deprecated use {@link #getSupportedModes()} instead
+ */
+ @Deprecated
+ public float[] getSupportedRefreshRates() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.getDefaultRefreshRates();
+ }
+ }
+
+ /**
+ * Returns the active mode of the display.
+ */
+ public Mode getMode() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.getMode();
+ }
+ }
+
+ /**
+ * Gets the supported modes of this display.
+ */
+ public Mode[] getSupportedModes() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ final Display.Mode[] modes = mDisplayInfo.supportedModes;
+ return Arrays.copyOf(modes, modes.length);
+ }
+ }
+
+ /**
+ * <p> Returns true if the connected display can be switched into a mode with minimal
+ * post processing. </p>
+ *
+ * <p> If the Display sink is connected via HDMI, this method will return true if the
+ * display supports either Auto Low Latency Mode or Game Content Type.
+ *
+ * <p> If the Display sink has an internal connection or uses some other protocol than
+ * HDMI, this method will return true if the sink can be switched into an
+ * implementation-defined low latency image processing mode. </p>
+ *
+ * <p> The ability to switch to a mode with minimal post processing may be disabled
+ * by a user setting in the system settings menu. In that case, this method returns
+ * false. </p>
+ *
+ * @see android.view.Window#setPreferMinimalPostProcessing
+ */
+ @SuppressLint("VisiblySynchronized")
+ public boolean isMinimalPostProcessingSupported() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.minimalPostProcessingSupported;
+ }
+ }
+
+ /**
+ * Request the display applies a color mode.
+ * @hide
+ */
+ @RequiresPermission(CONFIGURE_DISPLAY_COLOR_MODE)
+ public void requestColorMode(int colorMode) {
+ mGlobal.requestColorMode(mDisplayId, colorMode);
+ }
+
+ /**
+ * Returns the active color mode of this display
+ * @hide
+ */
+ public int getColorMode() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.colorMode;
+ }
+ }
+
+ /**
+ * @hide
+ * Get current remove mode of the display - what actions should be performed with the display's
+ * content when it is removed. Default behavior for public displays in this case is to move all
+ * activities to the primary display and make it focused. For private display - destroy all
+ * activities.
+ *
+ * @see #REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY
+ * @see #REMOVE_MODE_DESTROY_CONTENT
+ */
+ // TODO (b/114338689): Remove the method and use IWindowManager#getRemoveContentMode
+ public int getRemoveMode() {
+ return mDisplayInfo.removeMode;
+ }
+
+ /**
+ * Returns the display's HDR capabilities.
+ *
+ * @see #isHdr()
+ */
+ public HdrCapabilities getHdrCapabilities() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ if (mDisplayInfo.userDisabledHdrTypes.length == 0) {
+ return mDisplayInfo.hdrCapabilities;
+ }
+
+ if (mDisplayInfo.hdrCapabilities == null) {
+ return null;
+ }
+
+ ArraySet<Integer> enabledTypesSet = new ArraySet<>();
+ for (int supportedType : mDisplayInfo.hdrCapabilities.getSupportedHdrTypes()) {
+ boolean typeDisabled = false;
+ for (int userDisabledType : mDisplayInfo.userDisabledHdrTypes) {
+ if (supportedType == userDisabledType) {
+ typeDisabled = true;
+ break;
+ }
+ }
+ if (!typeDisabled) {
+ enabledTypesSet.add(supportedType);
+ }
+ }
+
+ int[] enabledTypes = new int[enabledTypesSet.size()];
+ int index = 0;
+ for (int enabledType : enabledTypesSet) {
+ enabledTypes[index++] = enabledType;
+ }
+ return new HdrCapabilities(enabledTypes,
+ mDisplayInfo.hdrCapabilities.mMaxLuminance,
+ mDisplayInfo.hdrCapabilities.mMaxAverageLuminance,
+ mDisplayInfo.hdrCapabilities.mMinLuminance);
+ }
+ }
+
+ /**
+ * @hide
+ * Returns the display's HDR supported types.
+ *
+ * @see #isHdr()
+ * @see HdrCapabilities#getSupportedHdrTypes()
+ */
+ @TestApi
+ @NonNull
+ public int[] getReportedHdrTypes() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ if (mDisplayInfo.hdrCapabilities == null) {
+ return new int[0];
+ }
+ return mDisplayInfo.hdrCapabilities.getSupportedHdrTypes();
+ }
+ }
+
+ /**
+ * Returns whether this display supports any HDR type.
+ *
+ * @see #getHdrCapabilities()
+ * @see HdrCapabilities#getSupportedHdrTypes()
+ */
+ public boolean isHdr() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ HdrCapabilities hdrCapabilities = getHdrCapabilities();
+ if (hdrCapabilities == null) {
+ return false;
+ }
+ return !(hdrCapabilities.getSupportedHdrTypes().length == 0);
+ }
+ }
+
+ /**
+ * Returns whether this display can be used to display wide color gamut content.
+ * This does not necessarily mean the device itself can render wide color gamut
+ * content. To ensure wide color gamut content can be produced, refer to
+ * {@link Configuration#isScreenWideColorGamut()}.
+ */
+ public boolean isWideColorGamut() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.isWideColorGamut();
+ }
+ }
+
+ /**
+ * Returns the preferred wide color space of the Display.
+ * The returned wide gamut color space is based on hardware capability and
+ * is preferred by the composition pipeline.
+ * Returns null if the display doesn't support wide color gamut.
+ * {@link Display#isWideColorGamut()}.
+ */
+ @Nullable
+ public ColorSpace getPreferredWideGamutColorSpace() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ if (mDisplayInfo.isWideColorGamut()) {
+ return mGlobal.getPreferredWideGamutColorSpace();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Gets the supported color modes of this device.
+ * @hide
+ */
+ public int[] getSupportedColorModes() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ int[] colorModes = mDisplayInfo.supportedColorModes;
+ return Arrays.copyOf(colorModes, colorModes.length);
+ }
+ }
+
+ /**
+ * Gets the supported wide color gamuts of this device.
+ *
+ * @return Supported WCG color spaces.
+ * @hide
+ */
+ @SuppressLint("VisiblySynchronized")
+ @NonNull
+ @TestApi
+ public @ColorMode ColorSpace[] getSupportedWideColorGamut() {
+ synchronized (mLock) {
+ final ColorSpace[] defaultColorSpaces = new ColorSpace[0];
+ updateDisplayInfoLocked();
+ if (!isWideColorGamut()) {
+ return defaultColorSpaces;
+ }
+
+ final int[] colorModes = getSupportedColorModes();
+ final List<ColorSpace> colorSpaces = new ArrayList<>();
+ for (int colorMode : colorModes) {
+ // Refer to DisplayInfo#isWideColorGamut.
+ switch (colorMode) {
+ case COLOR_MODE_DCI_P3:
+ colorSpaces.add(ColorSpace.get(ColorSpace.Named.DCI_P3));
+ break;
+ case COLOR_MODE_DISPLAY_P3:
+ colorSpaces.add(ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ break;
+ }
+ }
+ return colorSpaces.toArray(defaultColorSpaces);
+ }
+ }
+
+ /**
+ * Gets the app VSYNC offset, in nanoseconds. This is a positive value indicating
+ * the phase offset of the VSYNC events provided by Choreographer relative to the
+ * display refresh. For example, if Choreographer reports that the refresh occurred
+ * at time N, it actually occurred at (N - appVsyncOffset).
+ * <p>
+ * Apps generally do not need to be aware of this. It's only useful for fine-grained
+ * A/V synchronization.
+ */
+ public long getAppVsyncOffsetNanos() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.appVsyncOffsetNanos;
+ }
+ }
+
+ /**
+ * This is how far in advance a buffer must be queued for presentation at
+ * a given time. If you want a buffer to appear on the screen at
+ * time N, you must submit the buffer before (N - presentationDeadline).
+ * <p>
+ * The desired presentation time for GLES rendering may be set with
+ * {@link android.opengl.EGLExt#eglPresentationTimeANDROID}. For video decoding, use
+ * {@link android.media.MediaCodec#releaseOutputBuffer(int, long)}. Times are
+ * expressed in nanoseconds, using the system monotonic clock
+ * ({@link System#nanoTime}).
+ */
+ public long getPresentationDeadlineNanos() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.presentationDeadlineNanos;
+ }
+ }
+
+ /**
+ * Returns the product-specific information about the display or the directly connected
+ * device on the display chain.
+ * For example, if the display is transitively connected, this field may contain product
+ * information about the intermediate device.
+ * Returns {@code null} if product information is not available.
+ */
+ @Nullable
+ public DeviceProductInfo getDeviceProductInfo() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mDisplayInfo.deviceProductInfo;
+ }
+ }
+
+ /**
+ * Gets display metrics that describe the size and density of this display.
+ * The size returned by this method does not necessarily represent the
+ * actual raw size (native resolution) of the display.
+ * <p>
+ * 1. The returned size may be adjusted to exclude certain system decor elements
+ * that are always visible.
+ * </p><p>
+ * 2. It may be scaled to provide compatibility with older applications that
+ * were originally designed for smaller displays.
+ * </p><p>
+ * 3. It can be different depending on the WindowManager to which the display belongs.
+ * </p><p>
+ * - If requested from non-Activity context (e.g. Application context via
+ * {@code (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE)})
+ * metrics will report the size of the entire display based on current rotation and with
+ * subtracted system decoration areas.
+ * </p><p>
+ * - If requested from activity (either using {@code getWindowManager()} or
+ * {@code (WindowManager) getSystemService(Context.WINDOW_SERVICE)}) resulting metrics will
+ * correspond to current app window metrics. In this case the size can be smaller than physical
+ * size in multi-window mode.
+ * </p>
+ *
+ * @param outMetrics A {@link DisplayMetrics} object to receive the metrics.
+ * @deprecated Use {@link WindowMetrics#getBounds()} to get the dimensions of the application
+ * window area, and {@link Configuration#densityDpi} to get the current density.
+ */
+ @Deprecated
+ public void getMetrics(DisplayMetrics outMetrics) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ mDisplayInfo.getAppMetrics(outMetrics, getDisplayAdjustments());
+ }
+ }
+
+ /**
+ * Gets the size of the largest region of the display accessible to an app in the current system
+ * state, without subtracting any window decor or applying scaling factors.
+ * <p>
+ * The size is adjusted based on the current rotation of the display.
+ * <p></p>
+ * The returned size will fall into one of these scenarios:
+ * <ol>
+ * <li>The device has no partitions on the display. The returned value is the largest region
+ * of the display accessible to an app in the current system state, regardless of windowing
+ * mode.</li>
+ * <li>The device divides a single display into multiple partitions. An application is
+ * restricted to a portion of the display. This is common in devices where the display changes
+ * size, such as foldables or large screens. The returned size will match the portion of
+ * the display the application is restricted to.</li>
+ * <li>The window manager is emulating a different display size, using {@code adb shell wm
+ * size}. The returned size will match the emulated display size.</li>
+ * </ol>
+ * </p><p>
+ * The returned value is <b>unsuitable to use when sizing and placing UI elements</b>, since it
+ * does not reflect the application window size in any of these scenarios.
+ * {@link WindowManager#getCurrentWindowMetrics()} is an alternative that returns the size
+ * of the current application window, even if the window is on a device with a partitioned
+ * display. This helps prevent UI bugs where UI elements are misaligned or placed beyond the
+ * bounds of the window.
+ * <p></p>
+ * Handling multi-window mode correctly is necessary since applications are not always
+ * fullscreen. A user on a large screen device, such as a tablet or Chrome OS devices, is more
+ * likely to use multi-window modes.
+ * <p></p>
+ * For example, consider a device with a display partitioned into two halves. The user may have
+ * a fullscreen application open on the first partition. They may have two applications open in
+ * split screen (an example of multi-window mode) on the second partition, with each application
+ * consuming half of the partition. In this case,
+ * {@link WindowManager#getCurrentWindowMetrics()} reports the fullscreen window is half of the
+ * screen in size, and each split screen window is a quarter of the screen in size. On the other
+ * hand, {@link #getRealSize} reports half of the screen size for all windows, since the
+ * application windows are all restricted to their respective partitions.
+ * </p>
+ *
+ * @param outSize Set to the real size of the display.
+ * @deprecated Use {@link WindowManager#getCurrentWindowMetrics()} to identify the current size
+ * of the activity window. UI-related work, such as choosing UI layouts, should rely
+ * upon {@link WindowMetrics#getBounds()}.
+ */
+ @Deprecated
+ public void getRealSize(Point outSize) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ if (shouldReportMaxBounds()) {
+ final Rect bounds = mResources.getConfiguration()
+ .windowConfiguration.getMaxBounds();
+ outSize.x = bounds.width();
+ outSize.y = bounds.height();
+ if (DEBUG) {
+ Log.d(TAG, "getRealSize determined from max bounds: " + outSize);
+ }
+ // Skip adjusting by fixed rotation, since if it is necessary, the configuration
+ // should already reflect the expected rotation.
+ return;
+ }
+ outSize.x = mDisplayInfo.logicalWidth;
+ outSize.y = mDisplayInfo.logicalHeight;
+ if (mMayAdjustByFixedRotation) {
+ getDisplayAdjustments().adjustSize(outSize, mDisplayInfo.rotation);
+ }
+ }
+ }
+
+ /**
+ * Gets the size of the largest region of the display accessible to an app in the current system
+ * state, without subtracting any window decor or applying scaling factors.
+ * <p>
+ * The size is adjusted based on the current rotation of the display.
+ * <p></p>
+ * The returned size will fall into one of these scenarios:
+ * <ol>
+ * <li>The device has no partitions on the display. The returned value is the largest region
+ * of the display accessible to an app in the current system state, regardless of windowing
+ * mode.</li>
+ * <li>The device divides a single display into multiple partitions. An application is
+ * restricted to a portion of the display. This is common in devices where the display changes
+ * size, such as foldables or large screens. The returned size will match the portion of
+ * the display the application is restricted to.</li>
+ * <li>The window manager is emulating a different display size, using {@code adb shell wm
+ * size}. The returned size will match the emulated display size.</li>
+ * </ol>
+ * </p><p>
+ * The returned value is <b>unsuitable to use when sizing and placing UI elements</b>, since it
+ * does not reflect the application window size in any of these scenarios.
+ * {@link WindowManager#getCurrentWindowMetrics()} is an alternative that returns the size
+ * of the current application window, even if the window is on a device with a partitioned
+ * display. This helps prevent UI bugs where UI elements are misaligned or placed beyond the
+ * bounds of the window.
+ * <p></p>
+ * Handling multi-window mode correctly is necessary since applications are not always
+ * fullscreen. A user on a large screen device, such as a tablet or Chrome OS devices, is more
+ * likely to use multi-window modes.
+ * <p></p>
+ * For example, consider a device with a display partitioned into two halves. The user may have
+ * a fullscreen application open on the first partition. They may have two applications open in
+ * split screen (an example of multi-window mode) on the second partition, with each application
+ * consuming half of the partition. In this case,
+ * {@link WindowManager#getCurrentWindowMetrics()} reports the fullscreen window is half of the
+ * screen in size, and each split screen window is a quarter of the screen in size. On the other
+ * hand, {@link #getRealMetrics} reports half of the screen size for all windows, since the
+ * application windows are all restricted to their respective partitions.
+ * </p>
+ *
+ * @param outMetrics A {@link DisplayMetrics} object to receive the metrics.
+ * @deprecated Use {@link WindowManager#getCurrentWindowMetrics()} to identify the current size
+ * of the activity window. UI-related work, such as choosing UI layouts, should rely
+ * upon {@link WindowMetrics#getBounds()}. Use {@link Configuration#densityDpi} to
+ * get the current density.
+ */
+ @Deprecated
+ public void getRealMetrics(DisplayMetrics outMetrics) {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ if (shouldReportMaxBounds()) {
+ mDisplayInfo.getMaxBoundsMetrics(outMetrics,
+ CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO,
+ mResources.getConfiguration());
+ if (DEBUG) {
+ Log.d(TAG, "getRealMetrics determined from max bounds: " + outMetrics);
+ }
+ // Skip adjusting by fixed rotation, since if it is necessary, the configuration
+ // should already reflect the expected rotation.
+ return;
+ }
+ mDisplayInfo.getLogicalMetrics(outMetrics,
+ CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+ if (mMayAdjustByFixedRotation) {
+ getDisplayAdjustments().adjustMetrics(outMetrics, mDisplayInfo.rotation);
+ }
+ }
+ }
+
+ /**
+ * Determines if {@link WindowConfiguration#getMaxBounds()} should be reported as the
+ * display dimensions. The max bounds field may be smaller than the logical dimensions
+ * when apps need to be sandboxed.
+ *
+ * Depends upon {@link WindowConfiguration#getMaxBounds()} being set in
+ * {@link com.android.server.wm.ConfigurationContainer#providesMaxBounds()}. In most cases, this
+ * value reflects the size of the current DisplayArea.
+ * @return {@code true} when max bounds should be applied.
+ */
+ private boolean shouldReportMaxBounds() {
+ if (mResources == null) {
+ return false;
+ }
+ final Configuration config = mResources.getConfiguration();
+ // TODO(b/179308296) Temporarily exclude Launcher from being given max bounds, by checking
+ // if the caller is the recents component.
+ return config != null && !config.windowConfiguration.getMaxBounds().isEmpty()
+ && !isRecentsComponent();
+ }
+
+ /**
+ * Returns {@code true} when the calling package is the recents component.
+ * TODO(b/179308296) Remove once Launcher addresses issue
+ */
+ boolean isRecentsComponent() {
+ if (mIsRecentsComponent.isPresent()) {
+ return mIsRecentsComponent.get();
+ }
+ if (mResources == null) {
+ return false;
+ }
+ try {
+ String recentsComponent = mResources.getString(R.string.config_recentsComponentName);
+ if (recentsComponent == null) {
+ return false;
+ }
+ String recentsPackage = ComponentName.unflattenFromString(recentsComponent)
+ .getPackageName();
+ mIsRecentsComponent = Optional.of(recentsPackage != null
+ && recentsPackage.equals(ActivityThread.currentPackageName()));
+ return mIsRecentsComponent.get();
+ } catch (Resources.NotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Gets the state of the display, such as whether it is on or off.
+ *
+ * @return The state of the display: one of {@link #STATE_OFF}, {@link #STATE_ON},
+ * {@link #STATE_DOZE}, {@link #STATE_DOZE_SUSPEND}, {@link #STATE_ON_SUSPEND}, or
+ * {@link #STATE_UNKNOWN}.
+ */
+ public int getState() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ return mIsValid ? mDisplayInfo.state : STATE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Returns true if the specified UID has access to this display.
+ * @hide
+ */
+ @TestApi
+ public boolean hasAccess(int uid) {
+ return hasAccess(uid, mFlags, mOwnerUid, mDisplayId);
+ }
+
+ /** @hide */
+ public static boolean hasAccess(int uid, int flags, int ownerUid, int displayId) {
+ return (flags & Display.FLAG_PRIVATE) == 0
+ || uid == ownerUid
+ || uid == Process.SYSTEM_UID
+ || uid == 0
+ // Check if the UID is present on given display.
+ || DisplayManagerGlobal.getInstance().isUidPresentOnDisplay(uid, displayId);
+ }
+
+ /**
+ * Returns true if the display is a public presentation display.
+ * @hide
+ */
+ public boolean isPublicPresentation() {
+ return (mFlags & (Display.FLAG_PRIVATE | Display.FLAG_PRESENTATION)) ==
+ Display.FLAG_PRESENTATION;
+ }
+
+ /**
+ * @return {@code true} if the display is a trusted display.
+ *
+ * @see #FLAG_TRUSTED
+ * @hide
+ */
+ public boolean isTrusted() {
+ return (mFlags & FLAG_TRUSTED) == FLAG_TRUSTED;
+ }
+
+ private void updateDisplayInfoLocked() {
+ // Note: The display manager caches display info objects on our behalf.
+ DisplayInfo newInfo = mGlobal.getDisplayInfo(mDisplayId);
+ if (newInfo == null) {
+ // Preserve the old mDisplayInfo after the display is removed.
+ if (mIsValid) {
+ mIsValid = false;
+ if (DEBUG) {
+ Log.d(TAG, "Logical display " + mDisplayId + " was removed.");
+ }
+ }
+ } else {
+ // Use the new display info. (It might be the same object if nothing changed.)
+ mDisplayInfo = newInfo;
+ if (!mIsValid) {
+ mIsValid = true;
+ if (DEBUG) {
+ Log.d(TAG, "Logical display " + mDisplayId + " was recreated.");
+ }
+ }
+ }
+
+ mMayAdjustByFixedRotation = mResources != null
+ && mResources.hasOverrideDisplayAdjustments();
+ }
+
+ private void updateCachedAppSizeIfNeededLocked() {
+ long now = SystemClock.uptimeMillis();
+ if (now > mLastCachedAppSizeUpdate + CACHED_APP_SIZE_DURATION_MILLIS) {
+ updateDisplayInfoLocked();
+ mDisplayInfo.getAppMetrics(mTempMetrics, getDisplayAdjustments());
+ mCachedAppWidthCompat = mTempMetrics.widthPixels;
+ mCachedAppHeightCompat = mTempMetrics.heightPixels;
+ mLastCachedAppSizeUpdate = now;
+ }
+ }
+
+ // For debugging purposes
+ @Override
+ public String toString() {
+ synchronized (mLock) {
+ updateDisplayInfoLocked();
+ final DisplayAdjustments adjustments = getDisplayAdjustments();
+ mDisplayInfo.getAppMetrics(mTempMetrics, adjustments);
+ return "Display id " + mDisplayId + ": " + mDisplayInfo
+ + (mMayAdjustByFixedRotation
+ ? (", " + adjustments.getFixedRotationAdjustments() + ", ") : ", ")
+ + mTempMetrics + ", isValid=" + mIsValid;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static String typeToString(int type) {
+ switch (type) {
+ case TYPE_UNKNOWN:
+ return "UNKNOWN";
+ case TYPE_INTERNAL:
+ return "INTERNAL";
+ case TYPE_EXTERNAL:
+ return "EXTERNAL";
+ case TYPE_WIFI:
+ return "WIFI";
+ case TYPE_OVERLAY:
+ return "OVERLAY";
+ case TYPE_VIRTUAL:
+ return "VIRTUAL";
+ default:
+ return Integer.toString(type);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_UNKNOWN:
+ return "UNKNOWN";
+ case STATE_OFF:
+ return "OFF";
+ case STATE_ON:
+ return "ON";
+ case STATE_DOZE:
+ return "DOZE";
+ case STATE_DOZE_SUSPEND:
+ return "DOZE_SUSPEND";
+ case STATE_VR:
+ return "VR";
+ case STATE_ON_SUSPEND:
+ return "ON_SUSPEND";
+ default:
+ return Integer.toString(state);
+ }
+ }
+
+ /**
+ * Returns true if display updates may be suspended while in the specified
+ * display power state. In SUSPEND states, updates are absolutely forbidden.
+ * @hide
+ */
+ public static boolean isSuspendedState(int state) {
+ return state == STATE_OFF || state == STATE_DOZE_SUSPEND || state == STATE_ON_SUSPEND;
+ }
+
+ /**
+ * Returns true if the display may be in a reduced operating mode while in the
+ * specified display power state.
+ * @hide
+ */
+ public static boolean isDozeState(int state) {
+ return state == STATE_DOZE || state == STATE_DOZE_SUSPEND;
+ }
+
+ /**
+ * Returns true if the display is in active state such as {@link #STATE_ON}
+ * or {@link #STATE_VR}.
+ * @hide
+ */
+ public static boolean isActiveState(int state) {
+ return state == STATE_ON || state == STATE_VR;
+ }
+
+ /**
+ * Returns true if the display is in an off state such as {@link #STATE_OFF}.
+ * @hide
+ */
+ public static boolean isOffState(int state) {
+ return state == STATE_OFF;
+ }
+
+ /**
+ * Returns true if the display is in an on state such as {@link #STATE_ON}
+ * or {@link #STATE_VR} or {@link #STATE_ON_SUSPEND}.
+ * @hide
+ */
+ public static boolean isOnState(int state) {
+ return state == STATE_ON || state == STATE_VR || state == STATE_ON_SUSPEND;
+ }
+
+ /**
+ * A mode supported by a given display.
+ *
+ * @see Display#getSupportedModes()
+ */
+ public static final class Mode implements Parcelable {
+ /**
+ * @hide
+ */
+ public static final Mode[] EMPTY_ARRAY = new Mode[0];
+
+ private final int mModeId;
+ private final int mWidth;
+ private final int mHeight;
+ private final float mRefreshRate;
+ @NonNull
+ private final float[] mAlternativeRefreshRates;
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public Mode(int modeId, int width, int height, float refreshRate) {
+ this(modeId, width, height, refreshRate, new float[0]);
+ }
+
+ /**
+ * @hide
+ */
+ public Mode(int modeId, int width, int height, float refreshRate,
+ float[] alternativeRefreshRates) {
+ mModeId = modeId;
+ mWidth = width;
+ mHeight = height;
+ mRefreshRate = refreshRate;
+ mAlternativeRefreshRates =
+ Arrays.copyOf(alternativeRefreshRates, alternativeRefreshRates.length);
+ Arrays.sort(mAlternativeRefreshRates);
+ }
+
+ /**
+ * Returns this mode's id.
+ */
+ public int getModeId() {
+ return mModeId;
+ }
+
+ /**
+ * Returns the physical width of the display in pixels when configured in this mode's
+ * resolution.
+ * <p>
+ * Note that due to application UI scaling, the number of pixels made available to
+ * applications when the mode is active (as reported by {@link Display#getWidth()} may
+ * differ from the mode's actual resolution (as reported by this function).
+ * <p>
+ * For example, applications running on a 4K display may have their UI laid out and rendered
+ * in 1080p and then scaled up. Applications can take advantage of the extra resolution by
+ * rendering content through a {@link android.view.SurfaceView} using full size buffers.
+ */
+ public int getPhysicalWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Returns the physical height of the display in pixels when configured in this mode's
+ * resolution.
+ * <p>
+ * Note that due to application UI scaling, the number of pixels made available to
+ * applications when the mode is active (as reported by {@link Display#getHeight()} may
+ * differ from the mode's actual resolution (as reported by this function).
+ * <p>
+ * For example, applications running on a 4K display may have their UI laid out and rendered
+ * in 1080p and then scaled up. Applications can take advantage of the extra resolution by
+ * rendering content through a {@link android.view.SurfaceView} using full size buffers.
+ */
+ public int getPhysicalHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Returns the refresh rate in frames per second.
+ */
+ public float getRefreshRate() {
+ return mRefreshRate;
+ }
+
+ /**
+ * Returns an array of refresh rates which can be switched to seamlessly.
+ * <p>
+ * A seamless switch is one without visual interruptions, such as a black screen for
+ * a second or two.
+ * <p>
+ * Presence in this list does not guarantee a switch will occur to the desired
+ * refresh rate, but rather, if a switch does occur to a refresh rate in this list,
+ * it is guaranteed to be seamless.
+ * <p>
+ * The binary relation "refresh rate X is alternative to Y" is non-reflexive,
+ * symmetric and transitive. For example the mode 1920x1080 60Hz, will never have an
+ * alternative refresh rate of 60Hz. If 1920x1080 60Hz has an alternative of 50Hz
+ * then 1920x1080 50Hz will have alternative refresh rate of 60Hz. If 1920x1080 60Hz
+ * has an alternative of 50Hz and 1920x1080 50Hz has an alternative of 24Hz, then 1920x1080
+ * 60Hz will also have an alternative of 24Hz.
+ *
+ * @see Surface#setFrameRate
+ * @see SurfaceControl.Transaction#setFrameRate
+ */
+ @NonNull
+ public float[] getAlternativeRefreshRates() {
+ return mAlternativeRefreshRates;
+ }
+
+ /**
+ * Returns {@code true} if this mode matches the given parameters.
+ *
+ * @hide
+ */
+ public boolean matches(int width, int height, float refreshRate) {
+ return mWidth == width &&
+ mHeight == height &&
+ Float.floatToIntBits(mRefreshRate) == Float.floatToIntBits(refreshRate);
+ }
+
+ /**
+ * Returns {@code true} if this mode equals to the other mode in all parameters except
+ * the refresh rate.
+ *
+ * @hide
+ */
+ public boolean equalsExceptRefreshRate(@Nullable Display.Mode other) {
+ return mWidth == other.mWidth && mHeight == other.mHeight;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Mode)) {
+ return false;
+ }
+ Mode that = (Mode) other;
+ return mModeId == that.mModeId && matches(that.mWidth, that.mHeight, that.mRefreshRate)
+ && Arrays.equals(mAlternativeRefreshRates, that.mAlternativeRefreshRates);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 1;
+ hash = hash * 17 + mModeId;
+ hash = hash * 17 + mWidth;
+ hash = hash * 17 + mHeight;
+ hash = hash * 17 + Float.floatToIntBits(mRefreshRate);
+ hash = hash * 17 + Arrays.hashCode(mAlternativeRefreshRates);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder("{")
+ .append("id=").append(mModeId)
+ .append(", width=").append(mWidth)
+ .append(", height=").append(mHeight)
+ .append(", fps=").append(mRefreshRate)
+ .append(", alternativeRefreshRates=")
+ .append(Arrays.toString(mAlternativeRefreshRates))
+ .append("}")
+ .toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private Mode(Parcel in) {
+ this(in.readInt(), in.readInt(), in.readInt(), in.readFloat(), in.createFloatArray());
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int parcelableFlags) {
+ out.writeInt(mModeId);
+ out.writeInt(mWidth);
+ out.writeInt(mHeight);
+ out.writeFloat(mRefreshRate);
+ out.writeFloatArray(mAlternativeRefreshRates);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final @android.annotation.NonNull Parcelable.Creator<Mode> CREATOR
+ = new Parcelable.Creator<Mode>() {
+ @Override
+ public Mode createFromParcel(Parcel in) {
+ return new Mode(in);
+ }
+
+ @Override
+ public Mode[] newArray(int size) {
+ return new Mode[size];
+ }
+ };
+ }
+
+ /**
+ * Encapsulates the HDR capabilities of a given display.
+ * For example, what HDR types it supports and details about the desired luminance data.
+ * <p>You can get an instance for a given {@link Display} object with
+ * {@link Display#getHdrCapabilities getHdrCapabilities()}.
+ */
+ public static final class HdrCapabilities implements Parcelable {
+ /**
+ * Invalid luminance value.
+ */
+ public static final float INVALID_LUMINANCE = -1;
+ /**
+ * Dolby Vision high dynamic range (HDR) display.
+ */
+ public static final int HDR_TYPE_DOLBY_VISION = 1;
+ /**
+ * HDR10 display.
+ */
+ public static final int HDR_TYPE_HDR10 = 2;
+ /**
+ * Hybrid Log-Gamma HDR display.
+ */
+ public static final int HDR_TYPE_HLG = 3;
+
+ /**
+ * HDR10+ display.
+ */
+ public static final int HDR_TYPE_HDR10_PLUS = 4;
+
+ /** @hide */
+ public static final int[] HDR_TYPES = {
+ HDR_TYPE_DOLBY_VISION,
+ HDR_TYPE_HDR10,
+ HDR_TYPE_HLG,
+ HDR_TYPE_HDR10_PLUS
+ };
+
+ /** @hide */
+ @IntDef(prefix = { "HDR_TYPE_" }, value = {
+ HDR_TYPE_DOLBY_VISION,
+ HDR_TYPE_HDR10,
+ HDR_TYPE_HLG,
+ HDR_TYPE_HDR10_PLUS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HdrType {}
+
+ private @HdrType int[] mSupportedHdrTypes = new int[0];
+ private float mMaxLuminance = INVALID_LUMINANCE;
+ private float mMaxAverageLuminance = INVALID_LUMINANCE;
+ private float mMinLuminance = INVALID_LUMINANCE;
+
+ /**
+ * @hide
+ */
+ public HdrCapabilities() {
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public HdrCapabilities(int[] supportedHdrTypes, float maxLuminance,
+ float maxAverageLuminance, float minLuminance) {
+ mSupportedHdrTypes = supportedHdrTypes;
+ Arrays.sort(mSupportedHdrTypes);
+ mMaxLuminance = maxLuminance;
+ mMaxAverageLuminance = maxAverageLuminance;
+ mMinLuminance = minLuminance;
+ }
+
+ /**
+ * Gets the supported HDR types of this display.
+ * Returns empty array if HDR is not supported by the display.
+ */
+ public @HdrType int[] getSupportedHdrTypes() {
+ return mSupportedHdrTypes;
+ }
+ /**
+ * Returns the desired content max luminance data in cd/m2 for this display.
+ */
+ public float getDesiredMaxLuminance() {
+ return mMaxLuminance;
+ }
+ /**
+ * Returns the desired content max frame-average luminance data in cd/m2 for this display.
+ */
+ public float getDesiredMaxAverageLuminance() {
+ return mMaxAverageLuminance;
+ }
+ /**
+ * Returns the desired content min luminance data in cd/m2 for this display.
+ */
+ public float getDesiredMinLuminance() {
+ return mMinLuminance;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof HdrCapabilities)) {
+ return false;
+ }
+ HdrCapabilities that = (HdrCapabilities) other;
+
+ return Arrays.equals(mSupportedHdrTypes, that.mSupportedHdrTypes)
+ && mMaxLuminance == that.mMaxLuminance
+ && mMaxAverageLuminance == that.mMaxAverageLuminance
+ && mMinLuminance == that.mMinLuminance;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 23;
+ hash = hash * 17 + Arrays.hashCode(mSupportedHdrTypes);
+ hash = hash * 17 + Float.floatToIntBits(mMaxLuminance);
+ hash = hash * 17 + Float.floatToIntBits(mMaxAverageLuminance);
+ hash = hash * 17 + Float.floatToIntBits(mMinLuminance);
+ return hash;
+ }
+
+ public static final @android.annotation.NonNull Creator<HdrCapabilities> CREATOR = new Creator<HdrCapabilities>() {
+ @Override
+ public HdrCapabilities createFromParcel(Parcel source) {
+ return new HdrCapabilities(source);
+ }
+
+ @Override
+ public HdrCapabilities[] newArray(int size) {
+ return new HdrCapabilities[size];
+ }
+ };
+
+ private HdrCapabilities(Parcel source) {
+ readFromParcel(source);
+ }
+
+ /**
+ * @hide
+ */
+ public void readFromParcel(Parcel source) {
+ int types = source.readInt();
+ mSupportedHdrTypes = new int[types];
+ for (int i = 0; i < types; ++i) {
+ mSupportedHdrTypes[i] = source.readInt();
+ }
+ mMaxLuminance = source.readFloat();
+ mMaxAverageLuminance = source.readFloat();
+ mMinLuminance = source.readFloat();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSupportedHdrTypes.length);
+ for (int i = 0; i < mSupportedHdrTypes.length; ++i) {
+ dest.writeInt(mSupportedHdrTypes[i]);
+ }
+ dest.writeFloat(mMaxLuminance);
+ dest.writeFloat(mMaxAverageLuminance);
+ dest.writeFloat(mMinLuminance);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "HdrCapabilities{"
+ + "mSupportedHdrTypes=" + Arrays.toString(mSupportedHdrTypes)
+ + ", mMaxLuminance=" + mMaxLuminance
+ + ", mMaxAverageLuminance=" + mMaxAverageLuminance
+ + ", mMinLuminance=" + mMinLuminance + '}';
+ }
+ }
+}
diff --git a/android/view/DisplayAddress.java b/android/view/DisplayAddress.java
new file mode 100644
index 0000000..91a24c6
--- /dev/null
+++ b/android/view/DisplayAddress.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** Display identifier that is stable across reboots.
+ *
+ * @hide
+ */
+public abstract class DisplayAddress implements Parcelable {
+ /**
+ * Creates an address for a physical display given its stable ID.
+ *
+ * A physical display ID is stable if the display can be identified using EDID information.
+ *
+ * @param physicalDisplayId A physical display ID.
+ * @return The {@link Physical} address.
+ * @see SurfaceControl#getPhysicalDisplayIds
+ */
+ @NonNull
+ public static Physical fromPhysicalDisplayId(long physicalDisplayId) {
+ return new Physical(physicalDisplayId);
+ }
+
+ /**
+ * Creates an address for a physical display given its port and model.
+ *
+ * @param port A port in the range [0, 255].
+ * @param model A positive integer, or {@code null} if the model cannot be identified.
+ * @return The {@link Physical} address.
+ */
+ @NonNull
+ public static Physical fromPortAndModel(int port, Long model) {
+ return new Physical(port, model);
+ }
+
+ /**
+ * Creates an address for a network display given its MAC address.
+ *
+ * @param macAddress A MAC address in colon notation.
+ * @return The {@link Network} address.
+ */
+ @NonNull
+ public static Network fromMacAddress(String macAddress) {
+ return new Network(macAddress);
+ }
+
+ /**
+ * Address for a physically connected display.
+ *
+ * A {@link Physical} address is represented by a 64-bit identifier combining the port and model
+ * of a display. The port, located in the least significant byte, uniquely identifies a physical
+ * connector on the device for display output like eDP or HDMI. The model, located in the upper
+ * bits, uniquely identifies a display model across manufacturers by encoding EDID information.
+ * While the port is always stable, the model may not be available if EDID identification is not
+ * supported by the platform, in which case the address is not unique.
+ */
+ public static final class Physical extends DisplayAddress {
+ private static final long UNKNOWN_MODEL = 0;
+ private static final int MODEL_SHIFT = 8;
+
+ private final long mPhysicalDisplayId;
+
+ /**
+ * Stable display ID combining port and model.
+ *
+ * @return An ID in the range [0, 2^64) interpreted as signed.
+ * @see SurfaceControl#getPhysicalDisplayIds
+ */
+ public long getPhysicalDisplayId() {
+ return mPhysicalDisplayId;
+ }
+
+ /**
+ * Physical port to which the display is connected.
+ *
+ * @return A port in the range [0, 255].
+ */
+ public int getPort() {
+ return (int) (mPhysicalDisplayId & 0xFF);
+ }
+
+ /**
+ * Model identifier unique across manufacturers.
+ *
+ * @return A positive integer, or {@code null} if the model cannot be identified.
+ */
+ @Nullable
+ public Long getModel() {
+ final long model = mPhysicalDisplayId >>> MODEL_SHIFT;
+ return model == UNKNOWN_MODEL ? null : model;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ return other instanceof Physical
+ && mPhysicalDisplayId == ((Physical) other).mPhysicalDisplayId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("{")
+ .append("port=").append(getPort());
+
+ final Long model = getModel();
+ if (model != null) {
+ builder.append(", model=0x").append(Long.toHexString(model));
+ }
+
+ return builder.append("}").toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(mPhysicalDisplayId);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mPhysicalDisplayId);
+ }
+
+ private Physical(long physicalDisplayId) {
+ mPhysicalDisplayId = physicalDisplayId;
+ }
+
+ private Physical(int port, Long model) {
+ if (port < 0 || port > 255) {
+ throw new IllegalArgumentException("The port should be in the interval [0, 255]");
+ }
+ mPhysicalDisplayId = Integer.toUnsignedLong(port)
+ | (model == null ? UNKNOWN_MODEL : (model << MODEL_SHIFT));
+ }
+
+ public static final @NonNull Parcelable.Creator<Physical> CREATOR =
+ new Parcelable.Creator<Physical>() {
+ @Override
+ public Physical createFromParcel(Parcel in) {
+ return new Physical(in.readLong());
+ }
+
+ @Override
+ public Physical[] newArray(int size) {
+ return new Physical[size];
+ }
+ };
+ }
+
+ /**
+ * Address for a network-connected display.
+ */
+ public static final class Network extends DisplayAddress {
+ private final String mMacAddress;
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ return other instanceof Network && mMacAddress.equals(((Network) other).mMacAddress);
+ }
+
+ @Override
+ public String toString() {
+ return mMacAddress;
+ }
+
+ @Override
+ public int hashCode() {
+ return mMacAddress.hashCode();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mMacAddress);
+ }
+
+ private Network(String macAddress) {
+ mMacAddress = macAddress;
+ }
+
+ public static final @NonNull Parcelable.Creator<Network> CREATOR =
+ new Parcelable.Creator<Network>() {
+ @Override
+ public Network createFromParcel(Parcel in) {
+ return new Network(in.readString());
+ }
+
+ @Override
+ public Network[] newArray(int size) {
+ return new Network[size];
+ }
+ };
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/DisplayAdjustments.java b/android/view/DisplayAdjustments.java
new file mode 100644
index 0000000..e307eff
--- /dev/null
+++ b/android/view/DisplayAdjustments.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.DisplayMetrics;
+
+import java.util.Objects;
+
+/** @hide */
+public class DisplayAdjustments {
+ public static final DisplayAdjustments DEFAULT_DISPLAY_ADJUSTMENTS = new DisplayAdjustments();
+
+ private volatile CompatibilityInfo mCompatInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
+ private final Configuration mConfiguration = new Configuration(Configuration.EMPTY);
+ private FixedRotationAdjustments mFixedRotationAdjustments;
+
+ @UnsupportedAppUsage
+ public DisplayAdjustments() {
+ }
+
+ public DisplayAdjustments(@Nullable Configuration configuration) {
+ if (configuration != null) {
+ mConfiguration.setTo(configuration);
+ }
+ }
+
+ public DisplayAdjustments(@NonNull DisplayAdjustments daj) {
+ setCompatibilityInfo(daj.mCompatInfo);
+ mConfiguration.setTo(daj.getConfiguration());
+ mFixedRotationAdjustments = daj.mFixedRotationAdjustments;
+ }
+
+ @UnsupportedAppUsage
+ public void setCompatibilityInfo(@Nullable CompatibilityInfo compatInfo) {
+ if (this == DEFAULT_DISPLAY_ADJUSTMENTS) {
+ throw new IllegalArgumentException(
+ "setCompatbilityInfo: Cannot modify DEFAULT_DISPLAY_ADJUSTMENTS");
+ }
+ if (compatInfo != null && (compatInfo.isScalingRequired()
+ || !compatInfo.supportsScreen())) {
+ mCompatInfo = compatInfo;
+ } else {
+ mCompatInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
+ }
+ }
+
+ public CompatibilityInfo getCompatibilityInfo() {
+ return mCompatInfo;
+ }
+
+ /**
+ * Updates the configuration for the DisplayAdjustments with new configuration.
+ * Default to EMPTY configuration if new configuration is {@code null}
+ * @param configuration new configuration
+ * @throws IllegalArgumentException if trying to modify DEFAULT_DISPLAY_ADJUSTMENTS
+ */
+ public void setConfiguration(@Nullable Configuration configuration) {
+ if (this == DEFAULT_DISPLAY_ADJUSTMENTS) {
+ throw new IllegalArgumentException(
+ "setConfiguration: Cannot modify DEFAULT_DISPLAY_ADJUSTMENTS");
+ }
+ mConfiguration.setTo(configuration != null ? configuration : Configuration.EMPTY);
+ }
+
+ @UnsupportedAppUsage
+ @NonNull
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ public void setFixedRotationAdjustments(FixedRotationAdjustments fixedRotationAdjustments) {
+ mFixedRotationAdjustments = fixedRotationAdjustments;
+ }
+
+ public FixedRotationAdjustments getFixedRotationAdjustments() {
+ return mFixedRotationAdjustments;
+ }
+
+ /** Returns {@code false} if the width and height of display should swap. */
+ private boolean noFlip(@Surface.Rotation int realRotation) {
+ final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments;
+ if (rotationAdjustments == null) {
+ return true;
+ }
+ // Check if the delta is rotated by 90 degrees.
+ return (realRotation - rotationAdjustments.mRotation + 4) % 2 == 0;
+ }
+
+ /** Adjusts the given size if possible. */
+ public void adjustSize(@NonNull Point size, @Surface.Rotation int realRotation) {
+ if (noFlip(realRotation)) {
+ return;
+ }
+ final int w = size.x;
+ size.x = size.y;
+ size.y = w;
+ }
+
+ /** Adjusts the given metrics if possible. */
+ public void adjustMetrics(@NonNull DisplayMetrics metrics, @Surface.Rotation int realRotation) {
+ if (noFlip(realRotation)) {
+ return;
+ }
+ int w = metrics.widthPixels;
+ metrics.widthPixels = metrics.heightPixels;
+ metrics.heightPixels = w;
+
+ w = metrics.noncompatWidthPixels;
+ metrics.noncompatWidthPixels = metrics.noncompatHeightPixels;
+ metrics.noncompatHeightPixels = w;
+ }
+
+ /** Adjusts global display metrics that is available to applications. */
+ public void adjustGlobalAppMetrics(@NonNull DisplayMetrics metrics) {
+ final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments;
+ if (rotationAdjustments == null) {
+ return;
+ }
+ metrics.noncompatWidthPixels = metrics.widthPixels = rotationAdjustments.mAppWidth;
+ metrics.noncompatHeightPixels = metrics.heightPixels = rotationAdjustments.mAppHeight;
+ }
+
+ /** Returns the adjusted cutout if available. Otherwise the original cutout is returned. */
+ @Nullable
+ public DisplayCutout getDisplayCutout(@Nullable DisplayCutout realCutout) {
+ final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments;
+ return rotationAdjustments != null && rotationAdjustments.mRotatedDisplayCutout != null
+ ? rotationAdjustments.mRotatedDisplayCutout
+ : realCutout;
+ }
+
+ /**
+ * Returns the adjusted {@link RoundedCorners} if available. Otherwise the original
+ * {@link RoundedCorners} is returned.
+ */
+ @Nullable
+ public RoundedCorners adjustRoundedCorner(@Nullable RoundedCorners realRoundedCorners,
+ @Surface.Rotation int realRotation, int displayWidth, int displayHeight) {
+ final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments;
+ if (realRoundedCorners == null || rotationAdjustments == null
+ || rotationAdjustments.mRotation == realRotation) {
+ return realRoundedCorners;
+ }
+
+ return realRoundedCorners.rotate(
+ rotationAdjustments.mRotation, displayWidth, displayHeight);
+ }
+
+ /** Returns the adjusted rotation if available. Otherwise the original rotation is returned. */
+ @Surface.Rotation
+ public int getRotation(@Surface.Rotation int realRotation) {
+ final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments;
+ return rotationAdjustments != null ? rotationAdjustments.mRotation : realRotation;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 17;
+ hash = hash * 31 + Objects.hashCode(mCompatInfo);
+ hash = hash * 31 + Objects.hashCode(mConfiguration);
+ hash = hash * 31 + Objects.hashCode(mFixedRotationAdjustments);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (!(o instanceof DisplayAdjustments)) {
+ return false;
+ }
+ DisplayAdjustments daj = (DisplayAdjustments)o;
+ return Objects.equals(daj.mCompatInfo, mCompatInfo)
+ && Objects.equals(daj.mConfiguration, mConfiguration)
+ && Objects.equals(daj.mFixedRotationAdjustments, mFixedRotationAdjustments);
+ }
+
+ /**
+ * An application can be launched in different rotation than the real display. This class
+ * provides the information to adjust the values returned by {@link Display}.
+ * @hide
+ */
+ public static class FixedRotationAdjustments implements Parcelable {
+ /** The application-based rotation. */
+ @Surface.Rotation
+ final int mRotation;
+
+ /**
+ * The rotated {@link DisplayInfo#appWidth}. The value cannot be simply swapped according
+ * to rotation because it minus the region of screen decorations.
+ */
+ final int mAppWidth;
+
+ /** The rotated {@link DisplayInfo#appHeight}. */
+ final int mAppHeight;
+
+ /** Non-null if the device has cutout. */
+ @Nullable
+ final DisplayCutout mRotatedDisplayCutout;
+
+ public FixedRotationAdjustments(@Surface.Rotation int rotation, int appWidth, int appHeight,
+ DisplayCutout cutout) {
+ mRotation = rotation;
+ mAppWidth = appWidth;
+ mAppHeight = appHeight;
+ mRotatedDisplayCutout = cutout;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 17;
+ hash = hash * 31 + mRotation;
+ hash = hash * 31 + mAppWidth;
+ hash = hash * 31 + mAppHeight;
+ hash = hash * 31 + Objects.hashCode(mRotatedDisplayCutout);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (!(o instanceof FixedRotationAdjustments)) {
+ return false;
+ }
+ final FixedRotationAdjustments other = (FixedRotationAdjustments) o;
+ return mRotation == other.mRotation
+ && mAppWidth == other.mAppWidth && mAppHeight == other.mAppHeight
+ && Objects.equals(mRotatedDisplayCutout, other.mRotatedDisplayCutout);
+ }
+
+ @Override
+ public String toString() {
+ return "FixedRotationAdjustments{rotation=" + Surface.rotationToString(mRotation)
+ + " appWidth=" + mAppWidth + " appHeight=" + mAppHeight
+ + " cutout=" + mRotatedDisplayCutout + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRotation);
+ dest.writeInt(mAppWidth);
+ dest.writeInt(mAppHeight);
+ dest.writeTypedObject(
+ new DisplayCutout.ParcelableWrapper(mRotatedDisplayCutout), flags);
+ }
+
+ private FixedRotationAdjustments(Parcel in) {
+ mRotation = in.readInt();
+ mAppWidth = in.readInt();
+ mAppHeight = in.readInt();
+ final DisplayCutout.ParcelableWrapper cutoutWrapper =
+ in.readTypedObject(DisplayCutout.ParcelableWrapper.CREATOR);
+ mRotatedDisplayCutout = cutoutWrapper != null ? cutoutWrapper.get() : null;
+ }
+
+ public static final Creator<FixedRotationAdjustments> CREATOR =
+ new Creator<FixedRotationAdjustments>() {
+ public FixedRotationAdjustments createFromParcel(Parcel in) {
+ return new FixedRotationAdjustments(in);
+ }
+
+ public FixedRotationAdjustments[] newArray(int size) {
+ return new FixedRotationAdjustments[size];
+ }
+ };
+ }
+}
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java
new file mode 100644
index 0000000..e1a4402
--- /dev/null
+++ b/android/view/DisplayCutout.java
@@ -0,0 +1,1142 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
+import static android.view.DisplayCutoutProto.BOUND_BOTTOM;
+import static android.view.DisplayCutoutProto.BOUND_LEFT;
+import static android.view.DisplayCutoutProto.BOUND_RIGHT;
+import static android.view.DisplayCutoutProto.BOUND_TOP;
+import static android.view.DisplayCutoutProto.INSETS;
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.RotationUtils;
+import android.util.proto.ProtoOutputStream;
+import android.view.Surface.Rotation;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents the area of the display that is not functional for displaying content.
+ *
+ * <p>{@code DisplayCutout} is immutable.
+ */
+public final class DisplayCutout {
+
+ private static final String TAG = "DisplayCutout";
+
+ /**
+ * Category for overlays that allow emulating a display cutout on devices that don't have
+ * one.
+ *
+ * @see android.content.om.IOverlayManager
+ * @hide
+ */
+ public static final String EMULATION_OVERLAY_CATEGORY =
+ "com.android.internal.display_cutout_emulation";
+
+ private static final Rect ZERO_RECT = new Rect();
+ private static final CutoutPathParserInfo EMPTY_PARSER_INFO = new CutoutPathParserInfo(
+ 0 /* displayWidth */, 0 /* displayHeight */, 0f /* density */, "" /* cutoutSpec */,
+ 0 /* rotation */, 0f /* scale */);
+
+ /**
+ * An instance where {@link #isEmpty()} returns {@code true}.
+ *
+ * @hide
+ */
+ public static final DisplayCutout NO_CUTOUT = new DisplayCutout(
+ ZERO_RECT, Insets.NONE, ZERO_RECT, ZERO_RECT, ZERO_RECT, ZERO_RECT, EMPTY_PARSER_INFO,
+ false /* copyArguments */);
+
+
+ private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null);
+ private static final Object CACHE_LOCK = new Object();
+
+ @GuardedBy("CACHE_LOCK")
+ private static String sCachedSpec;
+ @GuardedBy("CACHE_LOCK")
+ private static int sCachedDisplayWidth;
+ @GuardedBy("CACHE_LOCK")
+ private static int sCachedDisplayHeight;
+ @GuardedBy("CACHE_LOCK")
+ private static float sCachedDensity;
+ @GuardedBy("CACHE_LOCK")
+ private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR;
+ @GuardedBy("CACHE_LOCK")
+ private static Insets sCachedWaterfallInsets;
+
+ @GuardedBy("CACHE_LOCK")
+ private static CutoutPathParserInfo sCachedCutoutPathParserInfo;
+ @GuardedBy("CACHE_LOCK")
+ private static Path sCachedCutoutPath;
+
+ private final Rect mSafeInsets;
+ @NonNull
+ private final Insets mWaterfallInsets;
+
+ /**
+ * The bound is at the left of the screen.
+ * @hide
+ */
+ public static final int BOUNDS_POSITION_LEFT = 0;
+
+ /**
+ * The bound is at the top of the screen.
+ * @hide
+ */
+ public static final int BOUNDS_POSITION_TOP = 1;
+
+ /**
+ * The bound is at the right of the screen.
+ * @hide
+ */
+ public static final int BOUNDS_POSITION_RIGHT = 2;
+
+ /**
+ * The bound is at the bottom of the screen.
+ * @hide
+ */
+ public static final int BOUNDS_POSITION_BOTTOM = 3;
+
+ /**
+ * The number of possible positions at which bounds can be located.
+ * @hide
+ */
+ public static final int BOUNDS_POSITION_LENGTH = 4;
+
+ /** @hide */
+ @IntDef(prefix = { "BOUNDS_POSITION_" }, value = {
+ BOUNDS_POSITION_LEFT,
+ BOUNDS_POSITION_TOP,
+ BOUNDS_POSITION_RIGHT,
+ BOUNDS_POSITION_BOTTOM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BoundsPosition {}
+
+ private static class Bounds {
+ private final Rect[] mRects;
+
+ private Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments) {
+ mRects = new Rect[BOUNDS_POSITION_LENGTH];
+ mRects[BOUNDS_POSITION_LEFT] = getCopyOrRef(left, copyArguments);
+ mRects[BOUNDS_POSITION_TOP] = getCopyOrRef(top, copyArguments);
+ mRects[BOUNDS_POSITION_RIGHT] = getCopyOrRef(right, copyArguments);
+ mRects[BOUNDS_POSITION_BOTTOM] = getCopyOrRef(bottom, copyArguments);
+
+ }
+
+ private Bounds(Rect[] rects, boolean copyArguments) {
+ if (rects.length != BOUNDS_POSITION_LENGTH) {
+ throw new IllegalArgumentException(
+ "rects must have exactly 4 elements: rects=" + Arrays.toString(rects));
+ }
+ if (copyArguments) {
+ mRects = new Rect[BOUNDS_POSITION_LENGTH];
+ for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) {
+ mRects[i] = new Rect(rects[i]);
+ }
+ } else {
+ for (Rect rect : rects) {
+ if (rect == null) {
+ throw new IllegalArgumentException(
+ "rects must have non-null elements: rects="
+ + Arrays.toString(rects));
+ }
+ }
+ mRects = rects;
+ }
+ }
+
+ private boolean isEmpty() {
+ for (Rect rect : mRects) {
+ if (!rect.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Rect getRect(@BoundsPosition int pos) {
+ return new Rect(mRects[pos]);
+ }
+
+ private Rect[] getRects() {
+ Rect[] rects = new Rect[BOUNDS_POSITION_LENGTH];
+ for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) {
+ rects[i] = new Rect(mRects[i]);
+ }
+ return rects;
+ }
+
+ private void scale(float scale) {
+ for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) {
+ mRects[i].scale(scale);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (Rect rect : mRects) {
+ result = result * 48271 + rect.hashCode();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof Bounds) {
+ Bounds b = (Bounds) o;
+ return Arrays.deepEquals(mRects, b.mRects);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "Bounds=" + Arrays.toString(mRects);
+ }
+
+ }
+
+ private final Bounds mBounds;
+
+ /**
+ * Stores all the needed info to create the cutout paths.
+ *
+ * @hide
+ */
+ public static class CutoutPathParserInfo {
+ private final int mDisplayWidth;
+ private final int mDisplayHeight;
+ private final float mDensity;
+ private final String mCutoutSpec;
+ private final @Rotation int mRotation;
+ private final float mScale;
+
+ public CutoutPathParserInfo(int displayWidth, int displayHeight, float density,
+ String cutoutSpec, @Rotation int rotation, float scale) {
+ mDisplayWidth = displayWidth;
+ mDisplayHeight = displayHeight;
+ mDensity = density;
+ mCutoutSpec = cutoutSpec == null ? "" : cutoutSpec;
+ mRotation = rotation;
+ mScale = scale;
+ }
+
+ public CutoutPathParserInfo(CutoutPathParserInfo cutoutPathParserInfo) {
+ mDisplayWidth = cutoutPathParserInfo.mDisplayWidth;
+ mDisplayHeight = cutoutPathParserInfo.mDisplayHeight;
+ mDensity = cutoutPathParserInfo.mDensity;
+ mCutoutSpec = cutoutPathParserInfo.mCutoutSpec;
+ mRotation = cutoutPathParserInfo.mRotation;
+ mScale = cutoutPathParserInfo.mScale;
+ }
+
+ public int getDisplayWidth() {
+ return mDisplayWidth;
+ }
+
+ public int getDisplayHeight() {
+ return mDisplayHeight;
+ }
+
+ public float getDensity() {
+ return mDensity;
+ }
+
+ public @NonNull String getCutoutSpec() {
+ return mCutoutSpec;
+ }
+
+ public int getRotation() {
+ return mRotation;
+ }
+
+ public float getScale() {
+ return mScale;
+ }
+
+ private boolean hasCutout() {
+ return !mCutoutSpec.isEmpty();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ result = result * 48271 + Integer.hashCode(mDisplayWidth);
+ result = result * 48271 + Integer.hashCode(mDisplayHeight);
+ result = result * 48271 + Float.hashCode(mDensity);
+ result = result * 48271 + mCutoutSpec.hashCode();
+ result = result * 48271 + Integer.hashCode(mRotation);
+ result = result * 48271 + Float.hashCode(mScale);
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof CutoutPathParserInfo) {
+ CutoutPathParserInfo c = (CutoutPathParserInfo) o;
+ return mDisplayWidth == c.mDisplayWidth && mDisplayHeight == c.mDisplayHeight
+ && mDensity == c.mDensity && mCutoutSpec.equals(c.mCutoutSpec)
+ && mRotation == c.mRotation && mScale == c.mScale;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "CutoutPathParserInfo{displayWidth=" + mDisplayWidth
+ + " displayHeight=" + mDisplayHeight
+ + " density={" + mDensity + "}"
+ + " cutoutSpec={" + mCutoutSpec + "}"
+ + " rotation={" + mRotation + "}"
+ + " scale={" + mScale + "}"
+ + "}";
+ }
+ }
+
+ private final @NonNull CutoutPathParserInfo mCutoutPathParserInfo;
+
+ /**
+ * Creates a DisplayCutout instance.
+ *
+ * <p>Note that this is only useful for tests. For production code, developers should always
+ * use a {@link DisplayCutout} obtained from the system.</p>
+ *
+ * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+ * {@link #getSafeInsetTop()} etc.
+ * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed,
+ * it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed,
+ * it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundRight the right bounding rect of the display cutout in pixels. If null is
+ * passed, it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is
+ * passed, it's treated as an empty rectangle (0,0)-(0,0).
+ */
+ // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
+ public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
+ @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom) {
+ this(safeInsets.toRect(), Insets.NONE, boundLeft, boundTop, boundRight, boundBottom, null,
+ true);
+ }
+
+ /**
+ * Creates a DisplayCutout instance.
+ *
+ * <p>Note that this is only useful for tests. For production code, developers should always
+ * use a {@link DisplayCutout} obtained from the system.</p>
+ *
+ * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+ * {@link #getSafeInsetTop()} etc.
+ * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed,
+ * it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed,
+ * it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundRight the right bounding rect of the display cutout in pixels. If null is
+ * passed, it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is
+ * passed, it's treated as an empty rectangle (0,0)-(0,0).
+ * @param waterfallInsets the insets for the curved areas in waterfall display.
+ */
+ public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
+ @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
+ @NonNull Insets waterfallInsets) {
+ this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom,
+ null, true);
+ }
+
+ /**
+ * Creates a DisplayCutout instance.
+ *
+ * <p>Note that this is only useful for tests. For production code, developers should always
+ * use a {@link DisplayCutout} obtained from the system.</p>
+ *
+ * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+ * {@link #getSafeInsetTop()} etc.
+ * @param boundingRects the bounding rects of the display cutouts as returned by
+ * {@link #getBoundingRects()} ()}.
+ * @deprecated Use {@link DisplayCutout#DisplayCutout(Insets, Rect, Rect, Rect, Rect)} instead.
+ */
+ // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
+ @Deprecated
+ public DisplayCutout(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) {
+ this(safeInsets, Insets.NONE, extractBoundsFromList(safeInsets, boundingRects), null,
+ true /* copyArguments */);
+ }
+
+ /**
+ * Creates a DisplayCutout instance.
+ *
+ * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+ * {@link #getSafeInsetTop()} etc.
+ * @param waterfallInsets the insets for the curved areas in waterfall display.
+ * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed,
+ * it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed,
+ * it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundRight the right bounding rect of the display cutout in pixels. If null is
+ * passed, it's treated as an empty rectangle (0,0)-(0,0).
+ * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is
+ * passed, it's treated as an empty rectangle (0,0)-(0,0).
+ * @param info the cutout path parser info.
+ * @param copyArguments if true, create a copy of the arguments. If false, the passed arguments
+ * are not copied and MUST remain unchanged forever.
+ */
+ private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop,
+ Rect boundRight, Rect boundBottom, CutoutPathParserInfo info,
+ boolean copyArguments) {
+ mSafeInsets = getCopyOrRef(safeInsets, copyArguments);
+ mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets;
+ mBounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments);
+ mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info;
+ }
+
+ private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds,
+ CutoutPathParserInfo info, boolean copyArguments) {
+ mSafeInsets = getCopyOrRef(safeInsets, copyArguments);
+ mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets;
+ mBounds = new Bounds(bounds, copyArguments);
+ mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info;
+ }
+
+ private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds,
+ CutoutPathParserInfo info) {
+ mSafeInsets = safeInsets;
+ mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets;
+ mBounds = bounds;
+ mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info;
+ }
+
+ private static Rect getCopyOrRef(Rect r, boolean copyArguments) {
+ if (r == null) {
+ return ZERO_RECT;
+ } else if (copyArguments) {
+ return new Rect(r);
+ } else {
+ return r;
+ }
+ }
+
+ /**
+ * Returns the insets representing the curved areas of a waterfall display.
+ *
+ * A waterfall display has curved areas along the edges of the screen. Apps should be careful
+ * when showing UI and handling touch input in those insets because the curve may impair
+ * legibility and can frequently lead to unintended touch inputs.
+ *
+ * @return the insets for the curved areas of a waterfall display in pixels or {@code
+ * Insets.NONE} if there are no curved areas or they don't overlap with the window.
+ */
+ public @NonNull Insets getWaterfallInsets() {
+ return mWaterfallInsets;
+ }
+
+
+ /**
+ * Find the position of the bounding rect, and create an array of Rect whose index represents
+ * the position (= BoundsPosition).
+ *
+ * @hide
+ */
+ public static Rect[] extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects) {
+ Rect[] sortedBounds = new Rect[BOUNDS_POSITION_LENGTH];
+ for (int i = 0; i < sortedBounds.length; ++i) {
+ sortedBounds[i] = ZERO_RECT;
+ }
+ if (safeInsets != null && boundingRects != null) {
+ // There is at most one non-functional area per short edge of the device, but none
+ // on the long edges, so either a) safeInsets.top and safeInsets.bottom is 0, or
+ // b) safeInsets.left and safeInset.right is 0.
+ final boolean topBottomInset = safeInsets.top > 0 || safeInsets.bottom > 0;
+ for (Rect bound : boundingRects) {
+ if (topBottomInset) {
+ if (bound.top == 0) {
+ sortedBounds[BOUNDS_POSITION_TOP] = bound;
+ } else {
+ sortedBounds[BOUNDS_POSITION_BOTTOM] = bound;
+ }
+ } else {
+ if (bound.left == 0) {
+ sortedBounds[BOUNDS_POSITION_LEFT] = bound;
+ } else {
+ sortedBounds[BOUNDS_POSITION_RIGHT] = bound;
+ }
+ }
+ }
+ }
+ return sortedBounds;
+ }
+
+ /**
+ * Returns true if there is no cutout, i.e. the bounds are empty.
+ *
+ * @hide
+ */
+ public boolean isBoundsEmpty() {
+ return mBounds.isEmpty();
+ }
+
+ /**
+ * Returns true if the safe insets are empty (and therefore the current view does not
+ * overlap with the cutout or cutout area).
+ *
+ * @hide
+ */
+ public boolean isEmpty() {
+ return mSafeInsets.equals(ZERO_RECT);
+ }
+
+ /**
+ * Returns the inset from the top which avoids the display cutout in pixels.
+ *
+ * @see WindowInsets.Type#displayCutout()
+ */
+ public int getSafeInsetTop() {
+ return mSafeInsets.top;
+ }
+
+ /**
+ * Returns the inset from the bottom which avoids the display cutout in pixels.
+ *
+ * @see WindowInsets.Type#displayCutout()
+ */
+ public int getSafeInsetBottom() {
+ return mSafeInsets.bottom;
+ }
+
+ /**
+ * Returns the inset from the left which avoids the display cutout in pixels.
+ *
+ * @see WindowInsets.Type#displayCutout()
+ */
+ public int getSafeInsetLeft() {
+ return mSafeInsets.left;
+ }
+
+ /**
+ * Returns the inset from the right which avoids the display cutout in pixels.
+ *
+ * @see WindowInsets.Type#displayCutout()
+ */
+ public int getSafeInsetRight() {
+ return mSafeInsets.right;
+ }
+
+ /**
+ * Returns the safe insets in a rect in pixel units.
+ *
+ * @return a rect which is set to the safe insets.
+ * @hide
+ */
+ public Rect getSafeInsets() {
+ return new Rect(mSafeInsets);
+ }
+
+ /**
+ * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional
+ * area on the display.
+ *
+ * There will be at most one non-functional area per short edge of the device, and none on
+ * the long edges.
+ *
+ * @return a list of bounding {@code Rect}s, one for each display cutout area. No empty Rect is
+ * returned.
+ */
+ @NonNull
+ public List<Rect> getBoundingRects() {
+ List<Rect> result = new ArrayList<>();
+ for (Rect bound : getBoundingRectsAll()) {
+ if (!bound.isEmpty()) {
+ result.add(new Rect(bound));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns an array of {@code Rect}s, each of which is the bounding rectangle for a non-
+ * functional area on the display. Ordinal value of BoundPosition is used as an index of
+ * the array.
+ *
+ * There will be at most one non-functional area per short edge of the device, and none on
+ * the long edges.
+ *
+ * @return an array of bounding {@code Rect}s, one for each display cutout area. This might
+ * contain ZERO_RECT, which means there is no cutout area at the position.
+ *
+ * @hide
+ */
+ public Rect[] getBoundingRectsAll() {
+ return mBounds.getRects();
+ }
+
+ /**
+ * Returns a bounding rectangle for a non-functional area on the display which is located on
+ * the left of the screen.
+ *
+ * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle
+ * is returned.
+ */
+ public @NonNull Rect getBoundingRectLeft() {
+ return mBounds.getRect(BOUNDS_POSITION_LEFT);
+ }
+
+ /**
+ * Returns a bounding rectangle for a non-functional area on the display which is located on
+ * the top of the screen.
+ *
+ * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle
+ * is returned.
+ */
+ public @NonNull Rect getBoundingRectTop() {
+ return mBounds.getRect(BOUNDS_POSITION_TOP);
+ }
+
+ /**
+ * Returns a bounding rectangle for a non-functional area on the display which is located on
+ * the right of the screen.
+ *
+ * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle
+ * is returned.
+ */
+ public @NonNull Rect getBoundingRectRight() {
+ return mBounds.getRect(BOUNDS_POSITION_RIGHT);
+ }
+
+ /**
+ * Returns a bounding rectangle for a non-functional area on the display which is located on
+ * the bottom of the screen.
+ *
+ * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle
+ * is returned.
+ */
+ public @NonNull Rect getBoundingRectBottom() {
+ return mBounds.getRect(BOUNDS_POSITION_BOTTOM);
+ }
+
+ /**
+ * Returns a {@link Path} that contains the cutout paths of all sides on the display.
+ *
+ * To get a cutout path for one specific side, apps can intersect the {@link Path} with the
+ * {@link Rect} obtained from {@link #getBoundingRectLeft()}, {@link #getBoundingRectTop()},
+ * {@link #getBoundingRectRight()} or {@link #getBoundingRectBottom()}.
+ *
+ * @return a {@link Path} contains all the cutout paths based on display coordinate. Returns
+ * null if there is no cutout on the display.
+ */
+ public @Nullable Path getCutoutPath() {
+ if (!mCutoutPathParserInfo.hasCutout()) {
+ return null;
+ }
+ synchronized (CACHE_LOCK) {
+ if (mCutoutPathParserInfo.equals(sCachedCutoutPathParserInfo)) {
+ return sCachedCutoutPath;
+ }
+ }
+ final CutoutSpecification cutoutSpec = new CutoutSpecification.Parser(
+ mCutoutPathParserInfo.getDensity(), mCutoutPathParserInfo.getDisplayWidth(),
+ mCutoutPathParserInfo.getDisplayHeight())
+ .parse(mCutoutPathParserInfo.getCutoutSpec());
+
+ final Path cutoutPath = cutoutSpec.getPath();
+ if (cutoutPath == null || cutoutPath.isEmpty()) {
+ return null;
+ }
+ final Matrix matrix = new Matrix();
+ if (mCutoutPathParserInfo.getRotation() != ROTATION_0) {
+ RotationUtils.transformPhysicalToLogicalCoordinates(
+ mCutoutPathParserInfo.getRotation(),
+ mCutoutPathParserInfo.getDisplayWidth(),
+ mCutoutPathParserInfo.getDisplayHeight(),
+ matrix
+ );
+ }
+ matrix.postScale(mCutoutPathParserInfo.getScale(), mCutoutPathParserInfo.getScale());
+ cutoutPath.transform(matrix);
+
+ synchronized (CACHE_LOCK) {
+ sCachedCutoutPathParserInfo = new CutoutPathParserInfo(mCutoutPathParserInfo);
+ sCachedCutoutPath = cutoutPath;
+ }
+ return cutoutPath;
+ }
+
+ /**
+ * @return the {@link CutoutPathParserInfo};
+ *
+ * @hide
+ */
+ public CutoutPathParserInfo getCutoutPathParserInfo() {
+ return mCutoutPathParserInfo;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ result = 48271 * result + mSafeInsets.hashCode();
+ result = 48271 * result + mBounds.hashCode();
+ result = 48271 * result + mWaterfallInsets.hashCode();
+ result = 48271 * result + mCutoutPathParserInfo.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof DisplayCutout) {
+ DisplayCutout c = (DisplayCutout) o;
+ return mSafeInsets.equals(c.mSafeInsets) && mBounds.equals(c.mBounds)
+ && mWaterfallInsets.equals(c.mWaterfallInsets)
+ && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayCutout{insets=" + mSafeInsets
+ + " waterfall=" + mWaterfallInsets
+ + " boundingRect={" + mBounds + "}"
+ + " cutoutPathParserInfo={" + mCutoutPathParserInfo + "}"
+ + "}";
+ }
+
+ /**
+ * @hide
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ mSafeInsets.dumpDebug(proto, INSETS);
+ mBounds.getRect(BOUNDS_POSITION_LEFT).dumpDebug(proto, BOUND_LEFT);
+ mBounds.getRect(BOUNDS_POSITION_TOP).dumpDebug(proto, BOUND_TOP);
+ mBounds.getRect(BOUNDS_POSITION_RIGHT).dumpDebug(proto, BOUND_RIGHT);
+ mBounds.getRect(BOUNDS_POSITION_BOTTOM).dumpDebug(proto, BOUND_BOTTOM);
+ mWaterfallInsets.toRect().dumpDebug(proto, INSETS);
+ proto.end(token);
+ }
+
+ /**
+ * Insets the reference frame of the cutout in the given directions.
+ *
+ * @return a copy of this instance which has been inset
+ * @hide
+ */
+ public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) {
+ if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0
+ || (isBoundsEmpty() && mWaterfallInsets.equals(Insets.NONE))) {
+ return this;
+ }
+
+ Rect safeInsets = insetInsets(insetLeft, insetTop, insetRight, insetBottom,
+ new Rect(mSafeInsets));
+
+ // If we are not cutting off part of the cutout by insetting it on bottom/right, and we also
+ // don't move it around, we can avoid the allocation and copy of the instance.
+ if (insetLeft == 0 && insetTop == 0 && mSafeInsets.equals(safeInsets)) {
+ return this;
+ }
+
+ Rect waterfallInsets = insetInsets(insetLeft, insetTop, insetRight, insetBottom,
+ mWaterfallInsets.toRect());
+
+ Rect[] bounds = mBounds.getRects();
+ for (int i = 0; i < bounds.length; ++i) {
+ if (!bounds[i].equals(ZERO_RECT)) {
+ bounds[i].offset(-insetLeft, -insetTop);
+ }
+ }
+
+ return new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds,
+ mCutoutPathParserInfo, false /* copyArguments */);
+ }
+
+ private Rect insetInsets(int insetLeft, int insetTop, int insetRight, int insetBottom,
+ Rect insets) {
+ // Note: it's not really well defined what happens when the inset is negative, because we
+ // don't know if the safe inset needs to expand in general.
+ if (insetTop > 0 || insets.top > 0) {
+ insets.top = atLeastZero(insets.top - insetTop);
+ }
+ if (insetBottom > 0 || insets.bottom > 0) {
+ insets.bottom = atLeastZero(insets.bottom - insetBottom);
+ }
+ if (insetLeft > 0 || insets.left > 0) {
+ insets.left = atLeastZero(insets.left - insetLeft);
+ }
+ if (insetRight > 0 || insets.right > 0) {
+ insets.right = atLeastZero(insets.right - insetRight);
+ }
+ return insets;
+ }
+
+ /**
+ * Returns a copy of this instance with the safe insets replaced with the parameter.
+ *
+ * @param safeInsets the new safe insets in pixels
+ * @return a copy of this instance with the safe insets replaced with the argument.
+ *
+ * @hide
+ */
+ public DisplayCutout replaceSafeInsets(Rect safeInsets) {
+ return new DisplayCutout(new Rect(safeInsets), mWaterfallInsets, mBounds,
+ mCutoutPathParserInfo);
+ }
+
+ private static int atLeastZero(int value) {
+ return value < 0 ? 0 : value;
+ }
+
+
+ /**
+ * Creates an instance from a bounding rect.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static DisplayCutout fromBoundingRect(
+ int left, int top, int right, int bottom, @BoundsPosition int pos) {
+ Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH];
+ for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) {
+ bounds[i] = (pos == i) ? new Rect(left, top, right, bottom) : new Rect();
+ }
+ return new DisplayCutout(ZERO_RECT, Insets.NONE, bounds, null, false /* copyArguments */);
+ }
+
+ /**
+ * Creates an instance from bounds, waterfall insets and CutoutPathParserInfo.
+ *
+ * @hide
+ */
+ public static DisplayCutout constructDisplayCutout(Rect[] bounds, Insets waterfallInsets,
+ CutoutPathParserInfo info) {
+ return new DisplayCutout(ZERO_RECT, waterfallInsets, bounds, info,
+ false /* copyArguments */);
+ }
+
+ /**
+ * Creates an instance from a bounding {@link Path}.
+ *
+ * @hide
+ */
+ public static DisplayCutout fromBounds(Rect[] bounds) {
+ return new DisplayCutout(ZERO_RECT, Insets.NONE, bounds, null /* cutoutPathParserInfo */,
+ false /* copyArguments */);
+ }
+
+ /**
+ * Creates the display cutout according to
+ * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest
+ * rectangle-base approximation of the cutout.
+ *
+ * @hide
+ */
+ public static DisplayCutout fromResourcesRectApproximation(Resources res, int displayWidth,
+ int displayHeight) {
+ return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
+ res.getString(R.string.config_mainBuiltInDisplayCutoutRectApproximation),
+ displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT,
+ loadWaterfallInset(res)).second;
+ }
+
+ /**
+ * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+ *
+ * @hide
+ */
+ public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) {
+ return pathAndDisplayCutoutFromSpec(
+ res.getString(R.string.config_mainBuiltInDisplayCutout), null,
+ displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT,
+ loadWaterfallInset(res)).first;
+ }
+
+ /**
+ * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static DisplayCutout fromSpec(String pathSpec, int displayWidth,
+ int displayHeight, float density, Insets waterfallInsets) {
+ return pathAndDisplayCutoutFromSpec(
+ pathSpec, null, displayWidth, displayHeight, density, waterfallInsets)
+ .second;
+ }
+
+ /**
+ * Gets the cutout path and the corresponding DisplayCutout instance from the spec string.
+ *
+ * @param pathSpec the spec string read from config_mainBuiltInDisplayCutout.
+ * @param rectSpec the spec string read from config_mainBuiltInDisplayCutoutRectApproximation.
+ * @param displayWidth the display width.
+ * @param displayHeight the display height.
+ * @param density the display density.
+ * @param waterfallInsets the waterfall insets of the display.
+ * @return a Pair contains the cutout path and the corresponding DisplayCutout instance.
+ */
+ private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(
+ String pathSpec, String rectSpec, int displayWidth, int displayHeight, float density,
+ Insets waterfallInsets) {
+ // Always use the rect approximation spec to create the cutout if it's not null because
+ // transforming and sending a Region constructed from a path is very costly.
+ String spec = rectSpec != null ? rectSpec : pathSpec;
+ if (TextUtils.isEmpty(spec) && waterfallInsets.equals(Insets.NONE)) {
+ return NULL_PAIR;
+ }
+
+ synchronized (CACHE_LOCK) {
+ if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth
+ && sCachedDisplayHeight == displayHeight
+ && sCachedDensity == density
+ && waterfallInsets.equals(sCachedWaterfallInsets)) {
+ return sCachedCutout;
+ }
+ }
+
+ spec = spec.trim();
+
+ CutoutSpecification cutoutSpec = new CutoutSpecification.Parser(density,
+ displayWidth, displayHeight).parse(spec);
+ Rect safeInset = cutoutSpec.getSafeInset();
+ final Rect boundLeft = cutoutSpec.getLeftBound();
+ final Rect boundTop = cutoutSpec.getTopBound();
+ final Rect boundRight = cutoutSpec.getRightBound();
+ final Rect boundBottom = cutoutSpec.getBottomBound();
+
+
+ if (!waterfallInsets.equals(Insets.NONE)) {
+ safeInset.set(
+ Math.max(waterfallInsets.left, safeInset.left),
+ Math.max(waterfallInsets.top, safeInset.top),
+ Math.max(waterfallInsets.right, safeInset.right),
+ Math.max(waterfallInsets.bottom, safeInset.bottom));
+ }
+
+ final CutoutPathParserInfo cutoutPathParserInfo = new CutoutPathParserInfo(displayWidth,
+ displayHeight, density, pathSpec.trim(), ROTATION_0, 1f /* scale */);
+
+ final DisplayCutout cutout = new DisplayCutout(
+ safeInset, waterfallInsets, boundLeft, boundTop, boundRight, boundBottom,
+ cutoutPathParserInfo , false /* copyArguments */);
+ final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout);
+ synchronized (CACHE_LOCK) {
+ sCachedSpec = spec;
+ sCachedDisplayWidth = displayWidth;
+ sCachedDisplayHeight = displayHeight;
+ sCachedDensity = density;
+ sCachedCutout = result;
+ sCachedWaterfallInsets = waterfallInsets;
+ }
+ return result;
+ }
+
+ private static Insets loadWaterfallInset(Resources res) {
+ return Insets.of(
+ res.getDimensionPixelSize(R.dimen.waterfall_display_left_edge_size),
+ res.getDimensionPixelSize(R.dimen.waterfall_display_top_edge_size),
+ res.getDimensionPixelSize(R.dimen.waterfall_display_right_edge_size),
+ res.getDimensionPixelSize(R.dimen.waterfall_display_bottom_edge_size));
+ }
+
+ /**
+ * Helper class for passing {@link DisplayCutout} through binder.
+ *
+ * Needed, because {@code readFromParcel} cannot be used with immutable classes.
+ *
+ * @hide
+ */
+ public static final class ParcelableWrapper implements Parcelable {
+
+ private DisplayCutout mInner;
+
+ public ParcelableWrapper() {
+ this(NO_CUTOUT);
+ }
+
+ public ParcelableWrapper(DisplayCutout cutout) {
+ mInner = cutout;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ writeCutoutToParcel(mInner, out, flags);
+ }
+
+ /**
+ * Writes a DisplayCutout to a {@link Parcel}.
+ *
+ * @see #readCutoutFromParcel(Parcel)
+ */
+ public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) {
+ if (cutout == null) {
+ out.writeInt(-1);
+ } else if (cutout == NO_CUTOUT) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(1);
+ out.writeTypedObject(cutout.mSafeInsets, flags);
+ out.writeTypedArray(cutout.mBounds.getRects(), flags);
+ out.writeTypedObject(cutout.mWaterfallInsets, flags);
+ out.writeInt(cutout.mCutoutPathParserInfo.getDisplayWidth());
+ out.writeInt(cutout.mCutoutPathParserInfo.getDisplayHeight());
+ out.writeFloat(cutout.mCutoutPathParserInfo.getDensity());
+ out.writeString(cutout.mCutoutPathParserInfo.getCutoutSpec());
+ out.writeInt(cutout.mCutoutPathParserInfo.getRotation());
+ out.writeFloat(cutout.mCutoutPathParserInfo.getScale());
+ }
+ }
+
+ /**
+ * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing
+ * instance.
+ *
+ * Needed for AIDL out parameters.
+ */
+ public void readFromParcel(Parcel in) {
+ mInner = readCutoutFromParcel(in);
+ }
+
+ public static final @android.annotation.NonNull Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() {
+ @Override
+ public ParcelableWrapper createFromParcel(Parcel in) {
+ return new ParcelableWrapper(readCutoutFromParcel(in));
+ }
+
+ @Override
+ public ParcelableWrapper[] newArray(int size) {
+ return new ParcelableWrapper[size];
+ }
+ };
+
+ /**
+ * Reads a DisplayCutout from a {@link Parcel}.
+ *
+ * @see #writeCutoutToParcel(DisplayCutout, Parcel, int)
+ */
+ public static DisplayCutout readCutoutFromParcel(Parcel in) {
+ int variant = in.readInt();
+ if (variant == -1) {
+ return null;
+ }
+ if (variant == 0) {
+ return NO_CUTOUT;
+ }
+
+ Rect safeInsets = in.readTypedObject(Rect.CREATOR);
+ Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH];
+ in.readTypedArray(bounds, Rect.CREATOR);
+ Insets waterfallInsets = in.readTypedObject(Insets.CREATOR);
+ int displayWidth = in.readInt();
+ int displayHeight = in.readInt();
+ float density = in.readFloat();
+ String cutoutSpec = in.readString();
+ int rotation = in.readInt();
+ float scale = in.readFloat();
+ final CutoutPathParserInfo info = new CutoutPathParserInfo(
+ displayWidth, displayHeight, density, cutoutSpec, rotation, scale);
+
+ return new DisplayCutout(
+ safeInsets, waterfallInsets, bounds, info, false /* copyArguments */);
+ }
+
+ public DisplayCutout get() {
+ return mInner;
+ }
+
+ public void set(ParcelableWrapper cutout) {
+ mInner = cutout.get();
+ }
+
+ public void set(DisplayCutout cutout) {
+ mInner = cutout;
+ }
+
+ public void scale(float scale) {
+ final Rect safeInsets = mInner.getSafeInsets();
+ safeInsets.scale(scale);
+ final Bounds bounds = new Bounds(mInner.mBounds.mRects, true);
+ bounds.scale(scale);
+ final Rect waterfallInsets = mInner.mWaterfallInsets.toRect();
+ waterfallInsets.scale(scale);
+ final CutoutPathParserInfo info = new CutoutPathParserInfo(
+ mInner.mCutoutPathParserInfo.getDisplayWidth(),
+ mInner.mCutoutPathParserInfo.getDisplayHeight(),
+ mInner.mCutoutPathParserInfo.getDensity(),
+ mInner.mCutoutPathParserInfo.getCutoutSpec(),
+ mInner.mCutoutPathParserInfo.getRotation(),
+ scale);
+
+ mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info);
+ }
+
+ @Override
+ public int hashCode() {
+ return mInner.hashCode();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return o instanceof ParcelableWrapper
+ && mInner.equals(((ParcelableWrapper) o).mInner);
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(mInner);
+ }
+ }
+}
diff --git a/android/view/DisplayEventReceiver.java b/android/view/DisplayEventReceiver.java
new file mode 100644
index 0000000..5c08632
--- /dev/null
+++ b/android/view/DisplayEventReceiver.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.FrameInfo;
+import android.os.Build;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.annotation.optimization.FastNative;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides a low-level mechanism for an application to receive display events
+ * such as vertical sync.
+ *
+ * The display event receive is NOT thread safe. Moreover, its methods must only
+ * be called on the Looper thread to which it is attached.
+ *
+ * @hide
+ */
+public abstract class DisplayEventReceiver {
+
+ /**
+ * When retrieving vsync events, this specifies that the vsync event should happen at the normal
+ * vsync-app tick.
+ * <p>
+ * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
+ */
+ public static final int VSYNC_SOURCE_APP = 0;
+
+ /**
+ * When retrieving vsync events, this specifies that the vsync event should happen whenever
+ * Surface Flinger is processing a frame.
+ * <p>
+ * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
+ */
+ public static final int VSYNC_SOURCE_SURFACE_FLINGER = 1;
+
+ /**
+ * Specifies to generate mode changed events from Surface Flinger.
+ * <p>
+ * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
+ */
+ public static final int EVENT_REGISTRATION_MODE_CHANGED_FLAG = 0x1;
+
+ /**
+ * Specifies to generate frame rate override events from Surface Flinger.
+ * <p>
+ * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
+ */
+ public static final int EVENT_REGISTRATION_FRAME_RATE_OVERRIDE_FLAG = 0x2;
+
+ private static final String TAG = "DisplayEventReceiver";
+
+ @UnsupportedAppUsage
+ private long mReceiverPtr;
+
+ // We keep a reference message queue object here so that it is not
+ // GC'd while the native peer of the receiver is using them.
+ private MessageQueue mMessageQueue;
+
+ private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
+ MessageQueue messageQueue, int vsyncSource, int eventRegistration);
+ private static native void nativeDispose(long receiverPtr);
+ @FastNative
+ private static native void nativeScheduleVsync(long receiverPtr);
+
+ /**
+ * Creates a display event receiver.
+ *
+ * @param looper The looper to use when invoking callbacks.
+ */
+ @UnsupportedAppUsage
+ public DisplayEventReceiver(Looper looper) {
+ this(looper, VSYNC_SOURCE_APP, 0);
+ }
+
+ /**
+ * Creates a display event receiver.
+ *
+ * @param looper The looper to use when invoking callbacks.
+ * @param vsyncSource The source of the vsync tick. Must be on of the VSYNC_SOURCE_* values.
+ * @param eventRegistration Which events to dispatch. Must be a bitfield consist of the
+ * EVENT_REGISTRATION_*_FLAG values.
+ */
+ public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
+ if (looper == null) {
+ throw new IllegalArgumentException("looper must not be null");
+ }
+
+ mMessageQueue = looper.getQueue();
+ mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
+ vsyncSource, eventRegistration);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Disposes the receiver.
+ */
+ public void dispose() {
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ if (mReceiverPtr != 0) {
+ nativeDispose(mReceiverPtr);
+ mReceiverPtr = 0;
+ }
+ mMessageQueue = null;
+ }
+
+ static final class VsyncEventData {
+ // The frame timeline vsync id, used to correlate a frame
+ // produced by HWUI with the timeline data stored in Surface Flinger.
+ public final long id;
+
+ // The frame deadline timestamp in {@link System#nanoTime()} timebase that it is
+ // allotted for the frame to be completed.
+ public final long frameDeadline;
+
+ /**
+ * The current interval between frames in ns. This will be used to align
+ * {@link FrameInfo#VSYNC} to the current vsync in case Choreographer callback was heavily
+ * delayed by the app.
+ */
+ public final long frameInterval;
+
+ VsyncEventData(long id, long frameDeadline, long frameInterval) {
+ this.id = id;
+ this.frameDeadline = frameDeadline;
+ this.frameInterval = frameInterval;
+ }
+
+ VsyncEventData() {
+ this.id = FrameInfo.INVALID_VSYNC_ID;
+ this.frameDeadline = Long.MAX_VALUE;
+ this.frameInterval = -1;
+ }
+ }
+
+ /**
+ * Called when a vertical sync pulse is received.
+ * The recipient should render a frame and then call {@link #scheduleVsync}
+ * to schedule the next vertical sync pulse.
+ *
+ * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()}
+ * timebase.
+ * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+ * @param frame The frame number. Increases by one for each vertical sync interval.
+ * @param vsyncEventData The vsync event data.
+ */
+ public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
+ VsyncEventData vsyncEventData) {
+ }
+
+ /**
+ * Called when a display hotplug event is received.
+ *
+ * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()}
+ * timebase.
+ * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+ * @param connected True if the display is connected, false if it disconnected.
+ */
+ @UnsupportedAppUsage
+ public void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected) {
+ }
+
+ /**
+ * Called when a display mode changed event is received.
+ *
+ * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()}
+ * timebase.
+ * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+ * @param modeId The new mode Id
+ */
+ public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
+ }
+
+ /**
+ * Represents a mapping between a UID and an override frame rate
+ */
+ public static class FrameRateOverride {
+ // The application uid
+ public final int uid;
+
+ // The frame rate that this application runs at
+ public final float frameRateHz;
+
+
+ @VisibleForTesting
+ public FrameRateOverride(int uid, float frameRateHz) {
+ this.uid = uid;
+ this.frameRateHz = frameRateHz;
+ }
+
+ @Override
+ public String toString() {
+ return "{uid=" + uid + " frameRateHz=" + frameRateHz + "}";
+ }
+ }
+
+ /**
+ * Called when frame rate override event is received.
+ *
+ * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()}
+ * timebase.
+ * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+ * @param overrides The mappings from uid to frame rates
+ */
+ public void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
+ FrameRateOverride[] overrides) {
+ }
+
+ /**
+ * Schedules a single vertical sync pulse to be delivered when the next
+ * display frame begins.
+ */
+ @UnsupportedAppUsage
+ public void scheduleVsync() {
+ if (mReceiverPtr == 0) {
+ Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ + "receiver has already been disposed.");
+ } else {
+ nativeScheduleVsync(mReceiverPtr);
+ }
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame,
+ long frameTimelineVsyncId, long frameDeadline, long frameInterval) {
+ onVsync(timestampNanos, physicalDisplayId, frame,
+ new VsyncEventData(frameTimelineVsyncId, frameDeadline, frameInterval));
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void dispatchHotplug(long timestampNanos, long physicalDisplayId, boolean connected) {
+ onHotplug(timestampNanos, physicalDisplayId, connected);
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ private void dispatchModeChanged(long timestampNanos, long physicalDisplayId, int modeId) {
+ onModeChanged(timestampNanos, physicalDisplayId, modeId);
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ private void dispatchFrameRateOverrides(long timestampNanos, long physicalDisplayId,
+ FrameRateOverride[] overrides) {
+ onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
+ }
+
+}
diff --git a/android/view/DisplayInfo.java b/android/view/DisplayInfo.java
new file mode 100644
index 0000000..8e5f905
--- /dev/null
+++ b/android/view/DisplayInfo.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.DisplayInfoProto.APP_HEIGHT;
+import static android.view.DisplayInfoProto.APP_WIDTH;
+import static android.view.DisplayInfoProto.FLAGS;
+import static android.view.DisplayInfoProto.LOGICAL_HEIGHT;
+import static android.view.DisplayInfoProto.LOGICAL_WIDTH;
+import static android.view.DisplayInfoProto.NAME;
+
+import android.annotation.Nullable;
+import android.app.WindowConfiguration;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.hardware.display.DeviceProductInfo;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.proto.ProtoOutputStream;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Describes the characteristics of a particular logical display.
+ * @hide
+ */
+public final class DisplayInfo implements Parcelable {
+ /**
+ * The surface flinger layer stack associated with this logical display.
+ */
+ public int layerStack;
+
+ /**
+ * Display flags.
+ */
+ public int flags;
+
+ /**
+ * Display type.
+ */
+ public int type;
+
+ /**
+ * Logical display identifier.
+ */
+ public int displayId;
+
+ /**
+ * Display Group identifier.
+ */
+ public int displayGroupId;
+
+ /**
+ * Display address, or null if none.
+ * Interpretation varies by display type.
+ */
+ public DisplayAddress address;
+
+ /**
+ * Product-specific information about the display or the directly connected device on the
+ * display chain. For example, if the display is transitively connected, this field may contain
+ * product information about the intermediate device.
+ */
+ public DeviceProductInfo deviceProductInfo;
+
+ /**
+ * The human-readable name of the display.
+ */
+ public String name;
+
+ /**
+ * Unique identifier for the display. Shouldn't be displayed to the user.
+ */
+ public String uniqueId;
+
+ /**
+ * The width of the portion of the display that is available to applications, in pixels.
+ * Represents the size of the display minus any system decorations.
+ */
+ public int appWidth;
+
+ /**
+ * The height of the portion of the display that is available to applications, in pixels.
+ * Represents the size of the display minus any system decorations.
+ */
+ public int appHeight;
+
+ /**
+ * The smallest value of {@link #appWidth} that an application is likely to encounter,
+ * in pixels, excepting cases where the width may be even smaller due to the presence
+ * of a soft keyboard, for example.
+ */
+ public int smallestNominalAppWidth;
+
+ /**
+ * The smallest value of {@link #appHeight} that an application is likely to encounter,
+ * in pixels, excepting cases where the height may be even smaller due to the presence
+ * of a soft keyboard, for example.
+ */
+ public int smallestNominalAppHeight;
+
+ /**
+ * The largest value of {@link #appWidth} that an application is likely to encounter,
+ * in pixels, excepting cases where the width may be even larger due to system decorations
+ * such as the status bar being hidden, for example.
+ */
+ public int largestNominalAppWidth;
+
+ /**
+ * The largest value of {@link #appHeight} that an application is likely to encounter,
+ * in pixels, excepting cases where the height may be even larger due to system decorations
+ * such as the status bar being hidden, for example.
+ */
+ public int largestNominalAppHeight;
+
+ /**
+ * The logical width of the display, in pixels.
+ * Represents the usable size of the display which may be smaller than the
+ * physical size when the system is emulating a smaller display.
+ */
+ @UnsupportedAppUsage
+ public int logicalWidth;
+
+ /**
+ * The logical height of the display, in pixels.
+ * Represents the usable size of the display which may be smaller than the
+ * physical size when the system is emulating a smaller display.
+ */
+ @UnsupportedAppUsage
+ public int logicalHeight;
+
+ /**
+ * The {@link DisplayCutout} if present, otherwise {@code null}.
+ *
+ * @hide
+ */
+ // Remark on @UnsupportedAppUsage: Display.getCutout should be used instead
+ @Nullable
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ public DisplayCutout displayCutout;
+
+ /**
+ * The rotation of the display relative to its natural orientation.
+ * May be one of {@link android.view.Surface#ROTATION_0},
+ * {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180},
+ * {@link android.view.Surface#ROTATION_270}.
+ * <p>
+ * The value of this field is indeterminate if the logical display is presented on
+ * more than one physical display.
+ * </p>
+ */
+ @Surface.Rotation
+ @UnsupportedAppUsage
+ public int rotation;
+
+ /**
+ * The active display mode.
+ */
+ public int modeId;
+
+ /**
+ * The default display mode.
+ */
+ public int defaultModeId;
+
+ /**
+ * The supported modes of this display.
+ */
+ public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
+
+ /** The active color mode. */
+ public int colorMode;
+
+ /** The list of supported color modes */
+ public int[] supportedColorModes = { Display.COLOR_MODE_DEFAULT };
+
+ /** The display's HDR capabilities */
+ public Display.HdrCapabilities hdrCapabilities;
+
+ /** The formats disabled by user **/
+ public int[] userDisabledHdrTypes = {};
+
+ /**
+ * Indicates whether the display can be switched into a mode with minimal post
+ * processing.
+ *
+ * @see android.view.Display#isMinimalPostProcessingSupported
+ */
+ public boolean minimalPostProcessingSupported;
+
+ /**
+ * The logical display density which is the basis for density-independent
+ * pixels.
+ */
+ public int logicalDensityDpi;
+
+ /**
+ * The exact physical pixels per inch of the screen in the X dimension.
+ * <p>
+ * The value of this field is indeterminate if the logical display is presented on
+ * more than one physical display.
+ * </p>
+ */
+ public float physicalXDpi;
+
+ /**
+ * The exact physical pixels per inch of the screen in the Y dimension.
+ * <p>
+ * The value of this field is indeterminate if the logical display is presented on
+ * more than one physical display.
+ * </p>
+ */
+ public float physicalYDpi;
+
+ /**
+ * This is a positive value indicating the phase offset of the VSYNC events provided by
+ * Choreographer relative to the display refresh. For example, if Choreographer reports
+ * that the refresh occurred at time N, it actually occurred at (N - appVsyncOffsetNanos).
+ */
+ public long appVsyncOffsetNanos;
+
+ /**
+ * This is how far in advance a buffer must be queued for presentation at
+ * a given time. If you want a buffer to appear on the screen at
+ * time N, you must submit the buffer before (N - bufferDeadlineNanos).
+ */
+ public long presentationDeadlineNanos;
+
+ /**
+ * The state of the display, such as {@link android.view.Display#STATE_ON}.
+ */
+ public int state;
+
+ /**
+ * The UID of the application that owns this display, or zero if it is owned by the system.
+ * <p>
+ * If the display is private, then only the owner can use it.
+ * </p>
+ */
+ public int ownerUid;
+
+ /**
+ * The package name of the application that owns this display, or null if it is
+ * owned by the system.
+ * <p>
+ * If the display is private, then only the owner can use it.
+ * </p>
+ */
+ public String ownerPackageName;
+
+ /**
+ * The refresh rate override for this app. 0 means no override.
+ */
+ public float refreshRateOverride;
+
+ /**
+ * @hide
+ * Get current remove mode of the display - what actions should be performed with the display's
+ * content when it is removed.
+ *
+ * @see Display#getRemoveMode()
+ */
+ // TODO (b/114338689): Remove the flag and use IWindowManager#getRemoveContentMode
+ public int removeMode = Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY;
+
+ /**
+ * @hide
+ * The current minimum brightness constraint of the display. Value between 0.0 and 1.0,
+ * derived from the config constraints of the display device of this logical display.
+ */
+ public float brightnessMinimum;
+
+ /**
+ * @hide
+ * The current maximum brightness constraint of the display. Value between 0.0 and 1.0,
+ * derived from the config constraints of the display device of this logical display.
+ */
+ public float brightnessMaximum;
+
+ /**
+ * @hide
+ * The current default brightness of the display. Value between 0.0 and 1.0,
+ * derived from the configuration of the display device of this logical display.
+ */
+ public float brightnessDefault;
+
+ /**
+ * The {@link RoundedCorners} if present, otherwise {@code null}.
+ */
+ @Nullable
+ public RoundedCorners roundedCorners;
+
+ public static final @android.annotation.NonNull Creator<DisplayInfo> CREATOR = new Creator<DisplayInfo>() {
+ @Override
+ public DisplayInfo createFromParcel(Parcel source) {
+ return new DisplayInfo(source);
+ }
+
+ @Override
+ public DisplayInfo[] newArray(int size) {
+ return new DisplayInfo[size];
+ }
+ };
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769467)
+ public DisplayInfo() {
+ }
+
+ public DisplayInfo(DisplayInfo other) {
+ copyFrom(other);
+ }
+
+ private DisplayInfo(Parcel source) {
+ readFromParcel(source);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return o instanceof DisplayInfo && equals((DisplayInfo)o);
+ }
+
+ public boolean equals(DisplayInfo other) {
+ return other != null
+ && layerStack == other.layerStack
+ && flags == other.flags
+ && type == other.type
+ && displayId == other.displayId
+ && displayGroupId == other.displayGroupId
+ && Objects.equals(address, other.address)
+ && Objects.equals(deviceProductInfo, other.deviceProductInfo)
+ && Objects.equals(uniqueId, other.uniqueId)
+ && appWidth == other.appWidth
+ && appHeight == other.appHeight
+ && smallestNominalAppWidth == other.smallestNominalAppWidth
+ && smallestNominalAppHeight == other.smallestNominalAppHeight
+ && largestNominalAppWidth == other.largestNominalAppWidth
+ && largestNominalAppHeight == other.largestNominalAppHeight
+ && logicalWidth == other.logicalWidth
+ && logicalHeight == other.logicalHeight
+ && Objects.equals(displayCutout, other.displayCutout)
+ && rotation == other.rotation
+ && modeId == other.modeId
+ && defaultModeId == other.defaultModeId
+ && Arrays.equals(supportedModes, other.supportedModes)
+ && colorMode == other.colorMode
+ && Arrays.equals(supportedColorModes, other.supportedColorModes)
+ && Objects.equals(hdrCapabilities, other.hdrCapabilities)
+ && Arrays.equals(userDisabledHdrTypes, other.userDisabledHdrTypes)
+ && minimalPostProcessingSupported == other.minimalPostProcessingSupported
+ && logicalDensityDpi == other.logicalDensityDpi
+ && physicalXDpi == other.physicalXDpi
+ && physicalYDpi == other.physicalYDpi
+ && appVsyncOffsetNanos == other.appVsyncOffsetNanos
+ && presentationDeadlineNanos == other.presentationDeadlineNanos
+ && state == other.state
+ && ownerUid == other.ownerUid
+ && Objects.equals(ownerPackageName, other.ownerPackageName)
+ && removeMode == other.removeMode
+ && getRefreshRate() == other.getRefreshRate()
+ && brightnessMinimum == other.brightnessMinimum
+ && brightnessMaximum == other.brightnessMaximum
+ && brightnessDefault == other.brightnessDefault
+ && Objects.equals(roundedCorners, other.roundedCorners);
+ }
+
+ @Override
+ public int hashCode() {
+ return 0; // don't care
+ }
+
+ public void copyFrom(DisplayInfo other) {
+ layerStack = other.layerStack;
+ flags = other.flags;
+ type = other.type;
+ displayId = other.displayId;
+ displayGroupId = other.displayGroupId;
+ address = other.address;
+ deviceProductInfo = other.deviceProductInfo;
+ name = other.name;
+ uniqueId = other.uniqueId;
+ appWidth = other.appWidth;
+ appHeight = other.appHeight;
+ smallestNominalAppWidth = other.smallestNominalAppWidth;
+ smallestNominalAppHeight = other.smallestNominalAppHeight;
+ largestNominalAppWidth = other.largestNominalAppWidth;
+ largestNominalAppHeight = other.largestNominalAppHeight;
+ logicalWidth = other.logicalWidth;
+ logicalHeight = other.logicalHeight;
+ displayCutout = other.displayCutout;
+ rotation = other.rotation;
+ modeId = other.modeId;
+ defaultModeId = other.defaultModeId;
+ supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
+ colorMode = other.colorMode;
+ supportedColorModes = Arrays.copyOf(
+ other.supportedColorModes, other.supportedColorModes.length);
+ hdrCapabilities = other.hdrCapabilities;
+ userDisabledHdrTypes = other.userDisabledHdrTypes;
+ minimalPostProcessingSupported = other.minimalPostProcessingSupported;
+ logicalDensityDpi = other.logicalDensityDpi;
+ physicalXDpi = other.physicalXDpi;
+ physicalYDpi = other.physicalYDpi;
+ appVsyncOffsetNanos = other.appVsyncOffsetNanos;
+ presentationDeadlineNanos = other.presentationDeadlineNanos;
+ state = other.state;
+ ownerUid = other.ownerUid;
+ ownerPackageName = other.ownerPackageName;
+ removeMode = other.removeMode;
+ refreshRateOverride = other.refreshRateOverride;
+ brightnessMinimum = other.brightnessMinimum;
+ brightnessMaximum = other.brightnessMaximum;
+ brightnessDefault = other.brightnessDefault;
+ roundedCorners = other.roundedCorners;
+ }
+
+ public void readFromParcel(Parcel source) {
+ layerStack = source.readInt();
+ flags = source.readInt();
+ type = source.readInt();
+ displayId = source.readInt();
+ displayGroupId = source.readInt();
+ address = source.readParcelable(null);
+ deviceProductInfo = source.readParcelable(null);
+ name = source.readString8();
+ appWidth = source.readInt();
+ appHeight = source.readInt();
+ smallestNominalAppWidth = source.readInt();
+ smallestNominalAppHeight = source.readInt();
+ largestNominalAppWidth = source.readInt();
+ largestNominalAppHeight = source.readInt();
+ logicalWidth = source.readInt();
+ logicalHeight = source.readInt();
+ displayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(source);
+ rotation = source.readInt();
+ modeId = source.readInt();
+ defaultModeId = source.readInt();
+ int nModes = source.readInt();
+ supportedModes = new Display.Mode[nModes];
+ for (int i = 0; i < nModes; i++) {
+ supportedModes[i] = Display.Mode.CREATOR.createFromParcel(source);
+ }
+ colorMode = source.readInt();
+ int nColorModes = source.readInt();
+ supportedColorModes = new int[nColorModes];
+ for (int i = 0; i < nColorModes; i++) {
+ supportedColorModes[i] = source.readInt();
+ }
+ hdrCapabilities = source.readParcelable(null);
+ minimalPostProcessingSupported = source.readBoolean();
+ logicalDensityDpi = source.readInt();
+ physicalXDpi = source.readFloat();
+ physicalYDpi = source.readFloat();
+ appVsyncOffsetNanos = source.readLong();
+ presentationDeadlineNanos = source.readLong();
+ state = source.readInt();
+ ownerUid = source.readInt();
+ ownerPackageName = source.readString8();
+ uniqueId = source.readString8();
+ removeMode = source.readInt();
+ refreshRateOverride = source.readFloat();
+ brightnessMinimum = source.readFloat();
+ brightnessMaximum = source.readFloat();
+ brightnessDefault = source.readFloat();
+ roundedCorners = source.readTypedObject(RoundedCorners.CREATOR);
+ int numUserDisabledFormats = source.readInt();
+ userDisabledHdrTypes = new int[numUserDisabledFormats];
+ for (int i = 0; i < numUserDisabledFormats; i++) {
+ userDisabledHdrTypes[i] = source.readInt();
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(layerStack);
+ dest.writeInt(this.flags);
+ dest.writeInt(type);
+ dest.writeInt(displayId);
+ dest.writeInt(displayGroupId);
+ dest.writeParcelable(address, flags);
+ dest.writeParcelable(deviceProductInfo, flags);
+ dest.writeString8(name);
+ dest.writeInt(appWidth);
+ dest.writeInt(appHeight);
+ dest.writeInt(smallestNominalAppWidth);
+ dest.writeInt(smallestNominalAppHeight);
+ dest.writeInt(largestNominalAppWidth);
+ dest.writeInt(largestNominalAppHeight);
+ dest.writeInt(logicalWidth);
+ dest.writeInt(logicalHeight);
+ DisplayCutout.ParcelableWrapper.writeCutoutToParcel(displayCutout, dest, flags);
+ dest.writeInt(rotation);
+ dest.writeInt(modeId);
+ dest.writeInt(defaultModeId);
+ dest.writeInt(supportedModes.length);
+ for (int i = 0; i < supportedModes.length; i++) {
+ supportedModes[i].writeToParcel(dest, flags);
+ }
+ dest.writeInt(colorMode);
+ dest.writeInt(supportedColorModes.length);
+ for (int i = 0; i < supportedColorModes.length; i++) {
+ dest.writeInt(supportedColorModes[i]);
+ }
+ dest.writeParcelable(hdrCapabilities, flags);
+ dest.writeBoolean(minimalPostProcessingSupported);
+ dest.writeInt(logicalDensityDpi);
+ dest.writeFloat(physicalXDpi);
+ dest.writeFloat(physicalYDpi);
+ dest.writeLong(appVsyncOffsetNanos);
+ dest.writeLong(presentationDeadlineNanos);
+ dest.writeInt(state);
+ dest.writeInt(ownerUid);
+ dest.writeString8(ownerPackageName);
+ dest.writeString8(uniqueId);
+ dest.writeInt(removeMode);
+ dest.writeFloat(refreshRateOverride);
+ dest.writeFloat(brightnessMinimum);
+ dest.writeFloat(brightnessMaximum);
+ dest.writeFloat(brightnessDefault);
+ dest.writeTypedObject(roundedCorners, flags);
+ dest.writeInt(userDisabledHdrTypes.length);
+ for (int i = 0; i < userDisabledHdrTypes.length; i++) {
+ dest.writeInt(userDisabledHdrTypes[i]);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns the refresh rate the application would experience.
+ */
+ public float getRefreshRate() {
+ if (refreshRateOverride > 0) {
+ return refreshRateOverride;
+ }
+
+ return getMode().getRefreshRate();
+ }
+
+ public Display.Mode getMode() {
+ return findMode(modeId);
+ }
+
+ public Display.Mode getDefaultMode() {
+ return findMode(defaultModeId);
+ }
+
+ private Display.Mode findMode(int id) {
+ for (int i = 0; i < supportedModes.length; i++) {
+ if (supportedModes[i].getModeId() == id) {
+ return supportedModes[i];
+ }
+ }
+ throw new IllegalStateException("Unable to locate mode " + id);
+ }
+
+ /**
+ * Returns the id of the "default" mode with the given refresh rate, or {@code 0} if no suitable
+ * mode could be found.
+ */
+ @Nullable
+ public Display.Mode findDefaultModeByRefreshRate(float refreshRate) {
+ Display.Mode[] modes = supportedModes;
+ Display.Mode defaultMode = getDefaultMode();
+ for (int i = 0; i < modes.length; i++) {
+ if (modes[i].matches(
+ defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), refreshRate)) {
+ return modes[i];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the list of supported refresh rates in the default mode.
+ */
+ public float[] getDefaultRefreshRates() {
+ Display.Mode[] modes = supportedModes;
+ ArraySet<Float> rates = new ArraySet<>();
+ Display.Mode defaultMode = getDefaultMode();
+ for (int i = 0; i < modes.length; i++) {
+ Display.Mode mode = modes[i];
+ if (mode.getPhysicalWidth() == defaultMode.getPhysicalWidth()
+ && mode.getPhysicalHeight() == defaultMode.getPhysicalHeight()) {
+ rates.add(mode.getRefreshRate());
+ }
+ }
+ float[] result = new float[rates.size()];
+ int i = 0;
+ for (Float rate : rates) {
+ result[i++] = rate;
+ }
+ return result;
+ }
+
+ public void getAppMetrics(DisplayMetrics outMetrics) {
+ getAppMetrics(outMetrics, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+ }
+
+ public void getAppMetrics(DisplayMetrics outMetrics, DisplayAdjustments displayAdjustments) {
+ getMetricsWithSize(outMetrics, displayAdjustments.getCompatibilityInfo(),
+ displayAdjustments.getConfiguration(), appWidth, appHeight);
+ }
+
+ public void getAppMetrics(DisplayMetrics outMetrics, CompatibilityInfo ci,
+ Configuration configuration) {
+ getMetricsWithSize(outMetrics, ci, configuration, appWidth, appHeight);
+ }
+
+ /**
+ * Populates {@code outMetrics} with details of the logical display. Bounds are limited
+ * by the logical size of the display.
+ *
+ * @param outMetrics the {@link DisplayMetrics} to be populated
+ * @param compatInfo the {@link CompatibilityInfo} to be applied
+ * @param configuration the {@link Configuration}
+ */
+ public void getLogicalMetrics(DisplayMetrics outMetrics, CompatibilityInfo compatInfo,
+ Configuration configuration) {
+ getMetricsWithSize(outMetrics, compatInfo, configuration, logicalWidth, logicalHeight);
+ }
+
+ /**
+ * Similar to {@link #getLogicalMetrics}, but the limiting bounds are determined from
+ * {@link WindowConfiguration#getMaxBounds()}
+ */
+ public void getMaxBoundsMetrics(DisplayMetrics outMetrics, CompatibilityInfo compatInfo,
+ Configuration configuration) {
+ Rect bounds = configuration.windowConfiguration.getMaxBounds();
+ // Pass in null configuration to ensure width and height are not overridden to app bounds.
+ getMetricsWithSize(outMetrics, compatInfo, /* configuration= */ null,
+ bounds.width(), bounds.height());
+ }
+
+ public int getNaturalWidth() {
+ return rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 ?
+ logicalWidth : logicalHeight;
+ }
+
+ public int getNaturalHeight() {
+ return rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 ?
+ logicalHeight : logicalWidth;
+ }
+
+ public boolean isHdr() {
+ int[] types = hdrCapabilities != null ? hdrCapabilities.getSupportedHdrTypes() : null;
+ return types != null && types.length > 0;
+ }
+
+ public boolean isWideColorGamut() {
+ for (int colorMode : supportedColorModes) {
+ if (colorMode == Display.COLOR_MODE_DCI_P3 || colorMode > Display.COLOR_MODE_SRGB) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the specified UID has access to this display.
+ */
+ public boolean hasAccess(int uid) {
+ return Display.hasAccess(uid, flags, ownerUid, displayId);
+ }
+
+ private void getMetricsWithSize(DisplayMetrics outMetrics, CompatibilityInfo compatInfo,
+ Configuration configuration, int width, int height) {
+ outMetrics.densityDpi = outMetrics.noncompatDensityDpi = logicalDensityDpi;
+ outMetrics.density = outMetrics.noncompatDensity =
+ logicalDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ outMetrics.scaledDensity = outMetrics.noncompatScaledDensity = outMetrics.density;
+ outMetrics.xdpi = outMetrics.noncompatXdpi = physicalXDpi;
+ outMetrics.ydpi = outMetrics.noncompatYdpi = physicalYDpi;
+
+ final Rect appBounds = configuration != null
+ ? configuration.windowConfiguration.getAppBounds() : null;
+ width = appBounds != null ? appBounds.width() : width;
+ height = appBounds != null ? appBounds.height() : height;
+
+ outMetrics.noncompatWidthPixels = outMetrics.widthPixels = width;
+ outMetrics.noncompatHeightPixels = outMetrics.heightPixels = height;
+
+ if (!compatInfo.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) {
+ compatInfo.applyToDisplayMetrics(outMetrics);
+ }
+ }
+
+ // For debugging purposes
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("DisplayInfo{\"");
+ sb.append(name);
+ sb.append("\", displayId ");
+ sb.append(displayId);
+ sb.append("\", displayGroupId ");
+ sb.append(displayGroupId);
+ sb.append(flagsToString(flags));
+ sb.append(", real ");
+ sb.append(logicalWidth);
+ sb.append(" x ");
+ sb.append(logicalHeight);
+ sb.append(", largest app ");
+ sb.append(largestNominalAppWidth);
+ sb.append(" x ");
+ sb.append(largestNominalAppHeight);
+ sb.append(", smallest app ");
+ sb.append(smallestNominalAppWidth);
+ sb.append(" x ");
+ sb.append(smallestNominalAppHeight);
+ sb.append(", appVsyncOff ");
+ sb.append(appVsyncOffsetNanos);
+ sb.append(", presDeadline ");
+ sb.append(presentationDeadlineNanos);
+ sb.append(", mode ");
+ sb.append(modeId);
+ sb.append(", defaultMode ");
+ sb.append(defaultModeId);
+ sb.append(", modes ");
+ sb.append(Arrays.toString(supportedModes));
+ sb.append(", hdrCapabilities ");
+ sb.append(hdrCapabilities);
+ sb.append(", userDisabledHdrTypes ");
+ sb.append(Arrays.toString(userDisabledHdrTypes));
+ sb.append(", minimalPostProcessingSupported ");
+ sb.append(minimalPostProcessingSupported);
+ sb.append(", rotation ");
+ sb.append(rotation);
+ sb.append(", state ");
+ sb.append(Display.stateToString(state));
+
+ if (Process.myUid() != Process.SYSTEM_UID) {
+ sb.append("}");
+ return sb.toString();
+ }
+
+ sb.append(", type ");
+ sb.append(Display.typeToString(type));
+ sb.append(", uniqueId \"");
+ sb.append(uniqueId);
+ sb.append("\", app ");
+ sb.append(appWidth);
+ sb.append(" x ");
+ sb.append(appHeight);
+ sb.append(", density ");
+ sb.append(logicalDensityDpi);
+ sb.append(" (");
+ sb.append(physicalXDpi);
+ sb.append(" x ");
+ sb.append(physicalYDpi);
+ sb.append(") dpi, layerStack ");
+ sb.append(layerStack);
+ sb.append(", colorMode ");
+ sb.append(colorMode);
+ sb.append(", supportedColorModes ");
+ sb.append(Arrays.toString(supportedColorModes));
+ if (address != null) {
+ sb.append(", address ").append(address);
+ }
+ sb.append(", deviceProductInfo ");
+ sb.append(deviceProductInfo);
+ if (ownerUid != 0 || ownerPackageName != null) {
+ sb.append(", owner ").append(ownerPackageName);
+ sb.append(" (uid ").append(ownerUid).append(")");
+ }
+ sb.append(", removeMode ");
+ sb.append(removeMode);
+ sb.append(", refreshRateOverride ");
+ sb.append(refreshRateOverride);
+ sb.append(", brightnessMinimum ");
+ sb.append(brightnessMinimum);
+ sb.append(", brightnessMaximum ");
+ sb.append(brightnessMaximum);
+ sb.append(", brightnessDefault ");
+ sb.append(brightnessDefault);
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Write to a protocol buffer output stream.
+ * Protocol buffer message definition at {@link android.view.DisplayInfoProto}
+ *
+ * @param protoOutputStream Stream to write the Rect object to.
+ * @param fieldId Field Id of the DisplayInfoProto as defined in the parent message
+ */
+ public void dumpDebug(ProtoOutputStream protoOutputStream, long fieldId) {
+ final long token = protoOutputStream.start(fieldId);
+ protoOutputStream.write(LOGICAL_WIDTH, logicalWidth);
+ protoOutputStream.write(LOGICAL_HEIGHT, logicalHeight);
+ protoOutputStream.write(APP_WIDTH, appWidth);
+ protoOutputStream.write(APP_HEIGHT, appHeight);
+ protoOutputStream.write(NAME, name);
+ protoOutputStream.write(FLAGS, flags);
+ protoOutputStream.end(token);
+ }
+
+ private static String flagsToString(int flags) {
+ StringBuilder result = new StringBuilder();
+ if ((flags & Display.FLAG_SECURE) != 0) {
+ result.append(", FLAG_SECURE");
+ }
+ if ((flags & Display.FLAG_SUPPORTS_PROTECTED_BUFFERS) != 0) {
+ result.append(", FLAG_SUPPORTS_PROTECTED_BUFFERS");
+ }
+ if ((flags & Display.FLAG_PRIVATE) != 0) {
+ result.append(", FLAG_PRIVATE");
+ }
+ if ((flags & Display.FLAG_PRESENTATION) != 0) {
+ result.append(", FLAG_PRESENTATION");
+ }
+ if ((flags & Display.FLAG_SCALING_DISABLED) != 0) {
+ result.append(", FLAG_SCALING_DISABLED");
+ }
+ if ((flags & Display.FLAG_ROUND) != 0) {
+ result.append(", FLAG_ROUND");
+ }
+ if ((flags & Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD) != 0) {
+ result.append(", FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD");
+ }
+ if ((flags & Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0) {
+ result.append(", FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS");
+ }
+ if ((flags & Display.FLAG_TRUSTED) != 0) {
+ result.append(", FLAG_TRUSTED");
+ }
+ if ((flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0) {
+ result.append(", FLAG_OWN_DISPLAY_GROUP");
+ }
+ return result.toString();
+ }
+}
diff --git a/android/view/Display_Delegate.java b/android/view/Display_Delegate.java
new file mode 100644
index 0000000..53dc821
--- /dev/null
+++ b/android/view/Display_Delegate.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link Display}
+ *
+ * Through the layoutlib_create tool, the original methods of Display have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class Display_Delegate {
+
+ @LayoutlibDelegate
+ static void updateDisplayInfoLocked(Display theDisplay) {
+ // do nothing
+ }
+
+}
diff --git a/android/view/DragAndDropPermissions.java b/android/view/DragAndDropPermissions.java
new file mode 100644
index 0000000..973836a
--- /dev/null
+++ b/android/view/DragAndDropPermissions.java
@@ -0,0 +1,172 @@
+/*
+** Copyright 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.view;
+
+import static java.lang.Integer.toHexString;
+
+import android.app.Activity;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.view.IDragAndDropPermissions;
+
+/**
+ * {@link DragAndDropPermissions} controls the access permissions for the content URIs associated
+ * with a {@link DragEvent}.
+ * <p>
+ * Permission are granted when this object is created by {@link
+ * android.app.Activity#requestDragAndDropPermissions(DragEvent)
+ * Activity.requestDragAndDropPermissions}.
+ * Which permissions are granted is defined by the set of flags passed to {@link
+ * View#startDragAndDrop(android.content.ClipData, View.DragShadowBuilder, Object, int)
+ * View.startDragAndDrop} by the app that started the drag operation.
+ * </p>
+ * <p>
+ * The lifecycle of the permissions is bound to the activity used to call {@link
+ * android.app.Activity#requestDragAndDropPermissions(DragEvent) requestDragAndDropPermissions}. The
+ * permissions are revoked when this activity is destroyed, or when {@link #release()} is called,
+ * whichever occurs first.
+ * </p>
+ * <p>
+ * If you anticipate that your application will receive a large number of drops (e.g. document
+ * editor), you should try to call {@link #release()} on the obtained permissions as soon as they
+ * are no longer required. Permissions can be added to your activity's
+ * {@link Activity#onSaveInstanceState} bundle and later retrieved in order to manually release
+ * the permissions once they are no longer needed.
+ * </p>
+ * <p>
+ * Learn more about <a href="/guide/topics/ui/drag-drop#DragPermissionsMultiWindow">drag permissions
+ * in multi-window mode</a>.
+ * </p>
+ */
+public final class DragAndDropPermissions implements Parcelable {
+
+ private static final String TAG = "DragAndDrop";
+ private static final boolean DEBUG = false;
+
+ private final IDragAndDropPermissions mDragAndDropPermissions;
+
+ /**
+ * Create a new {@link DragAndDropPermissions} object to control the access permissions for
+ * content URIs associated with {@link DragEvent}.
+ * @param dragEvent Drag event
+ * @return {@link DragAndDropPermissions} object or null if there are no content URIs associated
+ * with the {@link DragEvent}.
+ * @hide
+ */
+ public static DragAndDropPermissions obtain(DragEvent dragEvent) {
+ if (dragEvent.getDragAndDropPermissions() == null) {
+ return null;
+ }
+ return new DragAndDropPermissions(dragEvent.getDragAndDropPermissions());
+ }
+
+ /** @hide */
+ private DragAndDropPermissions(IDragAndDropPermissions dragAndDropPermissions) {
+ mDragAndDropPermissions = dragAndDropPermissions;
+ }
+
+ /**
+ * Take permissions, binding their lifetime to the activity.
+ *
+ * <p>Note: This API is exposed to apps via
+ * {@link Activity#requestDragAndDropPermissions(DragEvent)}.
+ *
+ * @param activityToken Binder pointing to an Activity instance to bind the lifetime to.
+ * @return True if permissions are successfully taken.
+ *
+ * @hide
+ */
+ public boolean take(IBinder activityToken) {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, this + ": calling take() with activity-bound token: "
+ + toHexString(activityToken.hashCode()));
+ }
+ mDragAndDropPermissions.take(activityToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, this + ": take() failed with a RemoteException", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Take permissions transiently. Permissions will be tied to this object's lifecycle; if not
+ * released explicitly, they will be released automatically when there are no more references
+ * to this object and it's garbage collected.
+ *
+ * <p>Note: This API is not exposed to apps.
+ *
+ * @return True if permissions are successfully taken.
+ *
+ * @hide
+ */
+ public boolean takeTransient() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, this + ": calling takeTransient()");
+ }
+ mDragAndDropPermissions.takeTransient();
+ } catch (RemoteException e) {
+ Log.w(TAG, this + ": takeTransient() failed with a RemoteException", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Revoke permissions explicitly.
+ */
+ public void release() {
+ try {
+ mDragAndDropPermissions.release();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<DragAndDropPermissions> CREATOR =
+ new Parcelable.Creator<DragAndDropPermissions> () {
+ @Override
+ public DragAndDropPermissions createFromParcel(Parcel source) {
+ return new DragAndDropPermissions(source);
+ }
+
+ @Override
+ public DragAndDropPermissions[] newArray(int size) {
+ return new DragAndDropPermissions[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel destination, int flags) {
+ destination.writeStrongInterface(mDragAndDropPermissions);
+ }
+
+ private DragAndDropPermissions(Parcel in) {
+ mDragAndDropPermissions = IDragAndDropPermissions.Stub.asInterface(in.readStrongBinder());
+ }
+}
diff --git a/android/view/DragEvent.java b/android/view/DragEvent.java
new file mode 100644
index 0000000..b6b029b
--- /dev/null
+++ b/android/view/DragEvent.java
@@ -0,0 +1,634 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.view.IDragAndDropPermissions;
+
+//TODO: Improve Javadoc
+/**
+ * Represents an event that is sent out by the system at various times during a drag and drop
+ * operation. It is a data structure that contains several important pieces of data about
+ * the operation and the underlying data.
+ * <p>
+ * View objects that receive a DragEvent call {@link #getAction()}, which returns
+ * an action type that indicates the state of the drag and drop operation. This allows a View
+ * object to react to a change in state by changing its appearance or performing other actions.
+ * For example, a View can react to the {@link #ACTION_DRAG_ENTERED} action type by
+ * by changing one or more colors in its displayed image.
+ * </p>
+ * <p>
+ * During a drag and drop operation, the system displays an image that the user drags. This image
+ * is called a drag shadow. Several action types reflect the position of the drag shadow relative
+ * to the View receiving the event.
+ * </p>
+ * <p>
+ * Most methods return valid data only for certain event actions. This is summarized in the
+ * following table. Each possible {@link #getAction()} value is listed in the first column. The
+ * other columns indicate which method or methods return valid data for that getAction() value:
+ * </p>
+ * <table>
+ * <tr>
+ * <th scope="col">getAction() Value</th>
+ * <th scope="col">getClipDescription()</th>
+ * <th scope="col">getLocalState()</th>
+ * <th scope="col">getX()</th>
+ * <th scope="col">getY()</th>
+ * <th scope="col">getClipData()</th>
+ * <th scope="col">getResult()</th>
+ * </tr>
+ * <tr>
+ * <td>ACTION_DRAG_STARTED</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;"> </td>
+ * <td style="text-align: center;"> </td>
+ * </tr>
+ * <tr>
+ * <td>ACTION_DRAG_ENTERED</td>
+ * <td style="text-align: center;">X</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>
+ * <td style="text-align: center;"> </td>
+ * </tr>
+ * <tr>
+ * <td>ACTION_DRAG_LOCATION</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;"> </td>
+ * <td style="text-align: center;"> </td>
+ * </tr>
+ * <tr>
+ * <td>ACTION_DRAG_EXITED</td>
+ * <td style="text-align: center;">X</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>
+ * <td style="text-align: center;"> </td>
+ * </tr>
+ * <tr>
+ * <td>ACTION_DROP</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;">X</td>
+ * <td style="text-align: center;"> </td>
+ * </tr>
+ * <tr>
+ * <td>ACTION_DRAG_ENDED</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>
+ * <td style="text-align: center;">X</td>
+ * </tr>
+ * </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.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a guide to implementing drag and drop features, read the
+ * <a href="{@docRoot}guide/topics/ui/drag-drop.html">Drag and Drop</a> developer guide.</p>
+ * </div>
+ */
+public class DragEvent implements Parcelable {
+ private static final boolean TRACK_RECYCLED_LOCATION = false;
+
+ int mAction;
+ float mX, mY;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ ClipDescription mClipDescription;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ ClipData mClipData;
+ IDragAndDropPermissions mDragAndDropPermissions;
+
+ Object mLocalState;
+ boolean mDragResult;
+ boolean mEventHandlerWasCalled;
+
+ /**
+ * The drag surface containing the object being dragged. Only provided if the target window
+ * has the {@link WindowManager.LayoutParams#PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP} flag
+ * and is only sent with {@link #ACTION_DROP}.
+ */
+ private SurfaceControl mDragSurface;
+
+ /**
+ * The offsets from the touch that the surface is adjusted by as the surface is moved around the
+ * screen. Necessary for the target using the drag surface to animate it properly once it takes
+ * ownership of the drag surface after the drop.
+ */
+ private float mOffsetX;
+ private float mOffsetY;
+
+ private DragEvent mNext;
+ private RuntimeException mRecycledLocation;
+ private boolean mRecycled;
+
+ private static final int MAX_RECYCLED = 10;
+ private static final Object gRecyclerLock = new Object();
+ private static int gRecyclerUsed = 0;
+ private static DragEvent gRecyclerTop = null;
+
+ /**
+ * Action constant returned by {@link #getAction()}: Signals the start of a
+ * drag and drop operation. The View should return {@code true} from its
+ * {@link View#onDragEvent(DragEvent) onDragEvent()} handler method or
+ * {@link View.OnDragListener#onDrag(View,DragEvent) OnDragListener.onDrag()} listener
+ * if it can accept a drop. The onDragEvent() or onDrag() methods usually inspect the metadata
+ * from {@link #getClipDescription()} to determine if they can accept the data contained in
+ * this drag. For an operation that doesn't represent data transfer, these methods may
+ * perform other actions to determine whether or not the View should accept the data.
+ * If the View wants to indicate that it is a valid drop target, it can also react by
+ * changing its appearance.
+ * <p>
+ * Views added or becoming visible for the first time during a drag operation receive this
+ * event when they are added or becoming visible.
+ * </p>
+ * <p>
+ * A View only receives further drag events for the drag operation if it returns {@code true}
+ * in response to ACTION_DRAG_STARTED.
+ * </p>
+ * @see #ACTION_DRAG_ENDED
+ * @see #getX()
+ * @see #getY()
+ */
+ public static final int ACTION_DRAG_STARTED = 1;
+
+ /**
+ * Action constant returned by {@link #getAction()}: Sent to a View after
+ * {@link #ACTION_DRAG_ENTERED} while the drag shadow is still within the View object's bounding
+ * box, but not within a descendant view that can accept the data. The {@link #getX()} and
+ * {@link #getY()} methods supply
+ * the X and Y position of of the drag point within the View object's bounding box.
+ * <p>
+ * A View receives an {@link #ACTION_DRAG_ENTERED} event before receiving any
+ * ACTION_DRAG_LOCATION events.
+ * </p>
+ * <p>
+ * The system stops sending ACTION_DRAG_LOCATION events to a View once the user moves the
+ * drag shadow out of the View object's bounding box or into a descendant view that can accept
+ * the data. If the user moves the drag shadow back into the View object's bounding box or out
+ * of a descendant view that can accept the data, the View receives an ACTION_DRAG_ENTERED again
+ * before receiving any more ACTION_DRAG_LOCATION events.
+ * </p>
+ * @see #ACTION_DRAG_ENTERED
+ * @see #getX()
+ * @see #getY()
+ */
+ public static final int ACTION_DRAG_LOCATION = 2;
+
+ /**
+ * Action constant returned by {@link #getAction()}: Signals to a View that the user
+ * has released the drag shadow, and the drag point is within the bounding box of the View and
+ * not within a descendant view that can accept the data.
+ * The View should retrieve the data from the DragEvent by calling {@link #getClipData()}.
+ * The methods {@link #getX()} and {@link #getY()} return the X and Y position of the drop point
+ * within the View object's bounding box.
+ * <p>
+ * The View should return {@code true} from its {@link View#onDragEvent(DragEvent)}
+ * handler or {@link View.OnDragListener#onDrag(View,DragEvent) OnDragListener.onDrag()}
+ * listener if it accepted the drop, and {@code false} if it ignored the drop.
+ * </p>
+ * <p>
+ * The View can also react to this action by changing its appearance.
+ * </p>
+ * @see #getClipData()
+ * @see #getX()
+ * @see #getY()
+ */
+ public static final int ACTION_DROP = 3;
+
+ /**
+ * Action constant returned by {@link #getAction()}: Signals to a View that the drag and drop
+ * operation has concluded. A View that changed its appearance during the operation should
+ * return to its usual drawing state in response to this event.
+ * <p>
+ * All views with listeners that returned boolean <code>true</code> for the ACTION_DRAG_STARTED
+ * event will receive the ACTION_DRAG_ENDED event even if they are not currently visible when
+ * the drag ends. Views removed during the drag operation won't receive the ACTION_DRAG_ENDED
+ * event.
+ * </p>
+ * <p>
+ * The View object can call {@link #getResult()} to see the result of the operation.
+ * If a View returned {@code true} in response to {@link #ACTION_DROP}, then
+ * getResult() returns {@code true}, otherwise it returns {@code false}.
+ * </p>
+ * @see #ACTION_DRAG_STARTED
+ * @see #getResult()
+ */
+ public static final int ACTION_DRAG_ENDED = 4;
+
+ /**
+ * Action constant returned by {@link #getAction()}: Signals to a View that the drag point has
+ * entered the bounding box of the View.
+ * <p>
+ * If the View can accept a drop, it can react to ACTION_DRAG_ENTERED
+ * by changing its appearance in a way that tells the user that the View is the current
+ * drop target.
+ * </p>
+ * The system stops sending ACTION_DRAG_LOCATION events to a View once the user moves the
+ * drag shadow out of the View object's bounding box or into a descendant view that can accept
+ * the data. If the user moves the drag shadow back into the View object's bounding box or out
+ * of a descendant view that can accept the data, the View receives an ACTION_DRAG_ENTERED again
+ * before receiving any more ACTION_DRAG_LOCATION events.
+ * </p>
+ * @see #ACTION_DRAG_ENTERED
+ * @see #ACTION_DRAG_LOCATION
+ */
+ public static final int ACTION_DRAG_ENTERED = 5;
+
+ /**
+ * Action constant returned by {@link #getAction()}: Signals that the user has moved the
+ * drag shadow out of the bounding box of the View or into a descendant view that can accept
+ * the data.
+ * The View can react by changing its appearance in a way that tells the user that
+ * View is no longer the immediate drop target.
+ * <p>
+ * After the system sends an ACTION_DRAG_EXITED event to the View, the View receives no more
+ * ACTION_DRAG_LOCATION events until the user drags the drag shadow back over the View.
+ * </p>
+ *
+ */
+ public static final int ACTION_DRAG_EXITED = 6;
+
+ private DragEvent() {
+ }
+
+ private void init(int action, float x, float y, float offsetX, float offsetY,
+ ClipDescription description, ClipData data, SurfaceControl dragSurface,
+ IDragAndDropPermissions dragAndDropPermissions, Object localState, boolean result) {
+ mAction = action;
+ mX = x;
+ mY = y;
+ mOffsetX = offsetX;
+ mOffsetY = offsetY;
+ mClipDescription = description;
+ mClipData = data;
+ mDragSurface = dragSurface;
+ mDragAndDropPermissions = dragAndDropPermissions;
+ mLocalState = localState;
+ mDragResult = result;
+ }
+
+ static DragEvent obtain() {
+ return DragEvent.obtain(0, 0f, 0f, 0f, 0f, null, null, null, null, null, false);
+ }
+
+ /** @hide */
+ public static DragEvent obtain(int action, float x, float y, float offsetX, float offsetY,
+ Object localState, ClipDescription description, ClipData data,
+ SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions,
+ boolean result) {
+ final DragEvent ev;
+ synchronized (gRecyclerLock) {
+ if (gRecyclerTop == null) {
+ ev = new DragEvent();
+ ev.init(action, x, y, offsetX, offsetY, description, data, dragSurface,
+ dragAndDropPermissions, localState, result);
+ return ev;
+ }
+ ev = gRecyclerTop;
+ gRecyclerTop = ev.mNext;
+ gRecyclerUsed -= 1;
+ }
+ ev.mRecycledLocation = null;
+ ev.mRecycled = false;
+ ev.mNext = null;
+
+ ev.init(action, x, y, offsetX, offsetY, description, data, dragSurface,
+ dragAndDropPermissions, localState, result);
+
+ return ev;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static DragEvent obtain(DragEvent source) {
+ return obtain(source.mAction, source.mX, source.mY, source.mOffsetX, source.mOffsetY,
+ source.mLocalState, source.mClipDescription, source.mClipData, source.mDragSurface,
+ source.mDragAndDropPermissions, source.mDragResult);
+ }
+
+ /**
+ * Inspect the action value of this event.
+ * @return One of the following action constants, in the order in which they usually occur
+ * during a drag and drop operation:
+ * <ul>
+ * <li>{@link #ACTION_DRAG_STARTED}</li>
+ * <li>{@link #ACTION_DRAG_ENTERED}</li>
+ * <li>{@link #ACTION_DRAG_LOCATION}</li>
+ * <li>{@link #ACTION_DROP}</li>
+ * <li>{@link #ACTION_DRAG_EXITED}</li>
+ * <li>{@link #ACTION_DRAG_ENDED}</li>
+ * </ul>
+ */
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Gets the X coordinate of the drag point. The value is only valid if the event action is
+ * {@link #ACTION_DRAG_STARTED}, {@link #ACTION_DRAG_LOCATION} or {@link #ACTION_DROP}.
+ * @return The current drag point's X coordinate
+ */
+ public float getX() {
+ return mX;
+ }
+
+ /**
+ * Gets the Y coordinate of the drag point. The value is only valid if the event action is
+ * {@link #ACTION_DRAG_STARTED}, {@link #ACTION_DRAG_LOCATION} or {@link #ACTION_DROP}.
+ * @return The current drag point's Y coordinate
+ */
+ public float getY() {
+ return mY;
+ }
+
+ /** @hide */
+ public float getOffsetX() {
+ return mOffsetX;
+ }
+
+ /** @hide */
+ public float getOffsetY() {
+ return mOffsetY;
+ }
+
+ /**
+ * Returns the {@link android.content.ClipData} object sent to the system as part of the call
+ * to
+ * {@link android.view.View#startDragAndDrop(ClipData,View.DragShadowBuilder,Object,int)
+ * startDragAndDrop()}.
+ * This method only returns valid data if the event action is {@link #ACTION_DROP}.
+ * @return The ClipData sent to the system by startDragAndDrop().
+ */
+ public ClipData getClipData() {
+ return mClipData;
+ }
+
+ /**
+ * Returns the {@link android.content.ClipDescription} object contained in the
+ * {@link android.content.ClipData} object sent to the system as part of the call to
+ * {@link android.view.View#startDragAndDrop(ClipData,View.DragShadowBuilder,Object,int)
+ * startDragAndDrop()}.
+ * The drag handler or listener for a View can use the metadata in this object to decide if the
+ * View can accept the dragged View object's data.
+ * <p>
+ * This method returns valid data for all event actions except for {@link #ACTION_DRAG_ENDED}.
+ * @return The ClipDescription that was part of the ClipData sent to the system by
+ * startDragAndDrop().
+ */
+ public ClipDescription getClipDescription() {
+ return mClipDescription;
+ }
+
+ /** @hide */
+ public SurfaceControl getDragSurface() {
+ return mDragSurface;
+ }
+
+ /** @hide */
+ public IDragAndDropPermissions getDragAndDropPermissions() {
+ return mDragAndDropPermissions;
+ }
+
+ /**
+ * Returns the local state object sent to the system as part of the call to
+ * {@link android.view.View#startDragAndDrop(ClipData,View.DragShadowBuilder,Object,int)
+ * startDragAndDrop()}.
+ * The object is intended to provide local information about the drag and drop operation. For
+ * example, it can indicate whether the drag and drop operation is a copy or a move.
+ * <p>
+ * The local state is available only to views in the activity which has started the drag
+ * operation. In all other activities this method will return null
+ * </p>
+ * <p>
+ * This method returns valid data for all event actions.
+ * </p>
+ * @return The local state object sent to the system by startDragAndDrop().
+ */
+ public Object getLocalState() {
+ return mLocalState;
+ }
+
+ /**
+ * <p>
+ * Returns an indication of the result of the drag and drop operation.
+ * This method only returns valid data if the action type is {@link #ACTION_DRAG_ENDED}.
+ * The return value depends on what happens after the user releases the drag shadow.
+ * </p>
+ * <p>
+ * If the user releases the drag shadow on a View that can accept a drop, the system sends an
+ * {@link #ACTION_DROP} event to the View object's drag event listener. If the listener
+ * returns {@code true}, then getResult() will return {@code true}.
+ * If the listener returns {@code false}, then getResult() returns {@code false}.
+ * </p>
+ * <p>
+ * Notice that getResult() also returns {@code false} if no {@link #ACTION_DROP} is sent. This
+ * happens, for example, when the user releases the drag shadow over an area outside of the
+ * application. In this case, the system sends out {@link #ACTION_DRAG_ENDED} for the current
+ * operation, but never sends out {@link #ACTION_DROP}.
+ * </p>
+ * @return {@code true} if a drag event listener returned {@code true} in response to
+ * {@link #ACTION_DROP}. If the system did not send {@link #ACTION_DROP} before
+ * {@link #ACTION_DRAG_ENDED}, or if the listener returned {@code false} in response to
+ * {@link #ACTION_DROP}, then {@code false} is returned.
+ */
+ public boolean getResult() {
+ return mDragResult;
+ }
+
+ /**
+ * Recycle the DragEvent, to be re-used by a later caller. After calling
+ * this function you must never touch the event again.
+ *
+ * @hide
+ */
+ public final void recycle() {
+ // Ensure recycle is only called once!
+ if (TRACK_RECYCLED_LOCATION) {
+ if (mRecycledLocation != null) {
+ throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation);
+ }
+ mRecycledLocation = new RuntimeException("Last recycled here");
+ } else {
+ if (mRecycled) {
+ throw new RuntimeException(toString() + " recycled twice!");
+ }
+ mRecycled = true;
+ }
+
+ mClipData = null;
+ mClipDescription = null;
+ mLocalState = null;
+ mEventHandlerWasCalled = false;
+
+ synchronized (gRecyclerLock) {
+ if (gRecyclerUsed < MAX_RECYCLED) {
+ gRecyclerUsed++;
+ mNext = gRecyclerTop;
+ gRecyclerTop = this;
+ }
+ }
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified unmasked action
+ * such as "ACTION_DRAG_START", "ACTION_DRAG_END" or an equivalent numeric constant
+ * such as "35" if unknown.
+ *
+ * @param action The action.
+ * @return The symbolic name of the specified action.
+ * @see #getAction()
+ * @hide
+ */
+ public static String actionToString(int action) {
+ switch (action) {
+ case ACTION_DRAG_STARTED:
+ return "ACTION_DRAG_STARTED";
+ case ACTION_DRAG_LOCATION:
+ return "ACTION_DRAG_LOCATION";
+ case ACTION_DROP:
+ return "ACTION_DROP";
+ case ACTION_DRAG_ENDED:
+ return "ACTION_DRAG_ENDED";
+ case ACTION_DRAG_ENTERED:
+ return "ACTION_DRAG_ENTERED";
+ case ACTION_DRAG_EXITED:
+ return "ACTION_DRAG_EXITED";
+ }
+ return Integer.toString(action);
+ }
+
+ /**
+ * Returns a string containing a concise, human-readable representation of this DragEvent
+ * object.
+ * @return A string representation of the DragEvent object.
+ */
+ @Override
+ public String toString() {
+ return "DragEvent{" + Integer.toHexString(System.identityHashCode(this))
+ + " action=" + mAction + " @ (" + mX + ", " + mY + ") desc=" + mClipDescription
+ + " data=" + mClipData + " local=" + mLocalState + " result=" + mDragResult
+ + "}";
+ }
+
+ /* Parcelable interface */
+
+ /**
+ * Returns information about the {@link android.os.Parcel} representation of this DragEvent
+ * object.
+ * @return Information about the {@link android.os.Parcel} representation.
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Creates a {@link android.os.Parcel} object from this DragEvent object.
+ * @param dest A {@link android.os.Parcel} object in which to put the DragEvent object.
+ * @param flags Flags to store in the Parcel.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mAction);
+ dest.writeFloat(mX);
+ dest.writeFloat(mY);
+ dest.writeFloat(mOffsetX);
+ dest.writeFloat(mOffsetY);
+ dest.writeInt(mDragResult ? 1 : 0);
+ if (mClipData == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mClipData.writeToParcel(dest, flags);
+ }
+ if (mClipDescription == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mClipDescription.writeToParcel(dest, flags);
+ }
+ if (mDragSurface == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mDragSurface.writeToParcel(dest, flags);
+ }
+ if (mDragAndDropPermissions == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ dest.writeStrongBinder(mDragAndDropPermissions.asBinder());
+ }
+ }
+
+ /**
+ * A container for creating a DragEvent from a Parcel.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<DragEvent> CREATOR =
+ new Parcelable.Creator<DragEvent>() {
+ public DragEvent createFromParcel(Parcel in) {
+ DragEvent event = DragEvent.obtain();
+ event.mAction = in.readInt();
+ event.mX = in.readFloat();
+ event.mY = in.readFloat();
+ event.mOffsetX = in.readFloat();
+ event.mOffsetY = in.readFloat();
+ event.mDragResult = (in.readInt() != 0);
+ if (in.readInt() != 0) {
+ event.mClipData = ClipData.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ event.mClipDescription = ClipDescription.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ event.mDragSurface = SurfaceControl.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ event.mDragAndDropPermissions =
+ IDragAndDropPermissions.Stub.asInterface(in.readStrongBinder());;
+ }
+ return event;
+ }
+
+ public DragEvent[] newArray(int size) {
+ return new DragEvent[size];
+ }
+ };
+}
diff --git a/android/view/FallbackEventHandler.java b/android/view/FallbackEventHandler.java
new file mode 100644
index 0000000..8e00d6d
--- /dev/null
+++ b/android/view/FallbackEventHandler.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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;
+
+/**
+ * @hide
+ */
+public interface FallbackEventHandler {
+ public void setView(View v);
+ public void preDispatchKeyEvent(KeyEvent event);
+ public boolean dispatchKeyEvent(KeyEvent event);
+}
diff --git a/android/view/FocusFinder.java b/android/view/FocusFinder.java
new file mode 100644
index 0000000..064bc69
--- /dev/null
+++ b/android/view/FocusFinder.java
@@ -0,0 +1,986 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * The algorithm used for finding the next focusable view in a given direction
+ * from a view that currently has focus.
+ */
+public class FocusFinder {
+
+ private static final ThreadLocal<FocusFinder> tlFocusFinder =
+ new ThreadLocal<FocusFinder>() {
+ @Override
+ protected FocusFinder initialValue() {
+ return new FocusFinder();
+ }
+ };
+
+ /**
+ * Get the focus finder for this thread.
+ */
+ public static FocusFinder getInstance() {
+ return tlFocusFinder.get();
+ }
+
+ final Rect mFocusedRect = new Rect();
+ final Rect mOtherRect = new Rect();
+ final Rect mBestCandidateRect = new Rect();
+ private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator =
+ new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextFocusForwardId())
+ ? v.findUserSetNextFocus(r, View.FOCUS_FORWARD) : null);
+ private final UserSpecifiedFocusComparator mUserSpecifiedClusterComparator =
+ new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextClusterForwardId())
+ ? v.findUserSetNextKeyboardNavigationCluster(r, View.FOCUS_FORWARD) : null);
+ private final FocusSorter mFocusSorter = new FocusSorter();
+
+ private final ArrayList<View> mTempList = new ArrayList<View>();
+
+ // enforce thread local access
+ private FocusFinder() {}
+
+ /**
+ * Find the next view to take focus in root's descendants, starting from the view
+ * that currently is focused.
+ * @param root Contains focused. Cannot be null.
+ * @param focused Has focus now.
+ * @param direction Direction to look.
+ * @return The next focusable view, or null if none exists.
+ */
+ public final View findNextFocus(ViewGroup root, View focused, int direction) {
+ return findNextFocus(root, focused, null, direction);
+ }
+
+ /**
+ * Find the next view to take focus in root's descendants, searching from
+ * a particular rectangle in root's coordinates.
+ * @param root Contains focusedRect. Cannot be null.
+ * @param focusedRect The starting point of the search.
+ * @param direction Direction to look.
+ * @return The next focusable view, or null if none exists.
+ */
+ public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
+ mFocusedRect.set(focusedRect);
+ return findNextFocus(root, null, mFocusedRect, direction);
+ }
+
+ private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
+ View next = null;
+ ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
+ if (focused != null) {
+ next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
+ }
+ if (next != null) {
+ return next;
+ }
+ ArrayList<View> focusables = mTempList;
+ try {
+ focusables.clear();
+ effectiveRoot.addFocusables(focusables, direction);
+ if (!focusables.isEmpty()) {
+ next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
+ }
+ } finally {
+ focusables.clear();
+ }
+ return next;
+ }
+
+ /**
+ * Returns the "effective" root of a view. The "effective" root is the closest ancestor
+ * within-which focus should cycle.
+ * <p>
+ * For example: normal focus navigation would stay within a ViewGroup marked as
+ * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out.
+ * @return the "effective" root of {@param focused}
+ */
+ private ViewGroup getEffectiveRoot(ViewGroup root, View focused) {
+ if (focused == null || focused == root) {
+ return root;
+ }
+ ViewGroup effective = null;
+ ViewParent nextParent = focused.getParent();
+ do {
+ if (nextParent == root) {
+ return effective != null ? effective : root;
+ }
+ ViewGroup vg = (ViewGroup) nextParent;
+ if (vg.getTouchscreenBlocksFocus()
+ && focused.getContext().getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TOUCHSCREEN)
+ && vg.isKeyboardNavigationCluster()) {
+ // Don't stop and return here because the cluster could be nested and we only
+ // care about the top-most one.
+ effective = vg;
+ }
+ nextParent = nextParent.getParent();
+ } while (nextParent instanceof ViewGroup);
+ return root;
+ }
+
+ /**
+ * Find the root of the next keyboard navigation cluster after the current one.
+ * @param root The view tree to look inside. Cannot be null
+ * @param currentCluster The starting point of the search. Null means the default cluster
+ * @param direction Direction to look
+ * @return The next cluster, or null if none exists
+ */
+ public View findNextKeyboardNavigationCluster(
+ @NonNull View root,
+ @Nullable View currentCluster,
+ @View.FocusDirection int direction) {
+ View next = null;
+ if (currentCluster != null) {
+ next = findNextUserSpecifiedKeyboardNavigationCluster(root, currentCluster, direction);
+ if (next != null) {
+ return next;
+ }
+ }
+
+ final ArrayList<View> clusters = mTempList;
+ try {
+ clusters.clear();
+ root.addKeyboardNavigationClusters(clusters, direction);
+ if (!clusters.isEmpty()) {
+ next = findNextKeyboardNavigationCluster(
+ root, currentCluster, clusters, direction);
+ }
+ } finally {
+ clusters.clear();
+ }
+ return next;
+ }
+
+ private View findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster,
+ int direction) {
+ View userSetNextCluster =
+ currentCluster.findUserSetNextKeyboardNavigationCluster(root, direction);
+ if (userSetNextCluster != null && userSetNextCluster.hasFocusable()) {
+ return userSetNextCluster;
+ }
+ return null;
+ }
+
+ private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
+ // check for user specified next focus
+ View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
+ View cycleCheck = userSetNextFocus;
+ boolean cycleStep = true; // we want the first toggle to yield false
+ while (userSetNextFocus != null) {
+ if (userSetNextFocus.isFocusable()
+ && userSetNextFocus.getVisibility() == View.VISIBLE
+ && (!userSetNextFocus.isInTouchMode()
+ || userSetNextFocus.isFocusableInTouchMode())) {
+ return userSetNextFocus;
+ }
+ userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
+ if (cycleStep = !cycleStep) {
+ cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
+ if (cycleCheck == userSetNextFocus) {
+ // found a cycle, user-specified focus forms a loop and none of the views
+ // are currently focusable.
+ break;
+ }
+ }
+ }
+ return null;
+ }
+
+ private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
+ int direction, ArrayList<View> focusables) {
+ if (focused != null) {
+ if (focusedRect == null) {
+ focusedRect = mFocusedRect;
+ }
+ // fill in interesting rect from focused
+ focused.getFocusedRect(focusedRect);
+ root.offsetDescendantRectToMyCoords(focused, focusedRect);
+ } else {
+ if (focusedRect == null) {
+ focusedRect = mFocusedRect;
+ // make up a rect at top left or bottom right of root
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ case View.FOCUS_DOWN:
+ setFocusTopLeft(root, focusedRect);
+ break;
+ case View.FOCUS_FORWARD:
+ if (root.isLayoutRtl()) {
+ setFocusBottomRight(root, focusedRect);
+ } else {
+ setFocusTopLeft(root, focusedRect);
+ }
+ break;
+
+ case View.FOCUS_LEFT:
+ case View.FOCUS_UP:
+ setFocusBottomRight(root, focusedRect);
+ break;
+ case View.FOCUS_BACKWARD:
+ if (root.isLayoutRtl()) {
+ setFocusTopLeft(root, focusedRect);
+ } else {
+ setFocusBottomRight(root, focusedRect);
+ break;
+ }
+ }
+ }
+ }
+
+ switch (direction) {
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_BACKWARD:
+ return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
+ direction);
+ case View.FOCUS_UP:
+ case View.FOCUS_DOWN:
+ case View.FOCUS_LEFT:
+ case View.FOCUS_RIGHT:
+ return findNextFocusInAbsoluteDirection(focusables, root, focused,
+ focusedRect, direction);
+ default:
+ throw new IllegalArgumentException("Unknown direction: " + direction);
+ }
+ }
+
+ private View findNextKeyboardNavigationCluster(
+ View root,
+ View currentCluster,
+ List<View> clusters,
+ @View.FocusDirection int direction) {
+ try {
+ // Note: This sort is stable.
+ mUserSpecifiedClusterComparator.setFocusables(clusters, root);
+ Collections.sort(clusters, mUserSpecifiedClusterComparator);
+ } finally {
+ mUserSpecifiedClusterComparator.recycle();
+ }
+ final int count = clusters.size();
+
+ switch (direction) {
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_DOWN:
+ case View.FOCUS_RIGHT:
+ return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count);
+ case View.FOCUS_BACKWARD:
+ case View.FOCUS_UP:
+ case View.FOCUS_LEFT:
+ return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count);
+ default:
+ throw new IllegalArgumentException("Unknown direction: " + direction);
+ }
+ }
+
+ private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
+ View focused, Rect focusedRect, int direction) {
+ try {
+ // Note: This sort is stable.
+ mUserSpecifiedFocusComparator.setFocusables(focusables, root);
+ Collections.sort(focusables, mUserSpecifiedFocusComparator);
+ } finally {
+ mUserSpecifiedFocusComparator.recycle();
+ }
+
+ final int count = focusables.size();
+ if (count < 2) {
+ return null;
+ }
+ switch (direction) {
+ case View.FOCUS_FORWARD:
+ return getNextFocusable(focused, focusables, count);
+ case View.FOCUS_BACKWARD:
+ return getPreviousFocusable(focused, focusables, count);
+ }
+ return focusables.get(count - 1);
+ }
+
+ private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
+ final int rootBottom = root.getScrollY() + root.getHeight();
+ final int rootRight = root.getScrollX() + root.getWidth();
+ focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
+ }
+
+ private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
+ final int rootTop = root.getScrollY();
+ final int rootLeft = root.getScrollX();
+ focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
+ }
+
+ View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
+ Rect focusedRect, int direction) {
+ // initialize the best candidate to something impossible
+ // (so the first plausible view will become the best choice)
+ mBestCandidateRect.set(focusedRect);
+ switch(direction) {
+ case View.FOCUS_LEFT:
+ mBestCandidateRect.offset(focusedRect.width() + 1, 0);
+ break;
+ case View.FOCUS_RIGHT:
+ mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
+ break;
+ case View.FOCUS_UP:
+ mBestCandidateRect.offset(0, focusedRect.height() + 1);
+ break;
+ case View.FOCUS_DOWN:
+ mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
+ }
+
+ View closest = null;
+
+ int numFocusables = focusables.size();
+ for (int i = 0; i < numFocusables; i++) {
+ View focusable = focusables.get(i);
+
+ // only interested in other non-root views
+ if (focusable == focused || focusable == root) continue;
+
+ // get focus bounds of other view in same coordinate system
+ focusable.getFocusedRect(mOtherRect);
+ root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
+
+ if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
+ mBestCandidateRect.set(mOtherRect);
+ closest = focusable;
+ }
+ }
+ return closest;
+ }
+
+ private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
+ if (count < 2) {
+ return null;
+ }
+ if (focused != null) {
+ int position = focusables.lastIndexOf(focused);
+ if (position >= 0 && position + 1 < count) {
+ return focusables.get(position + 1);
+ }
+ }
+ return focusables.get(0);
+ }
+
+ private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
+ if (count < 2) {
+ return null;
+ }
+ if (focused != null) {
+ int position = focusables.indexOf(focused);
+ if (position > 0) {
+ return focusables.get(position - 1);
+ }
+ }
+ return focusables.get(count - 1);
+ }
+
+ private static View getNextKeyboardNavigationCluster(
+ View root,
+ View currentCluster,
+ List<View> clusters,
+ int count) {
+ if (currentCluster == null) {
+ // The current cluster is the default one.
+ // The next cluster after the default one is the first one.
+ // Note that the caller guarantees that 'clusters' is not empty.
+ return clusters.get(0);
+ }
+
+ final int position = clusters.lastIndexOf(currentCluster);
+ if (position >= 0 && position + 1 < count) {
+ // Return the next non-default cluster if we can find it.
+ return clusters.get(position + 1);
+ }
+
+ // The current cluster is the last one. The next one is the default one, i.e. the
+ // root.
+ return root;
+ }
+
+ private static View getPreviousKeyboardNavigationCluster(
+ View root,
+ View currentCluster,
+ List<View> clusters,
+ int count) {
+ if (currentCluster == null) {
+ // The current cluster is the default one.
+ // The previous cluster before the default one is the last one.
+ // Note that the caller guarantees that 'clusters' is not empty.
+ return clusters.get(count - 1);
+ }
+
+ final int position = clusters.indexOf(currentCluster);
+ if (position > 0) {
+ // Return the previous non-default cluster if we can find it.
+ return clusters.get(position - 1);
+ }
+
+ // The current cluster is the first one. The previous one is the default one, i.e.
+ // the root.
+ return root;
+ }
+
+ /**
+ * Is rect1 a better candidate than rect2 for a focus search in a particular
+ * direction from a source rect? This is the core routine that determines
+ * the order of focus searching.
+ * @param direction the direction (up, down, left, right)
+ * @param source The source we are searching from
+ * @param rect1 The candidate rectangle
+ * @param rect2 The current best candidate.
+ * @return Whether the candidate is the new best.
+ */
+ boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
+
+ // to be a better candidate, need to at least be a candidate in the first
+ // place :)
+ if (!isCandidate(source, rect1, direction)) {
+ return false;
+ }
+
+ // we know that rect1 is a candidate.. if rect2 is not a candidate,
+ // rect1 is better
+ if (!isCandidate(source, rect2, direction)) {
+ return true;
+ }
+
+ // if rect1 is better by beam, it wins
+ if (beamBeats(direction, source, rect1, rect2)) {
+ return true;
+ }
+
+ // if rect2 is better, then rect1 cant' be :)
+ if (beamBeats(direction, source, rect2, rect1)) {
+ return false;
+ }
+
+ // otherwise, do fudge-tastic comparison of the major and minor axis
+ return (getWeightedDistanceFor(
+ majorAxisDistance(direction, source, rect1),
+ minorAxisDistance(direction, source, rect1))
+ < getWeightedDistanceFor(
+ majorAxisDistance(direction, source, rect2),
+ minorAxisDistance(direction, source, rect2)));
+ }
+
+ /**
+ * One rectangle may be another candidate than another by virtue of being
+ * exclusively in the beam of the source rect.
+ * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's
+ * beam
+ */
+ boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
+ final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
+ final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
+
+ // if rect1 isn't exclusively in the src beam, it doesn't win
+ if (rect2InSrcBeam || !rect1InSrcBeam) {
+ return false;
+ }
+
+ // we know rect1 is in the beam, and rect2 is not
+
+ // if rect1 is to the direction of, and rect2 is not, rect1 wins.
+ // for example, for direction left, if rect1 is to the left of the source
+ // and rect2 is below, then we always prefer the in beam rect1, since rect2
+ // could be reached by going down.
+ if (!isToDirectionOf(direction, source, rect2)) {
+ return true;
+ }
+
+ // for horizontal directions, being exclusively in beam always wins
+ if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
+ return true;
+ }
+
+ // for vertical directions, beams only beat up to a point:
+ // now, as long as rect2 isn't completely closer, rect1 wins
+ // e.g for direction down, completely closer means for rect2's top
+ // edge to be closer to the source's top edge than rect1's bottom edge.
+ return (majorAxisDistance(direction, source, rect1)
+ < majorAxisDistanceToFarEdge(direction, source, rect2));
+ }
+
+ /**
+ * Fudge-factor opportunity: how to calculate distance given major and minor
+ * axis distances. Warning: this fudge factor is finely tuned, be sure to
+ * run all focus tests if you dare tweak it.
+ */
+ long getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) {
+ return 13 * majorAxisDistance * majorAxisDistance
+ + minorAxisDistance * minorAxisDistance;
+ }
+
+ /**
+ * Is destRect a candidate for the next focus given the direction? This
+ * checks whether the dest is at least partially to the direction of (e.g left of)
+ * from source.
+ *
+ * Includes an edge case for an empty rect (which is used in some cases when
+ * searching from a point on the screen).
+ */
+ boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
+ && srcRect.left > destRect.left;
+ case View.FOCUS_RIGHT:
+ return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
+ && srcRect.right < destRect.right;
+ case View.FOCUS_UP:
+ return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
+ && srcRect.top > destRect.top;
+ case View.FOCUS_DOWN:
+ return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
+ && srcRect.bottom < destRect.bottom;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+
+ /**
+ * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
+ * @param direction the direction (up, down, left, right)
+ * @param rect1 The first rectangle
+ * @param rect2 The second rectangle
+ * @return whether the beams overlap
+ */
+ boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ case View.FOCUS_RIGHT:
+ return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom);
+ case View.FOCUS_UP:
+ case View.FOCUS_DOWN:
+ return (rect2.right > rect1.left) && (rect2.left < rect1.right);
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * e.g for left, is 'to left of'
+ */
+ boolean isToDirectionOf(int direction, Rect src, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return src.left >= dest.right;
+ case View.FOCUS_RIGHT:
+ return src.right <= dest.left;
+ case View.FOCUS_UP:
+ return src.top >= dest.bottom;
+ case View.FOCUS_DOWN:
+ return src.bottom <= dest.top;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * @return The distance from the edge furthest in the given direction
+ * of source to the edge nearest in the given direction of dest. If the
+ * dest is not in the direction from source, return 0.
+ */
+ static int majorAxisDistance(int direction, Rect source, Rect dest) {
+ return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
+ }
+
+ static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return source.left - dest.right;
+ case View.FOCUS_RIGHT:
+ return dest.left - source.right;
+ case View.FOCUS_UP:
+ return source.top - dest.bottom;
+ case View.FOCUS_DOWN:
+ return dest.top - source.bottom;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * @return The distance along the major axis w.r.t the direction from the
+ * edge of source to the far edge of dest. If the
+ * dest is not in the direction from source, return 1 (to break ties with
+ * {@link #majorAxisDistance}).
+ */
+ static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
+ return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
+ }
+
+ static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return source.left - dest.left;
+ case View.FOCUS_RIGHT:
+ return dest.right - source.right;
+ case View.FOCUS_UP:
+ return source.top - dest.top;
+ case View.FOCUS_DOWN:
+ return dest.bottom - source.bottom;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * Find the distance on the minor axis w.r.t the direction to the nearest
+ * edge of the destination rectangle.
+ * @param direction the direction (up, down, left, right)
+ * @param source The source rect.
+ * @param dest The destination rect.
+ * @return The distance.
+ */
+ static int minorAxisDistance(int direction, Rect source, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ case View.FOCUS_RIGHT:
+ // the distance between the center verticals
+ return Math.abs(
+ ((source.top + source.height() / 2) -
+ ((dest.top + dest.height() / 2))));
+ case View.FOCUS_UP:
+ case View.FOCUS_DOWN:
+ // the distance between the center horizontals
+ return Math.abs(
+ ((source.left + source.width() / 2) -
+ ((dest.left + dest.width() / 2))));
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * Find the nearest touchable view to the specified view.
+ *
+ * @param root The root of the tree in which to search
+ * @param x X coordinate from which to start the search
+ * @param y Y coordinate from which to start the search
+ * @param direction Direction to look
+ * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array
+ * may already be populated with values.
+ * @return The nearest touchable view, or null if none exists.
+ */
+ public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) {
+ ArrayList<View> touchables = root.getTouchables();
+ int minDistance = Integer.MAX_VALUE;
+ View closest = null;
+
+ int numTouchables = touchables.size();
+
+ int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop();
+
+ Rect closestBounds = new Rect();
+ Rect touchableBounds = mOtherRect;
+
+ for (int i = 0; i < numTouchables; i++) {
+ View touchable = touchables.get(i);
+
+ // get visible bounds of other view in same coordinate system
+ touchable.getDrawingRect(touchableBounds);
+
+ root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true);
+
+ if (!isTouchCandidate(x, y, touchableBounds, direction)) {
+ continue;
+ }
+
+ int distance = Integer.MAX_VALUE;
+
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ distance = x - touchableBounds.right + 1;
+ break;
+ case View.FOCUS_RIGHT:
+ distance = touchableBounds.left;
+ break;
+ case View.FOCUS_UP:
+ distance = y - touchableBounds.bottom + 1;
+ break;
+ case View.FOCUS_DOWN:
+ distance = touchableBounds.top;
+ break;
+ }
+
+ if (distance < edgeSlop) {
+ // Give preference to innermost views
+ if (closest == null ||
+ closestBounds.contains(touchableBounds) ||
+ (!touchableBounds.contains(closestBounds) && distance < minDistance)) {
+ minDistance = distance;
+ closest = touchable;
+ closestBounds.set(touchableBounds);
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ deltas[0] = -distance;
+ break;
+ case View.FOCUS_RIGHT:
+ deltas[0] = distance;
+ break;
+ case View.FOCUS_UP:
+ deltas[1] = -distance;
+ break;
+ case View.FOCUS_DOWN:
+ deltas[1] = distance;
+ break;
+ }
+ }
+ }
+ }
+ return closest;
+ }
+
+
+ /**
+ * Is destRect a candidate for the next touch given the direction?
+ */
+ private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return destRect.left <= x && destRect.top <= y && y <= destRect.bottom;
+ case View.FOCUS_RIGHT:
+ return destRect.left >= x && destRect.top <= y && y <= destRect.bottom;
+ case View.FOCUS_UP:
+ return destRect.top <= y && destRect.left <= x && x <= destRect.right;
+ case View.FOCUS_DOWN:
+ return destRect.top >= y && destRect.left <= x && x <= destRect.right;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ private static final boolean isValidId(final int id) {
+ return id != 0 && id != View.NO_ID;
+ }
+
+ static final class FocusSorter {
+ private ArrayList<Rect> mRectPool = new ArrayList<>();
+ private int mLastPoolRect;
+ private int mRtlMult;
+ private HashMap<View, Rect> mRectByView = null;
+
+ private Comparator<View> mTopsComparator = (first, second) -> {
+ if (first == second) {
+ return 0;
+ }
+
+ Rect firstRect = mRectByView.get(first);
+ Rect secondRect = mRectByView.get(second);
+
+ int result = firstRect.top - secondRect.top;
+ if (result == 0) {
+ return firstRect.bottom - secondRect.bottom;
+ }
+ return result;
+ };
+
+ private Comparator<View> mSidesComparator = (first, second) -> {
+ if (first == second) {
+ return 0;
+ }
+
+ Rect firstRect = mRectByView.get(first);
+ Rect secondRect = mRectByView.get(second);
+
+ int result = firstRect.left - secondRect.left;
+ if (result == 0) {
+ return firstRect.right - secondRect.right;
+ }
+ return mRtlMult * result;
+ };
+
+ public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
+ int count = end - start;
+ if (count < 2) {
+ return;
+ }
+ if (mRectByView == null) {
+ mRectByView = new HashMap<>();
+ }
+ mRtlMult = isRtl ? -1 : 1;
+ for (int i = mRectPool.size(); i < count; ++i) {
+ mRectPool.add(new Rect());
+ }
+ for (int i = start; i < end; ++i) {
+ Rect next = mRectPool.get(mLastPoolRect++);
+ views[i].getDrawingRect(next);
+ root.offsetDescendantRectToMyCoords(views[i], next);
+ mRectByView.put(views[i], next);
+ }
+
+ // Sort top-to-bottom
+ Arrays.sort(views, start, count, mTopsComparator);
+ // Sweep top-to-bottom to identify rows
+ int sweepBottom = mRectByView.get(views[start]).bottom;
+ int rowStart = start;
+ int sweepIdx = start + 1;
+ for (; sweepIdx < end; ++sweepIdx) {
+ Rect currRect = mRectByView.get(views[sweepIdx]);
+ if (currRect.top >= sweepBottom) {
+ // Next view is on a new row, sort the row we've just finished left-to-right.
+ if ((sweepIdx - rowStart) > 1) {
+ Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
+ }
+ sweepBottom = currRect.bottom;
+ rowStart = sweepIdx;
+ } else {
+ // Next view vertically overlaps, we need to extend our "row height"
+ sweepBottom = Math.max(sweepBottom, currRect.bottom);
+ }
+ }
+ // Sort whatever's left (final row) left-to-right
+ if ((sweepIdx - rowStart) > 1) {
+ Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
+ }
+
+ mLastPoolRect = 0;
+ mRectByView.clear();
+ }
+ }
+
+ /**
+ * Public for testing.
+ *
+ * @hide
+ */
+ @TestApi
+ public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
+ getInstance().mFocusSorter.sort(views, start, end, root, isRtl);
+ }
+
+ /**
+ * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
+ * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
+ */
+ private static final class UserSpecifiedFocusComparator implements Comparator<View> {
+ private final ArrayMap<View, View> mNextFoci = new ArrayMap<>();
+ private final ArraySet<View> mIsConnectedTo = new ArraySet<>();
+ private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>();
+ private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>();
+ private final NextFocusGetter mNextFocusGetter;
+ private View mRoot;
+
+ public interface NextFocusGetter {
+ View get(View root, View view);
+ }
+
+ UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter) {
+ mNextFocusGetter = nextFocusGetter;
+ }
+
+ public void recycle() {
+ mRoot = null;
+ mHeadsOfChains.clear();
+ mIsConnectedTo.clear();
+ mOriginalOrdinal.clear();
+ mNextFoci.clear();
+ }
+
+ public void setFocusables(List<View> focusables, View root) {
+ mRoot = root;
+ for (int i = 0; i < focusables.size(); ++i) {
+ mOriginalOrdinal.put(focusables.get(i), i);
+ }
+
+ for (int i = focusables.size() - 1; i >= 0; i--) {
+ final View view = focusables.get(i);
+ final View next = mNextFocusGetter.get(mRoot, view);
+ if (next != null && mOriginalOrdinal.containsKey(next)) {
+ mNextFoci.put(view, next);
+ mIsConnectedTo.add(next);
+ }
+ }
+
+ for (int i = focusables.size() - 1; i >= 0; i--) {
+ final View view = focusables.get(i);
+ final View next = mNextFoci.get(view);
+ if (next != null && !mIsConnectedTo.contains(view)) {
+ setHeadOfChain(view);
+ }
+ }
+ }
+
+ private void setHeadOfChain(View head) {
+ for (View view = head; view != null; view = mNextFoci.get(view)) {
+ final View otherHead = mHeadsOfChains.get(view);
+ if (otherHead != null) {
+ if (otherHead == head) {
+ return; // This view has already had its head set properly
+ }
+ // A hydra -- multi-headed focus chain (e.g. A->C and B->C)
+ // Use the one we've already chosen instead and reset this chain.
+ view = head;
+ head = otherHead;
+ }
+ mHeadsOfChains.put(view, head);
+ }
+ }
+
+ public int compare(View first, View second) {
+ if (first == second) {
+ return 0;
+ }
+ // Order between views within a chain is immaterial -- next/previous is
+ // within a chain is handled elsewhere.
+ View firstHead = mHeadsOfChains.get(first);
+ View secondHead = mHeadsOfChains.get(second);
+ if (firstHead == secondHead && firstHead != null) {
+ if (first == firstHead) {
+ return -1; // first is the head, it should be first
+ } else if (second == firstHead) {
+ return 1; // second is the head, it should be first
+ } else if (mNextFoci.get(first) != null) {
+ return -1; // first is not the end of the chain
+ } else {
+ return 1; // first is end of chain
+ }
+ }
+ boolean involvesChain = false;
+ if (firstHead != null) {
+ first = firstHead;
+ involvesChain = true;
+ }
+ if (secondHead != null) {
+ second = secondHead;
+ involvesChain = true;
+ }
+
+ if (involvesChain) {
+ // keep original order between chains
+ return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+}
diff --git a/android/view/FocusFinderHelper.java b/android/view/FocusFinderHelper.java
new file mode 100644
index 0000000..69dc056
--- /dev/null
+++ b/android/view/FocusFinderHelper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+
+/**
+ * A helper class that allows unit tests to access FocusFinder methods.
+ * @hide
+ */
+public class FocusFinderHelper {
+
+ private FocusFinder mFocusFinder;
+
+ /**
+ * Wrap the FocusFinder object
+ */
+ public FocusFinderHelper(FocusFinder focusFinder) {
+ mFocusFinder = focusFinder;
+ }
+
+ public boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
+ return mFocusFinder.isBetterCandidate(direction, source, rect1, rect2);
+ }
+
+ public boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
+ return mFocusFinder.beamBeats(direction, source, rect1, rect2);
+ }
+
+ public boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
+ return mFocusFinder.isCandidate(srcRect, destRect, direction);
+ }
+
+ public boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
+ return mFocusFinder.beamsOverlap(direction, rect1, rect2);
+ }
+
+ public static int majorAxisDistance(int direction, Rect source, Rect dest) {
+ return FocusFinder.majorAxisDistance(direction, source, dest);
+ }
+
+ public static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
+ return FocusFinder.majorAxisDistanceToFarEdge(direction, source, dest);
+ }
+}
diff --git a/android/view/FrameMetrics.java b/android/view/FrameMetrics.java
new file mode 100644
index 0000000..3cffeb0
--- /dev/null
+++ b/android/view/FrameMetrics.java
@@ -0,0 +1,358 @@
+/*
+ * 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.view;
+
+import static android.graphics.FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED;
+
+import android.annotation.IntDef;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class containing timing data for various milestones in a frame
+ * lifecycle reported by the rendering subsystem.
+ * <p>
+ * Supported metrics can be queried via their corresponding identifier.
+ * </p>
+ */
+public final class FrameMetrics {
+
+ /**
+ * Metric identifier for unknown delay.
+ * <p>
+ * Represents the number of nanoseconds elapsed waiting for the
+ * UI thread to become responsive and process the frame. This
+ * should be 0 most of the time.
+ * </p>
+ */
+ public static final int UNKNOWN_DELAY_DURATION = 0;
+
+ /**
+ * Metric identifier for input handling duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed issuing
+ * input handling callbacks.
+ * </p>
+ */
+ public static final int INPUT_HANDLING_DURATION = 1;
+
+ /**
+ * Metric identifier for animation callback duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed issuing
+ * animation callbacks.
+ * </p>
+ */
+ public static final int ANIMATION_DURATION = 2;
+
+ /**
+ * Metric identifier for layout/measure duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed measuring
+ * and laying out the invalidated pieces of the view hierarchy.
+ * </p>
+ */
+ public static final int LAYOUT_MEASURE_DURATION = 3;
+ /**
+ * Metric identifier for draw duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed computing
+ * DisplayLists for transformations applied to the view
+ * hierarchy.
+ * </p>
+ */
+ public static final int DRAW_DURATION = 4;
+
+ /**
+ * Metric identifier for sync duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed
+ * synchronizing the computed display lists with the render
+ * thread.
+ * </p>
+ */
+ public static final int SYNC_DURATION = 5;
+
+ /**
+ * Metric identifier for command issue duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed
+ * issuing draw commands to the GPU.
+ * </p>
+ */
+ public static final int COMMAND_ISSUE_DURATION = 6;
+
+ /**
+ * Metric identifier for swap buffers duration.
+ * <p>
+ * Represents the number of nanoseconds elapsed issuing
+ * the frame buffer for this frame to the display
+ * subsystem.
+ * </p>
+ */
+ public static final int SWAP_BUFFERS_DURATION = 7;
+
+ /**
+ * Metric identifier for total frame duration.
+ * <p>
+ * Represents the total time in nanoseconds this frame took to render
+ * and be issued to the display subsystem.
+ * </p>
+ * <p>
+ * Equal to the sum of the values of all other time-valued metric
+ * identifiers.
+ * </p>
+ */
+ public static final int TOTAL_DURATION = 8;
+
+ /**
+ * Metric identifier for a boolean value determining whether this frame was
+ * the first to draw in a new Window layout.
+ * <p>
+ * {@link #getMetric(int)} will return 0 for false, 1 for true.
+ * </p>
+ * <p>
+ * First draw frames are expected to be slow and should usually be exempt
+ * from display jank calculations as they do not cause skips in animations
+ * and are usually hidden by window animations or other tricks.
+ * </p>
+ */
+ public static final int FIRST_DRAW_FRAME = 9;
+
+ /**
+ * Metric identifier for the timestamp of the intended vsync for this frame.
+ * <p>
+ * The intended start point for the frame. If this value is different from
+ * {@link #VSYNC_TIMESTAMP}, there was work occurring on the UI thread that
+ * prevented it from responding to the vsync signal in a timely fashion.
+ * </p>
+ */
+ public static final int INTENDED_VSYNC_TIMESTAMP = 10;
+
+ /**
+ * Metric identifier for the timestamp of the actual vsync for this frame.
+ * <p>
+ * The time value that was used in all the vsync listeners and drawing for
+ * the frame (Choreographer frame callbacks, animations,
+ * {@link View#getDrawingTime()}, etc.)
+ * </p>
+ */
+ public static final int VSYNC_TIMESTAMP = 11;
+
+ /**
+ * Metric identifier for GPU duration.
+ * <p>
+ * Represents the total time in nanoseconds this frame took to complete on the GPU.
+ * </p>
+ **/
+ public static final int GPU_DURATION = 12;
+
+ /**
+ * Metric identifier for the total duration that was available to the app to produce a frame.
+ * <p>
+ * Represents the total time in nanoseconds the system allocated for the app to produce its
+ * frame. If FrameMetrics.TOTAL_DURATION < FrameMetrics.DEADLINE, the app hit its intended
+ * deadline and there was no jank visible to the user.
+ * </p>
+ **/
+ public static final int DEADLINE = 13;
+
+ /**
+ * Identifiers for metrics available for each frame.
+ *
+ * {@see #getMetric(int)}
+ * @hide
+ */
+ @IntDef({
+ UNKNOWN_DELAY_DURATION,
+ INPUT_HANDLING_DURATION,
+ ANIMATION_DURATION,
+ LAYOUT_MEASURE_DURATION,
+ DRAW_DURATION,
+ SYNC_DURATION,
+ COMMAND_ISSUE_DURATION,
+ SWAP_BUFFERS_DURATION,
+ TOTAL_DURATION,
+ FIRST_DRAW_FRAME,
+ INTENDED_VSYNC_TIMESTAMP,
+ VSYNC_TIMESTAMP,
+ GPU_DURATION,
+ DEADLINE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Metric {}
+
+ /**
+ * Timestamp indices for frame milestones.
+ *
+ * May change from release to release.
+ *
+ * Must be kept in sync with frameworks/base/libs/hwui/FrameInfo.h.
+ *
+ * @hide
+ */
+ @IntDef ({
+ Index.FLAGS,
+ Index.FRAME_TIMELINE_VSYNC_ID,
+ Index.INTENDED_VSYNC,
+ Index.VSYNC,
+ Index.INPUT_EVENT_ID,
+ Index.HANDLE_INPUT_START,
+ Index.ANIMATION_START,
+ Index.PERFORM_TRAVERSALS_START,
+ Index.DRAW_START,
+ Index.FRAME_DEADLINE,
+ Index.SYNC_QUEUED,
+ Index.SYNC_START,
+ Index.ISSUE_DRAW_COMMANDS_START,
+ Index.SWAP_BUFFERS,
+ Index.FRAME_COMPLETED,
+ Index.DEQUEUE_BUFFER_DURATION,
+ Index.QUEUE_BUFFER_DURATION,
+ Index.GPU_COMPLETED,
+ Index.SWAP_BUFFERS_COMPLETED,
+ Index.DISPLAY_PRESENT_TIME,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Index {
+ int FLAGS = 0;
+ int FRAME_TIMELINE_VSYNC_ID = 1;
+ int INTENDED_VSYNC = 2;
+ int VSYNC = 3;
+ int INPUT_EVENT_ID = 4;
+ int HANDLE_INPUT_START = 5;
+ int ANIMATION_START = 6;
+ int PERFORM_TRAVERSALS_START = 7;
+ int DRAW_START = 8;
+ int FRAME_DEADLINE = 9;
+ int FRAME_START_TIME = 10;
+ int FRAME_INTERVAL = 11;
+ int SYNC_QUEUED = 12;
+ int SYNC_START = 13;
+ int ISSUE_DRAW_COMMANDS_START = 14;
+ int SWAP_BUFFERS = 15;
+ int FRAME_COMPLETED = 16;
+ int DEQUEUE_BUFFER_DURATION = 17;
+ int QUEUE_BUFFER_DURATION = 18;
+ int GPU_COMPLETED = 19;
+ int SWAP_BUFFERS_COMPLETED = 20;
+ int DISPLAY_PRESENT_TIME = 21;
+
+ int FRAME_STATS_COUNT = 22; // must always be last and in sync with
+ // FrameInfoIndex::NumIndexes in libs/hwui/FrameInfo.h
+ }
+
+ /*
+ * Bucket endpoints for each Metric defined above.
+ *
+ * Each defined metric *must* have a corresponding entry
+ * in this list.
+ */
+ private static final int[] DURATIONS = new int[] {
+ // UNKNOWN_DELAY
+ Index.INTENDED_VSYNC, Index.HANDLE_INPUT_START,
+ // INPUT_HANDLING
+ Index.HANDLE_INPUT_START, Index.ANIMATION_START,
+ // ANIMATION
+ Index.ANIMATION_START, Index.PERFORM_TRAVERSALS_START,
+ // LAYOUT_MEASURE
+ Index.PERFORM_TRAVERSALS_START, Index.DRAW_START,
+ // DRAW
+ Index.DRAW_START, Index.SYNC_QUEUED,
+ // SYNC
+ Index.SYNC_START, Index.ISSUE_DRAW_COMMANDS_START,
+ // COMMAND_ISSUE
+ Index.ISSUE_DRAW_COMMANDS_START, Index.SWAP_BUFFERS,
+ // SWAP_BUFFERS
+ Index.SWAP_BUFFERS, Index.SWAP_BUFFERS_COMPLETED,
+ // TOTAL_DURATION
+ Index.INTENDED_VSYNC, Index.FRAME_COMPLETED,
+ // RESERVED for FIRST_DRAW_FRAME
+ 0, 0,
+ // RESERVED forINTENDED_VSYNC_TIMESTAMP
+ 0, 0,
+ // RESERVED VSYNC_TIMESTAMP
+ 0, 0,
+ // GPU_DURATION
+ Index.SWAP_BUFFERS, Index.GPU_COMPLETED,
+ // DEADLINE
+ Index.INTENDED_VSYNC, Index.FRAME_DEADLINE,
+ };
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final long[] mTimingData;
+
+ /**
+ * Constructs a FrameMetrics object as a copy.
+ * <p>
+ * Use this method to copy out metrics reported by
+ * {@link Window.OnFrameMetricsAvailableListener#onFrameMetricsAvailable(
+ * Window, FrameMetrics, int)}
+ * </p>
+ * @param other the FrameMetrics object to copy.
+ */
+ public FrameMetrics(FrameMetrics other) {
+ mTimingData = new long[Index.FRAME_STATS_COUNT];
+ System.arraycopy(other.mTimingData, 0, mTimingData, 0, mTimingData.length);
+ }
+
+ /**
+ * @hide
+ */
+ public FrameMetrics() {
+ mTimingData = new long[Index.FRAME_STATS_COUNT];
+ }
+
+ /**
+ * Retrieves the value associated with Metric identifier {@code id}
+ * for this frame.
+ * <p>
+ * Boolean metrics are represented in [0,1], with 0 corresponding to
+ * false, and 1 corresponding to true.
+ * </p>
+ * @param id the metric to retrieve
+ * @return the value of the metric or -1 if it is not available.
+ */
+ public long getMetric(@Metric int id) {
+ if (id < UNKNOWN_DELAY_DURATION || id > DEADLINE) {
+ return -1;
+ }
+
+ if (mTimingData == null) {
+ return -1;
+ }
+
+ if (id == FIRST_DRAW_FRAME) {
+ return (mTimingData[Index.FLAGS] & FLAG_WINDOW_VISIBILITY_CHANGED) != 0 ? 1 : 0;
+ } else if (id == INTENDED_VSYNC_TIMESTAMP) {
+ return mTimingData[Index.INTENDED_VSYNC];
+ } else if (id == VSYNC_TIMESTAMP) {
+ return mTimingData[Index.VSYNC];
+ }
+
+ int durationsIdx = 2 * id;
+ return mTimingData[DURATIONS[durationsIdx + 1]]
+ - mTimingData[DURATIONS[durationsIdx]];
+ }
+}
+
diff --git a/android/view/FrameMetricsObserver.java b/android/view/FrameMetricsObserver.java
new file mode 100644
index 0000000..35d95be
--- /dev/null
+++ b/android/view/FrameMetricsObserver.java
@@ -0,0 +1,67 @@
+/*
+ * 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.view;
+
+import android.annotation.NonNull;
+import android.graphics.HardwareRendererObserver;
+import android.os.Handler;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides streaming access to frame stats information from the rendering
+ * subsystem to apps.
+ *
+ * @hide
+ */
+public class FrameMetricsObserver
+ implements HardwareRendererObserver.OnFrameMetricsAvailableListener {
+ private final WeakReference<Window> mWindow;
+ private final FrameMetrics mFrameMetrics;
+ private final HardwareRendererObserver mObserver;
+ /*package*/ final Window.OnFrameMetricsAvailableListener mListener;
+
+ /**
+ * Creates a FrameMetricsObserver
+ *
+ * @param handler the Handler to use when invoking callbacks
+ */
+ FrameMetricsObserver(@NonNull Window window, @NonNull Handler handler,
+ @NonNull Window.OnFrameMetricsAvailableListener listener) {
+ mWindow = new WeakReference<>(window);
+ mListener = listener;
+ mFrameMetrics = new FrameMetrics();
+ mObserver = new HardwareRendererObserver(this, mFrameMetrics.mTimingData, handler,
+ false /*waitForPresentTime*/);
+ }
+
+ /**
+ * Implementation of OnFrameMetricsAvailableListener
+ * @param dropCountSinceLastInvocation the number of reports dropped since the last time
+ * @Override
+ */
+ public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
+ if (mWindow.get() != null) {
+ mListener.onFrameMetricsAvailable(mWindow.get(), mFrameMetrics,
+ dropCountSinceLastInvocation);
+ }
+ }
+
+ /*package*/ HardwareRendererObserver getRendererObserver() {
+ return mObserver;
+ }
+}
diff --git a/android/view/FrameStats.java b/android/view/FrameStats.java
new file mode 100644
index 0000000..3fbe6fe
--- /dev/null
+++ b/android/view/FrameStats.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * This is the base class for frame statistics.
+ */
+public abstract class FrameStats {
+ /**
+ * Undefined time.
+ */
+ public static final long UNDEFINED_TIME_NANO = -1;
+
+ /** @hide */
+ protected long mRefreshPeriodNano;
+
+ /** @hide */
+ protected long[] mFramesPresentedTimeNano;
+
+ /**
+ * Gets the refresh period of the display hosting the window(s) for
+ * which these statistics apply.
+ *
+ * @return The refresh period in nanoseconds.
+ */
+ public final long getRefreshPeriodNano() {
+ return mRefreshPeriodNano;
+ }
+
+ /**
+ * Gets the number of frames for which there is data.
+ *
+ * @return The number of frames.
+ */
+ public final int getFrameCount() {
+ return mFramesPresentedTimeNano != null
+ ? mFramesPresentedTimeNano.length : 0;
+ }
+
+ /**
+ * Gets the start time of the interval for which these statistics
+ * apply. The start interval is the time when the first frame was
+ * presented.
+ *
+ * @return The start time in nanoseconds or {@link #UNDEFINED_TIME_NANO}
+ * if there is no frame data.
+ */
+ public final long getStartTimeNano() {
+ if (getFrameCount() <= 0) {
+ return UNDEFINED_TIME_NANO;
+ }
+ return mFramesPresentedTimeNano[0];
+ }
+
+ /**
+ * Gets the end time of the interval for which these statistics
+ * apply. The end interval is the time when the last frame was
+ * presented.
+ *
+ * @return The end time in nanoseconds or {@link #UNDEFINED_TIME_NANO}
+ * if there is no frame data.
+ */
+ public final long getEndTimeNano() {
+ if (getFrameCount() <= 0) {
+ return UNDEFINED_TIME_NANO;
+ }
+ return mFramesPresentedTimeNano[mFramesPresentedTimeNano.length - 1];
+ }
+
+ /**
+ * Get the time a frame at a given index was presented.
+ *
+ * @param index The frame index.
+ * @return The presented time in nanoseconds or {@link #UNDEFINED_TIME_NANO}
+ * if the frame is not presented yet.
+ */
+ public final long getFramePresentedTimeNano(int index) {
+ if (mFramesPresentedTimeNano == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mFramesPresentedTimeNano[index];
+ }
+}
diff --git a/android/view/GestureDetector.java b/android/view/GestureDetector.java
new file mode 100644
index 0000000..63a8300
--- /dev/null
+++ b/android/view/GestureDetector.java
@@ -0,0 +1,910 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
+
+import android.annotation.UiContext;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StrictMode;
+import android.os.SystemClock;
+
+import com.android.internal.util.FrameworkStatsLog;
+
+/**
+ * Detects various gestures and events using the supplied {@link MotionEvent}s.
+ * The {@link OnGestureListener} callback will notify users when a particular
+ * motion event has occurred. This class should only be used with {@link MotionEvent}s
+ * reported via touch (don't use for trackball events).
+ *
+ * To use this class:
+ * <ul>
+ * <li>Create an instance of the {@code GestureDetector} for your {@link View}
+ * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
+ * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
+ * will be executed when the events occur.
+ * <li>If listening for {@link OnContextClickListener#onContextClick(MotionEvent)}
+ * you must call {@link #onGenericMotionEvent(MotionEvent)}
+ * in {@link View#onGenericMotionEvent(MotionEvent)}.
+ * </ul>
+ */
+public class GestureDetector {
+ /**
+ * The listener that is used to notify when gestures occur.
+ * If you want to listen for all the different gestures then implement
+ * this interface. If you only want to listen for a subset it might
+ * be easier to extend {@link SimpleOnGestureListener}.
+ */
+ public interface OnGestureListener {
+
+ /**
+ * Notified when a tap occurs with the down {@link MotionEvent}
+ * that triggered it. This will be triggered immediately for
+ * every down event. All other events should be preceded by this.
+ *
+ * @param e The down motion event.
+ */
+ boolean onDown(MotionEvent e);
+
+ /**
+ * The user has performed a down {@link MotionEvent} and not performed
+ * a move or up yet. This event is commonly used to provide visual
+ * feedback to the user to let them know that their action has been
+ * recognized i.e. highlight an element.
+ *
+ * @param e The down motion event
+ */
+ void onShowPress(MotionEvent e);
+
+ /**
+ * Notified when a tap occurs with the up {@link MotionEvent}
+ * that triggered it.
+ *
+ * @param e The up motion event that completed the first tap
+ * @return true if the event is consumed, else false
+ */
+ boolean onSingleTapUp(MotionEvent e);
+
+ /**
+ * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
+ * current move {@link MotionEvent}. The distance in x and y is also supplied for
+ * convenience.
+ *
+ * @param e1 The first down motion event that started the scrolling.
+ * @param e2 The move motion event that triggered the current onScroll.
+ * @param distanceX The distance along the X axis that has been scrolled since the last
+ * call to onScroll. This is NOT the distance between {@code e1}
+ * and {@code e2}.
+ * @param distanceY The distance along the Y axis that has been scrolled since the last
+ * call to onScroll. This is NOT the distance between {@code e1}
+ * and {@code e2}.
+ * @return true if the event is consumed, else false
+ */
+ boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
+
+ /**
+ * Notified when a long press occurs with the initial on down {@link MotionEvent}
+ * that trigged it.
+ *
+ * @param e The initial on down motion event that started the longpress.
+ */
+ void onLongPress(MotionEvent e);
+
+ /**
+ * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
+ * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
+ * the x and y axis in pixels per second.
+ *
+ * @param e1 The first down motion event that started the fling.
+ * @param e2 The move motion event that triggered the current onFling.
+ * @param velocityX The velocity of this fling measured in pixels per second
+ * along the x axis.
+ * @param velocityY The velocity of this fling measured in pixels per second
+ * along the y axis.
+ * @return true if the event is consumed, else false
+ */
+ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+ }
+
+ /**
+ * The listener that is used to notify when a double-tap or a confirmed
+ * single-tap occur.
+ */
+ public interface OnDoubleTapListener {
+ /**
+ * Notified when a single-tap occurs.
+ * <p>
+ * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
+ * will only be called after the detector is confident that the user's
+ * first tap is not followed by a second tap leading to a double-tap
+ * gesture.
+ *
+ * @param e The down motion event of the single-tap.
+ * @return true if the event is consumed, else false
+ */
+ boolean onSingleTapConfirmed(MotionEvent e);
+
+ /**
+ * Notified when a double-tap occurs. Triggered on the down event of second tap.
+ *
+ * @param e The down motion event of the first tap of the double-tap.
+ * @return true if the event is consumed, else false
+ */
+ boolean onDoubleTap(MotionEvent e);
+
+ /**
+ * Notified when an event within a double-tap gesture occurs, including
+ * the down, move, and up events.
+ *
+ * @param e The motion event that occurred during the double-tap gesture.
+ * @return true if the event is consumed, else false
+ */
+ boolean onDoubleTapEvent(MotionEvent e);
+ }
+
+ /**
+ * The listener that is used to notify when a context click occurs. When listening for a
+ * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
+ * {@link View#onGenericMotionEvent(MotionEvent)}.
+ */
+ public interface OnContextClickListener {
+ /**
+ * Notified when a context click occurs.
+ *
+ * @param e The motion event that occurred during the context click.
+ * @return true if the event is consumed, else false
+ */
+ boolean onContextClick(MotionEvent e);
+ }
+
+ /**
+ * A convenience class to extend when you only want to listen for a subset
+ * of all the gestures. This implements all methods in the
+ * {@link OnGestureListener}, {@link OnDoubleTapListener}, and {@link OnContextClickListener}
+ * but does nothing and return {@code false} for all applicable methods.
+ */
+ public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
+ OnContextClickListener {
+
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+
+ public void onLongPress(MotionEvent e) {
+ }
+
+ public boolean onScroll(MotionEvent e1, MotionEvent e2,
+ float distanceX, float distanceY) {
+ return false;
+ }
+
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return false;
+ }
+
+ public void onShowPress(MotionEvent e) {
+ }
+
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+
+ public boolean onDoubleTap(MotionEvent e) {
+ return false;
+ }
+
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ return false;
+ }
+
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ return false;
+ }
+
+ public boolean onContextClick(MotionEvent e) {
+ return false;
+ }
+ }
+
+ private static final String TAG = GestureDetector.class.getSimpleName();
+ @UnsupportedAppUsage
+ private int mTouchSlopSquare;
+ private int mDoubleTapTouchSlopSquare;
+ private int mDoubleTapSlopSquare;
+ private float mAmbiguousGestureMultiplier;
+ @UnsupportedAppUsage
+ private int mMinimumFlingVelocity;
+ private int mMaximumFlingVelocity;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
+ private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
+ private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
+ private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
+
+ // constants for Message.what used by GestureHandler below
+ private static final int SHOW_PRESS = 1;
+ private static final int LONG_PRESS = 2;
+ private static final int TAP = 3;
+
+ private final Handler mHandler;
+ @UnsupportedAppUsage
+ private final OnGestureListener mListener;
+ private OnDoubleTapListener mDoubleTapListener;
+ private OnContextClickListener mContextClickListener;
+
+ private boolean mStillDown;
+ private boolean mDeferConfirmSingleTap;
+ private boolean mInLongPress;
+ private boolean mInContextClick;
+ @UnsupportedAppUsage
+ private boolean mAlwaysInTapRegion;
+ private boolean mAlwaysInBiggerTapRegion;
+ private boolean mIgnoreNextUpEvent;
+ // Whether a classification has been recorded by statsd for the current event stream. Reset on
+ // ACTION_DOWN.
+ private boolean mHasRecordedClassification;
+
+ private MotionEvent mCurrentDownEvent;
+ private MotionEvent mCurrentMotionEvent;
+ private MotionEvent mPreviousUpEvent;
+
+ /**
+ * True when the user is still touching for the second tap (down, move, and
+ * up events). Can only be true if there is a double tap listener attached.
+ */
+ private boolean mIsDoubleTapping;
+
+ private float mLastFocusX;
+ private float mLastFocusY;
+ private float mDownFocusX;
+ private float mDownFocusY;
+
+ private boolean mIsLongpressEnabled;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+
+ /**
+ * Consistency verifier for debugging purposes.
+ */
+ private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled() ?
+ new InputEventConsistencyVerifier(this, 0) : null;
+
+ private class GestureHandler extends Handler {
+ GestureHandler() {
+ super();
+ }
+
+ GestureHandler(Handler handler) {
+ super(handler.getLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW_PRESS:
+ mListener.onShowPress(mCurrentDownEvent);
+ break;
+
+ case LONG_PRESS:
+ recordGestureClassification(msg.arg1);
+ dispatchLongPress();
+ break;
+
+ case TAP:
+ // If the user's finger is still down, do not count it as a tap
+ if (mDoubleTapListener != null) {
+ if (!mStillDown) {
+ recordGestureClassification(
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
+ mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
+ } else {
+ mDeferConfirmSingleTap = true;
+ }
+ }
+ break;
+
+ default:
+ throw new RuntimeException("Unknown message " + msg); //never
+ }
+ }
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener.
+ * This variant of the constructor should be used from a non-UI thread
+ * (as it allows specifying the Handler).
+ *
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ * @param handler the handler to use
+ *
+ * @throws NullPointerException if either {@code listener} or
+ * {@code handler} is null.
+ *
+ * @deprecated Use {@link #GestureDetector(android.content.Context,
+ * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
+ */
+ @Deprecated
+ public GestureDetector(OnGestureListener listener, Handler handler) {
+ this(null, listener, handler);
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener.
+ * You may only use this constructor from a UI thread (this is the usual situation).
+ * @see android.os.Handler#Handler()
+ *
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ *
+ * @throws NullPointerException if {@code listener} is null.
+ *
+ * @deprecated Use {@link #GestureDetector(android.content.Context,
+ * android.view.GestureDetector.OnGestureListener)} instead.
+ */
+ @Deprecated
+ public GestureDetector(OnGestureListener listener) {
+ this(null, listener, null);
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener.
+ * You may only use this constructor from a {@link android.os.Looper} thread.
+ * @see android.os.Handler#Handler()
+ *
+ * @param context An {@link android.app.Activity} or a {@link Context} created from
+ * {@link Context#createWindowContext(int, Bundle)}
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null. If the listener implements the {@link OnDoubleTapListener} or
+ * {@link OnContextClickListener} then it will also be set as the listener for
+ * these callbacks (for example when using the {@link SimpleOnGestureListener}).
+ *
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ // TODO(b/182007470): Use @ConfigurationContext instead
+ public GestureDetector(@UiContext Context context, OnGestureListener listener) {
+ this(context, listener, null);
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener that runs deferred events on the
+ * thread associated with the supplied {@link android.os.Handler}.
+ * @see android.os.Handler#Handler()
+ *
+ * @param context An {@link android.app.Activity} or a {@link Context} created from
+ * {@link Context#createWindowContext(int, Bundle)}
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null. If the listener implements the {@link OnDoubleTapListener} or
+ * {@link OnContextClickListener} then it will also be set as the listener for
+ * these callbacks (for example when using the {@link SimpleOnGestureListener}).
+ * @param handler the handler to use for running deferred listener events.
+ *
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ public GestureDetector(@UiContext Context context, OnGestureListener listener,
+ Handler handler) {
+ if (handler != null) {
+ mHandler = new GestureHandler(handler);
+ } else {
+ mHandler = new GestureHandler();
+ }
+ mListener = listener;
+ if (listener instanceof OnDoubleTapListener) {
+ setOnDoubleTapListener((OnDoubleTapListener) listener);
+ }
+ if (listener instanceof OnContextClickListener) {
+ setContextClickListener((OnContextClickListener) listener);
+ }
+ init(context);
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener that runs deferred events on the
+ * thread associated with the supplied {@link android.os.Handler}.
+ * @see android.os.Handler#Handler()
+ *
+ * @param context An {@link android.app.Activity} or a {@link Context} created from
+ * {@link Context#createWindowContext(int, Bundle)}
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ * @param handler the handler to use for running deferred listener events.
+ * @param unused currently not used.
+ *
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ public GestureDetector(@UiContext Context context, OnGestureListener listener, Handler handler,
+ boolean unused) {
+ this(context, listener, handler);
+ }
+
+ private void init(@UiContext Context context) {
+ if (mListener == null) {
+ throw new NullPointerException("OnGestureListener must not be null");
+ }
+ mIsLongpressEnabled = true;
+
+ // Fallback to support pre-donuts releases
+ int touchSlop, doubleTapSlop, doubleTapTouchSlop;
+ if (context == null) {
+ //noinspection deprecation
+ touchSlop = ViewConfiguration.getTouchSlop();
+ doubleTapTouchSlop = touchSlop; // Hack rather than adding a hidden method for this
+ doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
+ //noinspection deprecation
+ mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
+ mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
+ mAmbiguousGestureMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier();
+ } else {
+ StrictMode.assertConfigurationContext(context, "GestureDetector#init");
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ touchSlop = configuration.getScaledTouchSlop();
+ doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
+ doubleTapSlop = configuration.getScaledDoubleTapSlop();
+ mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
+ mAmbiguousGestureMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
+ }
+ mTouchSlopSquare = touchSlop * touchSlop;
+ mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
+ mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
+ }
+
+ /**
+ * Sets the listener which will be called for double-tap and related
+ * gestures.
+ *
+ * @param onDoubleTapListener the listener invoked for all the callbacks, or
+ * null to stop listening for double-tap gestures.
+ */
+ public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
+ mDoubleTapListener = onDoubleTapListener;
+ }
+
+ /**
+ * Sets the listener which will be called for context clicks.
+ *
+ * @param onContextClickListener the listener invoked for all the callbacks, or null to stop
+ * listening for context clicks.
+ */
+ public void setContextClickListener(OnContextClickListener onContextClickListener) {
+ mContextClickListener = onContextClickListener;
+ }
+
+ /**
+ * Set whether longpress is enabled, if this is enabled when a user
+ * presses and holds down you get a longpress event and nothing further.
+ * If it's disabled the user can press and hold down and then later
+ * moved their finger and you will get scroll events. By default
+ * longpress is enabled.
+ *
+ * @param isLongpressEnabled whether longpress should be enabled.
+ */
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ mIsLongpressEnabled = isLongpressEnabled;
+ }
+
+ /**
+ * @return true if longpress is enabled, else false.
+ */
+ public boolean isLongpressEnabled() {
+ return mIsLongpressEnabled;
+ }
+
+ /**
+ * Analyzes the given motion event and if applicable triggers the
+ * appropriate callbacks on the {@link OnGestureListener} supplied.
+ *
+ * @param ev The current motion event.
+ * @return true if the {@link OnGestureListener} consumed the event,
+ * else false.
+ */
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
+ }
+
+ final int action = ev.getAction();
+
+ if (mCurrentMotionEvent != null) {
+ mCurrentMotionEvent.recycle();
+ }
+ mCurrentMotionEvent = MotionEvent.obtain(ev);
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ final boolean pointerUp =
+ (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
+ final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
+ final boolean isGeneratedGesture =
+ (ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
+
+ // Determine focal point
+ float sumX = 0, sumY = 0;
+ final int count = ev.getPointerCount();
+ for (int i = 0; i < count; i++) {
+ if (skipIndex == i) continue;
+ sumX += ev.getX(i);
+ sumY += ev.getY(i);
+ }
+ final int div = pointerUp ? count - 1 : count;
+ final float focusX = sumX / div;
+ final float focusY = sumY / div;
+
+ boolean handled = false;
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ mDownFocusX = mLastFocusX = focusX;
+ mDownFocusY = mLastFocusY = focusY;
+ // Cancel long press and taps
+ cancelTaps();
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ mDownFocusX = mLastFocusX = focusX;
+ mDownFocusY = mLastFocusY = focusY;
+
+ // Check the dot product of current velocities.
+ // If the pointer that left was opposing another velocity vector, clear.
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+ final int upIndex = ev.getActionIndex();
+ final int id1 = ev.getPointerId(upIndex);
+ final float x1 = mVelocityTracker.getXVelocity(id1);
+ final float y1 = mVelocityTracker.getYVelocity(id1);
+ for (int i = 0; i < count; i++) {
+ if (i == upIndex) continue;
+
+ final int id2 = ev.getPointerId(i);
+ final float x = x1 * mVelocityTracker.getXVelocity(id2);
+ final float y = y1 * mVelocityTracker.getYVelocity(id2);
+
+ final float dot = x + y;
+ if (dot < 0) {
+ mVelocityTracker.clear();
+ break;
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ if (mDoubleTapListener != null) {
+ boolean hadTapMessage = mHandler.hasMessages(TAP);
+ if (hadTapMessage) mHandler.removeMessages(TAP);
+ if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
+ && hadTapMessage
+ && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
+ // This is a second tap
+ mIsDoubleTapping = true;
+ recordGestureClassification(
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
+ // Give a callback with the first tap of the double-tap
+ handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
+ // Give a callback with down event of the double-tap
+ handled |= mDoubleTapListener.onDoubleTapEvent(ev);
+ } else {
+ // This is a first tap
+ mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
+ }
+ }
+
+ mDownFocusX = mLastFocusX = focusX;
+ mDownFocusY = mLastFocusY = focusY;
+ if (mCurrentDownEvent != null) {
+ mCurrentDownEvent.recycle();
+ }
+ mCurrentDownEvent = MotionEvent.obtain(ev);
+ mAlwaysInTapRegion = true;
+ mAlwaysInBiggerTapRegion = true;
+ mStillDown = true;
+ mInLongPress = false;
+ mDeferConfirmSingleTap = false;
+ mHasRecordedClassification = false;
+
+ if (mIsLongpressEnabled) {
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.sendMessageAtTime(
+ mHandler.obtainMessage(
+ LONG_PRESS,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
+ 0 /* arg2 */),
+ mCurrentDownEvent.getDownTime()
+ + ViewConfiguration.getLongPressTimeout());
+ }
+ mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
+ mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
+ handled |= mListener.onDown(ev);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mInLongPress || mInContextClick) {
+ break;
+ }
+
+ final int motionClassification = ev.getClassification();
+ final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
+
+ final float scrollX = mLastFocusX - focusX;
+ final float scrollY = mLastFocusY - focusY;
+ if (mIsDoubleTapping) {
+ // Give the move events of the double-tap
+ recordGestureClassification(
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
+ handled |= mDoubleTapListener.onDoubleTapEvent(ev);
+ } else if (mAlwaysInTapRegion) {
+ final int deltaX = (int) (focusX - mDownFocusX);
+ final int deltaY = (int) (focusY - mDownFocusY);
+ int distance = (deltaX * deltaX) + (deltaY * deltaY);
+ int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
+
+ final boolean ambiguousGesture =
+ motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
+ final boolean shouldInhibitDefaultAction =
+ hasPendingLongPress && ambiguousGesture;
+ if (shouldInhibitDefaultAction) {
+ // Inhibit default long press
+ if (distance > slopSquare) {
+ // The default action here is to remove long press. But if the touch
+ // slop below gets increased, and we never exceed the modified touch
+ // slop while still receiving AMBIGUOUS_GESTURE, we risk that *nothing*
+ // will happen in response to user input. To prevent this,
+ // reschedule long press with a modified timeout.
+ mHandler.removeMessages(LONG_PRESS);
+ final long longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ mHandler.sendMessageAtTime(
+ mHandler.obtainMessage(
+ LONG_PRESS,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
+ 0 /* arg2 */),
+ ev.getDownTime()
+ + (long) (longPressTimeout * mAmbiguousGestureMultiplier));
+ }
+ // Inhibit default scroll. If a gesture is ambiguous, we prevent scroll
+ // until the gesture is resolved.
+ // However, for safety, simply increase the touch slop in case the
+ // classification is erroneous. Since the value is squared, multiply twice.
+ slopSquare *= mAmbiguousGestureMultiplier * mAmbiguousGestureMultiplier;
+ }
+
+ if (distance > slopSquare) {
+ recordGestureClassification(
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL);
+ handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
+ mLastFocusX = focusX;
+ mLastFocusY = focusY;
+ mAlwaysInTapRegion = false;
+ mHandler.removeMessages(TAP);
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ }
+ int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
+ if (distance > doubleTapSlopSquare) {
+ mAlwaysInBiggerTapRegion = false;
+ }
+ } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
+ recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL);
+ handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
+ mLastFocusX = focusX;
+ mLastFocusY = focusY;
+ }
+ final boolean deepPress =
+ motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
+ if (deepPress && hasPendingLongPress) {
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.sendMessage(
+ mHandler.obtainMessage(
+ LONG_PRESS,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS,
+ 0 /* arg2 */));
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ mStillDown = false;
+ MotionEvent currentUpEvent = MotionEvent.obtain(ev);
+ if (mIsDoubleTapping) {
+ // Finally, give the up event of the double-tap
+ recordGestureClassification(
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
+ handled |= mDoubleTapListener.onDoubleTapEvent(ev);
+ } else if (mInLongPress) {
+ mHandler.removeMessages(TAP);
+ mInLongPress = false;
+ } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
+ recordGestureClassification(
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
+ handled = mListener.onSingleTapUp(ev);
+ if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
+ mDoubleTapListener.onSingleTapConfirmed(ev);
+ }
+ } else if (!mIgnoreNextUpEvent) {
+
+ // A fling must travel the minimum tap distance
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ final int pointerId = ev.getPointerId(0);
+ velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+ final float velocityY = velocityTracker.getYVelocity(pointerId);
+ final float velocityX = velocityTracker.getXVelocity(pointerId);
+
+ if ((Math.abs(velocityY) > mMinimumFlingVelocity)
+ || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
+ handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
+ }
+ }
+ if (mPreviousUpEvent != null) {
+ mPreviousUpEvent.recycle();
+ }
+ // Hold the event we obtained above - listeners may have changed the original.
+ mPreviousUpEvent = currentUpEvent;
+ if (mVelocityTracker != null) {
+ // This may have been cleared when we called out to the
+ // application above.
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ mIsDoubleTapping = false;
+ mDeferConfirmSingleTap = false;
+ mIgnoreNextUpEvent = false;
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ cancel();
+ break;
+ }
+
+ if (!handled && mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
+ }
+ return handled;
+ }
+
+ /**
+ * Analyzes the given generic motion event and if applicable triggers the
+ * appropriate callbacks on the {@link OnGestureListener} supplied.
+ *
+ * @param ev The current motion event.
+ * @return true if the {@link OnGestureListener} consumed the event,
+ * else false.
+ */
+ public boolean onGenericMotionEvent(MotionEvent ev) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
+ }
+
+ final int actionButton = ev.getActionButton();
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_BUTTON_PRESS:
+ if (mContextClickListener != null && !mInContextClick && !mInLongPress
+ && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
+ || actionButton == MotionEvent.BUTTON_SECONDARY)) {
+ if (mContextClickListener.onContextClick(ev)) {
+ mInContextClick = true;
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.removeMessages(TAP);
+ return true;
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_BUTTON_RELEASE:
+ if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
+ || actionButton == MotionEvent.BUTTON_SECONDARY)) {
+ mInContextClick = false;
+ mIgnoreNextUpEvent = true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private void cancel() {
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.removeMessages(TAP);
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ mIsDoubleTapping = false;
+ mStillDown = false;
+ mAlwaysInTapRegion = false;
+ mAlwaysInBiggerTapRegion = false;
+ mDeferConfirmSingleTap = false;
+ mInLongPress = false;
+ mInContextClick = false;
+ mIgnoreNextUpEvent = false;
+ }
+
+ private void cancelTaps() {
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.removeMessages(TAP);
+ mIsDoubleTapping = false;
+ mAlwaysInTapRegion = false;
+ mAlwaysInBiggerTapRegion = false;
+ mDeferConfirmSingleTap = false;
+ mInLongPress = false;
+ mInContextClick = false;
+ mIgnoreNextUpEvent = false;
+ }
+
+ private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
+ MotionEvent secondDown) {
+ if (!mAlwaysInBiggerTapRegion) {
+ return false;
+ }
+
+ final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
+ if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
+ return false;
+ }
+
+ int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
+ int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
+ final boolean isGeneratedGesture =
+ (firstDown.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
+ int slopSquare = isGeneratedGesture ? 0 : mDoubleTapSlopSquare;
+ return (deltaX * deltaX + deltaY * deltaY < slopSquare);
+ }
+
+ private void dispatchLongPress() {
+ mHandler.removeMessages(TAP);
+ mDeferConfirmSingleTap = false;
+ mInLongPress = true;
+ mListener.onLongPress(mCurrentDownEvent);
+ }
+
+ private void recordGestureClassification(int classification) {
+ if (mHasRecordedClassification
+ || classification
+ == TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION) {
+ // Only record the first classification for an event stream.
+ return;
+ }
+ if (mCurrentDownEvent == null || mCurrentMotionEvent == null) {
+ // If the complete event stream wasn't seen, don't record anything.
+ mHasRecordedClassification = true;
+ return;
+ }
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED,
+ getClass().getName(),
+ classification,
+ (int) (SystemClock.uptimeMillis() - mCurrentMotionEvent.getDownTime()),
+ (float) Math.hypot(mCurrentMotionEvent.getRawX() - mCurrentDownEvent.getRawX(),
+ mCurrentMotionEvent.getRawY() - mCurrentDownEvent.getRawY()));
+ mHasRecordedClassification = true;
+ }
+}
diff --git a/android/view/GestureExclusionTracker.java b/android/view/GestureExclusionTracker.java
new file mode 100644
index 0000000..fffb323
--- /dev/null
+++ b/android/view/GestureExclusionTracker.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Used by {@link ViewRootImpl} to track system gesture exclusion rects reported by views.
+ */
+class GestureExclusionTracker {
+ private boolean mGestureExclusionViewsChanged = false;
+ private boolean mRootGestureExclusionRectsChanged = false;
+ private List<Rect> mRootGestureExclusionRects = Collections.emptyList();
+ private List<GestureExclusionViewInfo> mGestureExclusionViewInfos = new ArrayList<>();
+ private List<Rect> mGestureExclusionRects = Collections.emptyList();
+
+ public void updateRectsForView(@NonNull View view) {
+ boolean found = false;
+ final Iterator<GestureExclusionViewInfo> i = mGestureExclusionViewInfos.iterator();
+ while (i.hasNext()) {
+ final GestureExclusionViewInfo info = i.next();
+ final View v = info.getView();
+ if (v == null || !v.isAttachedToWindow() || !v.isAggregatedVisible()) {
+ mGestureExclusionViewsChanged = true;
+ i.remove();
+ continue;
+ }
+ if (v == view) {
+ found = true;
+ info.mDirty = true;
+ break;
+ }
+ }
+ if (!found && view.isAttachedToWindow()) {
+ mGestureExclusionViewInfos.add(new GestureExclusionViewInfo(view));
+ mGestureExclusionViewsChanged = true;
+ }
+ }
+
+ @Nullable
+ public List<Rect> computeChangedRects() {
+ boolean changed = mRootGestureExclusionRectsChanged;
+ final Iterator<GestureExclusionViewInfo> i = mGestureExclusionViewInfos.iterator();
+ final List<Rect> rects = new ArrayList<>(mRootGestureExclusionRects);
+ while (i.hasNext()) {
+ final GestureExclusionViewInfo info = i.next();
+ switch (info.update()) {
+ case GestureExclusionViewInfo.CHANGED:
+ changed = true;
+ // Deliberate fall-through
+ case GestureExclusionViewInfo.UNCHANGED:
+ rects.addAll(info.mExclusionRects);
+ break;
+ case GestureExclusionViewInfo.GONE:
+ mGestureExclusionViewsChanged = true;
+ i.remove();
+ break;
+ }
+ }
+ if (changed || mGestureExclusionViewsChanged) {
+ mGestureExclusionViewsChanged = false;
+ mRootGestureExclusionRectsChanged = false;
+ if (!mGestureExclusionRects.equals(rects)) {
+ mGestureExclusionRects = rects;
+ return rects;
+ }
+ }
+ return null;
+ }
+
+ public void setRootSystemGestureExclusionRects(@NonNull List<Rect> rects) {
+ Preconditions.checkNotNull(rects, "rects must not be null");
+ mRootGestureExclusionRects = rects;
+ mRootGestureExclusionRectsChanged = true;
+ }
+
+ @NonNull
+ public List<Rect> getRootSystemGestureExclusionRects() {
+ return mRootGestureExclusionRects;
+ }
+
+ private static class GestureExclusionViewInfo {
+ public static final int CHANGED = 0;
+ public static final int UNCHANGED = 1;
+ public static final int GONE = 2;
+
+ private final WeakReference<View> mView;
+ boolean mDirty = true;
+ List<Rect> mExclusionRects = Collections.emptyList();
+
+ GestureExclusionViewInfo(View view) {
+ mView = new WeakReference<>(view);
+ }
+
+ public View getView() {
+ return mView.get();
+ }
+
+ public int update() {
+ final View excludedView = getView();
+ if (excludedView == null || !excludedView.isAttachedToWindow()
+ || !excludedView.isAggregatedVisible()) return GONE;
+ final List<Rect> localRects = excludedView.getSystemGestureExclusionRects();
+ final List<Rect> newRects = new ArrayList<>(localRects.size());
+ for (Rect src : localRects) {
+ Rect mappedRect = new Rect(src);
+ ViewParent p = excludedView.getParent();
+ if (p != null && p.getChildVisibleRect(excludedView, mappedRect, null)) {
+ newRects.add(mappedRect);
+ }
+ }
+
+ if (mExclusionRects.equals(localRects)) return UNCHANGED;
+ mExclusionRects = newRects;
+ return CHANGED;
+ }
+ }
+}
diff --git a/android/view/GhostView.java b/android/view/GhostView.java
new file mode 100644
index 0000000..46d3c64
--- /dev/null
+++ b/android/view/GhostView.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RecordingCanvas;
+import android.graphics.RenderNode;
+import android.os.Build;
+import android.widget.FrameLayout;
+
+import java.util.ArrayList;
+
+/**
+ * This view draws another View in an Overlay without changing the parent. It will not be drawn
+ * by its parent because its visibility is set to INVISIBLE, but will be drawn
+ * here using its render node. When the GhostView is set to INVISIBLE, the View it is
+ * shadowing will become VISIBLE and when the GhostView becomes VISIBLE, the shadowed
+ * view becomes INVISIBLE.
+ * @hide
+ */
+public class GhostView extends View {
+ private final View mView;
+ private int mReferences;
+ private boolean mBeingMoved;
+
+ private GhostView(View view) {
+ super(view.getContext());
+ mView = view;
+ mView.mGhostView = this;
+ final ViewGroup parent = (ViewGroup) mView.getParent();
+ mView.setTransitionVisibility(View.INVISIBLE);
+ parent.invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (canvas instanceof RecordingCanvas) {
+ RecordingCanvas dlCanvas = (RecordingCanvas) canvas;
+ mView.mRecreateDisplayList = true;
+ RenderNode renderNode = mView.updateDisplayListIfDirty();
+ if (renderNode.hasDisplayList()) {
+ dlCanvas.enableZ(); // enable shadow for this rendernode
+ dlCanvas.drawRenderNode(renderNode);
+ dlCanvas.disableZ(); // re-disable reordering/shadows
+ }
+ }
+ }
+
+ public void setMatrix(Matrix matrix) {
+ mRenderNode.setAnimationMatrix(matrix);
+ }
+
+ @Override
+ public void setVisibility(@Visibility int visibility) {
+ super.setVisibility(visibility);
+ if (mView.mGhostView == this) {
+ int inverseVisibility = (visibility == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE;
+ mView.setTransitionVisibility(inverseVisibility);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (!mBeingMoved) {
+ mView.setTransitionVisibility(View.VISIBLE);
+ mView.mGhostView = null;
+ final ViewGroup parent = (ViewGroup) mView.getParent();
+ if (parent != null) {
+ parent.invalidate();
+ }
+ }
+ }
+
+ public static void calculateMatrix(View view, ViewGroup host, Matrix matrix) {
+ ViewGroup parent = (ViewGroup) view.getParent();
+ matrix.reset();
+ parent.transformMatrixToGlobal(matrix);
+ matrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
+ host.transformMatrixToLocal(matrix);
+ }
+
+ @UnsupportedAppUsage
+ public static GhostView addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
+ if (!(view.getParent() instanceof ViewGroup)) {
+ throw new IllegalArgumentException("Ghosted views must be parented by a ViewGroup");
+ }
+ ViewGroupOverlay overlay = viewGroup.getOverlay();
+ ViewOverlay.OverlayViewGroup overlayViewGroup = overlay.mOverlayViewGroup;
+ GhostView ghostView = view.mGhostView;
+ int previousRefCount = 0;
+ if (ghostView != null) {
+ View oldParent = (View) ghostView.getParent();
+ ViewGroup oldGrandParent = (ViewGroup) oldParent.getParent();
+ if (oldGrandParent != overlayViewGroup) {
+ previousRefCount = ghostView.mReferences;
+ oldGrandParent.removeView(oldParent);
+ ghostView = null;
+ }
+ }
+ if (ghostView == null) {
+ if (matrix == null) {
+ matrix = new Matrix();
+ calculateMatrix(view, viewGroup, matrix);
+ }
+ ghostView = new GhostView(view);
+ ghostView.setMatrix(matrix);
+ FrameLayout parent = new FrameLayout(view.getContext());
+ parent.setClipChildren(false);
+ copySize(viewGroup, parent);
+ copySize(viewGroup, ghostView);
+ parent.addView(ghostView);
+ ArrayList<View> tempViews = new ArrayList<View>();
+ int firstGhost = moveGhostViewsToTop(overlay.mOverlayViewGroup, tempViews);
+ insertIntoOverlay(overlay.mOverlayViewGroup, parent, ghostView, tempViews, firstGhost);
+ ghostView.mReferences = previousRefCount;
+ } else if (matrix != null) {
+ ghostView.setMatrix(matrix);
+ }
+ ghostView.mReferences++;
+ return ghostView;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ public static GhostView addGhost(View view, ViewGroup viewGroup) {
+ return addGhost(view, viewGroup, null);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ public static void removeGhost(View view) {
+ GhostView ghostView = view.mGhostView;
+ if (ghostView != null) {
+ ghostView.mReferences--;
+ if (ghostView.mReferences == 0) {
+ ViewGroup parent = (ViewGroup) ghostView.getParent();
+ ViewGroup grandParent = (ViewGroup) parent.getParent();
+ grandParent.removeView(parent);
+ }
+ }
+ }
+
+ public static GhostView getGhost(View view) {
+ return view.mGhostView;
+ }
+
+ private static void copySize(View from, View to) {
+ to.setLeft(0);
+ to.setTop(0);
+ to.setRight(from.getWidth());
+ to.setBottom(from.getHeight());
+ }
+
+ /**
+ * Move the GhostViews to the end so that they are on top of other views and it is easier
+ * to do binary search for the correct location for the GhostViews in insertIntoOverlay.
+ *
+ * @return The index of the first GhostView or -1 if no GhostView is in the ViewGroup
+ */
+ private static int moveGhostViewsToTop(ViewGroup viewGroup, ArrayList<View> tempViews) {
+ final int numChildren = viewGroup.getChildCount();
+ if (numChildren == 0) {
+ return -1;
+ } else if (isGhostWrapper(viewGroup.getChildAt(numChildren - 1))) {
+ // GhostViews are already at the end
+ int firstGhost = numChildren - 1;
+ for (int i = numChildren - 2; i >= 0; i--) {
+ if (!isGhostWrapper(viewGroup.getChildAt(i))) {
+ break;
+ }
+ firstGhost = i;
+ }
+ return firstGhost;
+ }
+
+ // Remove all GhostViews from the middle
+ for (int i = numChildren - 2; i >= 0; i--) {
+ View child = viewGroup.getChildAt(i);
+ if (isGhostWrapper(child)) {
+ tempViews.add(child);
+ GhostView ghostView = (GhostView)((ViewGroup)child).getChildAt(0);
+ ghostView.mBeingMoved = true;
+ viewGroup.removeViewAt(i);
+ ghostView.mBeingMoved = false;
+ }
+ }
+
+ final int firstGhost;
+ if (tempViews.isEmpty()) {
+ firstGhost = -1;
+ } else {
+ firstGhost = viewGroup.getChildCount();
+ // Add the GhostViews to the end
+ for (int i = tempViews.size() - 1; i >= 0; i--) {
+ viewGroup.addView(tempViews.get(i));
+ }
+ tempViews.clear();
+ }
+ return firstGhost;
+ }
+
+ /**
+ * Inserts a GhostView into the overlay's ViewGroup in the order in which they
+ * should be displayed by the UI.
+ */
+ private static void insertIntoOverlay(ViewGroup viewGroup, ViewGroup wrapper,
+ GhostView ghostView, ArrayList<View> tempParents, int firstGhost) {
+ if (firstGhost == -1) {
+ viewGroup.addView(wrapper);
+ } else {
+ ArrayList<View> viewParents = new ArrayList<View>();
+ getParents(ghostView.mView, viewParents);
+
+ int index = getInsertIndex(viewGroup, viewParents, tempParents, firstGhost);
+ if (index < 0 || index >= viewGroup.getChildCount()) {
+ viewGroup.addView(wrapper);
+ } else {
+ viewGroup.addView(wrapper, index);
+ }
+ }
+ }
+
+ /**
+ * Find the index into the overlay to insert the GhostView based on the order that the
+ * views should be drawn. This keeps GhostViews layered in the same order
+ * that they are ordered in the UI.
+ */
+ private static int getInsertIndex(ViewGroup overlayViewGroup, ArrayList<View> viewParents,
+ ArrayList<View> tempParents, int firstGhost) {
+ int low = firstGhost;
+ int high = overlayViewGroup.getChildCount() - 1;
+
+ while (low <= high) {
+ int mid = (low + high) / 2;
+ ViewGroup wrapper = (ViewGroup) overlayViewGroup.getChildAt(mid);
+ GhostView midView = (GhostView) wrapper.getChildAt(0);
+ getParents(midView.mView, tempParents);
+ if (isOnTop(viewParents, tempParents)) {
+ low = mid + 1;
+ } else {
+ high = mid - 1;
+ }
+ tempParents.clear();
+ }
+
+ return low;
+ }
+
+ /**
+ * Returns true if view is a GhostView's FrameLayout wrapper.
+ */
+ private static boolean isGhostWrapper(View view) {
+ if (view instanceof FrameLayout) {
+ FrameLayout frameLayout = (FrameLayout) view;
+ if (frameLayout.getChildCount() == 1) {
+ View child = frameLayout.getChildAt(0);
+ return child instanceof GhostView;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if viewParents is from a View that is on top of the comparedWith's view.
+ * The ArrayLists contain the ancestors of views in order from top most grandparent, to
+ * the view itself, in order. The goal is to find the first matching parent and then
+ * compare the draw order of the siblings.
+ */
+ private static boolean isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith) {
+ if (viewParents.isEmpty() || comparedWith.isEmpty() ||
+ viewParents.get(0) != comparedWith.get(0)) {
+ // Not the same decorView -- arbitrary ordering
+ return true;
+ }
+ int depth = Math.min(viewParents.size(), comparedWith.size());
+ for (int i = 1; i < depth; i++) {
+ View viewParent = viewParents.get(i);
+ View comparedWithParent = comparedWith.get(i);
+
+ if (viewParent != comparedWithParent) {
+ // i - 1 is the same parent, but these are different children.
+ return isOnTop(viewParent, comparedWithParent);
+ }
+ }
+
+ // one of these is the parent of the other
+ boolean isComparedWithTheParent = (comparedWith.size() == depth);
+ return isComparedWithTheParent;
+ }
+
+ /**
+ * Adds all the parents, grandparents, etc. of view to parents.
+ */
+ private static void getParents(View view, ArrayList<View> parents) {
+ ViewParent parent = view.getParent();
+ if (parent != null && parent instanceof ViewGroup) {
+ getParents((View) parent, parents);
+ }
+ parents.add(view);
+ }
+
+ /**
+ * Returns true if view would be drawn on top of comparedWith or false otherwise.
+ * view and comparedWith are siblings with the same parent. This uses the logic
+ * that dispatchDraw uses to determine which View should be drawn first.
+ */
+ private static boolean isOnTop(View view, View comparedWith) {
+ ViewGroup parent = (ViewGroup) view.getParent();
+
+ final int childrenCount = parent.getChildCount();
+ final ArrayList<View> preorderedList = parent.buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && parent.isChildrenDrawingOrderEnabled();
+
+ // This default value shouldn't be used because both view and comparedWith
+ // should be in the list. If there is an error, then just return an arbitrary
+ // view is on top.
+ boolean isOnTop = true;
+ for (int i = 0; i < childrenCount; i++) {
+ int childIndex = customOrder ? parent.getChildDrawingOrder(childrenCount, i) : i;
+ final View child = (preorderedList == null)
+ ? parent.getChildAt(childIndex) : preorderedList.get(childIndex);
+ if (child == view) {
+ isOnTop = false;
+ break;
+ } else if (child == comparedWith) {
+ isOnTop = true;
+ break;
+ }
+ }
+
+ if (preorderedList != null) {
+ preorderedList.clear();
+ }
+ return isOnTop;
+ }
+}
diff --git a/android/view/Gravity.java b/android/view/Gravity.java
new file mode 100644
index 0000000..c8bfd36
--- /dev/null
+++ b/android/view/Gravity.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.graphics.Rect;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Standard constants and tools for placing an object within a potentially
+ * larger container.
+ */
+public class Gravity
+{
+ /** Constant indicating that no gravity has been set **/
+ public static final int NO_GRAVITY = 0x0000;
+
+ /** Raw bit indicating the gravity for an axis has been specified. */
+ public static final int AXIS_SPECIFIED = 0x0001;
+
+ /** Raw bit controlling how the left/top edge is placed. */
+ public static final int AXIS_PULL_BEFORE = 0x0002;
+ /** Raw bit controlling how the right/bottom edge is placed. */
+ public static final int AXIS_PULL_AFTER = 0x0004;
+ /** Raw bit controlling whether the right/bottom edge is clipped to its
+ * container, based on the gravity direction being applied. */
+ public static final int AXIS_CLIP = 0x0008;
+
+ /** Bits defining the horizontal axis. */
+ public static final int AXIS_X_SHIFT = 0;
+ /** Bits defining the vertical axis. */
+ public static final int AXIS_Y_SHIFT = 4;
+
+ /** Push object to the top of its container, not changing its size. */
+ public static final int TOP = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
+ /** Push object to the bottom of its container, not changing its size. */
+ public static final int BOTTOM = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
+ /** Push object to the left of its container, not changing its size. */
+ public static final int LEFT = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
+ /** Push object to the right of its container, not changing its size. */
+ public static final int RIGHT = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
+
+ /** Place object in the vertical center of its container, not changing its
+ * size. */
+ public static final int CENTER_VERTICAL = AXIS_SPECIFIED<<AXIS_Y_SHIFT;
+ /** Grow the vertical size of the object if needed so it completely fills
+ * its container. */
+ public static final int FILL_VERTICAL = TOP|BOTTOM;
+
+ /** Place object in the horizontal center of its container, not changing its
+ * size. */
+ public static final int CENTER_HORIZONTAL = AXIS_SPECIFIED<<AXIS_X_SHIFT;
+ /** Grow the horizontal size of the object if needed so it completely fills
+ * its container. */
+ public static final int FILL_HORIZONTAL = LEFT|RIGHT;
+
+ /** Place the object in the center of its container in both the vertical
+ * and horizontal axis, not changing its size. */
+ public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL;
+
+ /** Grow the horizontal and vertical size of the object if needed so it
+ * completely fills its container. */
+ public static final int FILL = FILL_VERTICAL|FILL_HORIZONTAL;
+
+ /** Flag to clip the edges of the object to its container along the
+ * vertical axis. */
+ public static final int CLIP_VERTICAL = AXIS_CLIP<<AXIS_Y_SHIFT;
+
+ /** Flag to clip the edges of the object to its container along the
+ * horizontal axis. */
+ public static final int CLIP_HORIZONTAL = AXIS_CLIP<<AXIS_X_SHIFT;
+
+ /** Raw bit controlling whether the layout direction is relative or not (START/END instead of
+ * absolute LEFT/RIGHT).
+ */
+ public static final int RELATIVE_LAYOUT_DIRECTION = 0x00800000;
+
+ /**
+ * Binary mask to get the absolute horizontal gravity of a gravity.
+ */
+ public static final int HORIZONTAL_GRAVITY_MASK = (AXIS_SPECIFIED |
+ AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_X_SHIFT;
+ /**
+ * Binary mask to get the vertical gravity of a gravity.
+ */
+ public static final int VERTICAL_GRAVITY_MASK = (AXIS_SPECIFIED |
+ AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_Y_SHIFT;
+
+ /** Special constant to enable clipping to an overall display along the
+ * vertical dimension. This is not applied by default by
+ * {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so
+ * yourself by calling {@link #applyDisplay}.
+ */
+ public static final int DISPLAY_CLIP_VERTICAL = 0x10000000;
+
+ /** Special constant to enable clipping to an overall display along the
+ * horizontal dimension. This is not applied by default by
+ * {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so
+ * yourself by calling {@link #applyDisplay}.
+ */
+ public static final int DISPLAY_CLIP_HORIZONTAL = 0x01000000;
+
+ /** Push object to x-axis position at the start of its container, not changing its size. */
+ public static final int START = RELATIVE_LAYOUT_DIRECTION | LEFT;
+
+ /** Push object to x-axis position at the end of its container, not changing its size. */
+ public static final int END = RELATIVE_LAYOUT_DIRECTION | RIGHT;
+
+ /**
+ * Binary mask for the horizontal gravity and script specific direction bit.
+ */
+ public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END;
+
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {
+ Gravity.FILL,
+ Gravity.FILL_HORIZONTAL,
+ Gravity.FILL_VERTICAL,
+ Gravity.START,
+ Gravity.END,
+ Gravity.LEFT,
+ Gravity.RIGHT,
+ Gravity.TOP,
+ Gravity.BOTTOM,
+ Gravity.CENTER,
+ Gravity.CENTER_HORIZONTAL,
+ Gravity.CENTER_VERTICAL,
+ Gravity.DISPLAY_CLIP_HORIZONTAL,
+ Gravity.DISPLAY_CLIP_VERTICAL,
+ Gravity.CLIP_HORIZONTAL,
+ Gravity.CLIP_VERTICAL,
+ Gravity.NO_GRAVITY
+ })
+ public @interface GravityFlags {}
+
+ /**
+ * Apply a gravity constant to an object. This supposes that the layout direction is LTR.
+ *
+ * @param gravity The desired placement of the object, as defined by the
+ * constants in this class.
+ * @param w The horizontal size of the object.
+ * @param h The vertical size of the object.
+ * @param container The frame of the containing space, in which the object
+ * will be placed. Should be large enough to contain the
+ * width and height of the object.
+ * @param outRect Receives the computed frame of the object in its
+ * container.
+ */
+ public static void apply(int gravity, int w, int h, Rect container, Rect outRect) {
+ apply(gravity, w, h, container, 0, 0, outRect);
+ }
+
+ /**
+ * Apply a gravity constant to an object and take care if layout direction is RTL or not.
+ *
+ * @param gravity The desired placement of the object, as defined by the
+ * constants in this class.
+ * @param w The horizontal size of the object.
+ * @param h The vertical size of the object.
+ * @param container The frame of the containing space, in which the object
+ * will be placed. Should be large enough to contain the
+ * width and height of the object.
+ * @param outRect Receives the computed frame of the object in its
+ * container.
+ * @param layoutDirection The layout direction.
+ *
+ * @see View#LAYOUT_DIRECTION_LTR
+ * @see View#LAYOUT_DIRECTION_RTL
+ */
+ public static void apply(int gravity, int w, int h, Rect container,
+ Rect outRect, int layoutDirection) {
+ int absGravity = getAbsoluteGravity(gravity, layoutDirection);
+ apply(absGravity, w, h, container, 0, 0, outRect);
+ }
+
+ /**
+ * Apply a gravity constant to an object.
+ *
+ * @param gravity The desired placement of the object, as defined by the
+ * constants in this class.
+ * @param w The horizontal size of the object.
+ * @param h The vertical size of the object.
+ * @param container The frame of the containing space, in which the object
+ * will be placed. Should be large enough to contain the
+ * width and height of the object.
+ * @param xAdj Offset to apply to the X axis. If gravity is LEFT this
+ * pushes it to the right; if gravity is RIGHT it pushes it to
+ * the left; if gravity is CENTER_HORIZONTAL it pushes it to the
+ * right or left; otherwise it is ignored.
+ * @param yAdj Offset to apply to the Y axis. If gravity is TOP this pushes
+ * it down; if gravity is BOTTOM it pushes it up; if gravity is
+ * CENTER_VERTICAL it pushes it down or up; otherwise it is
+ * ignored.
+ * @param outRect Receives the computed frame of the object in its
+ * container.
+ */
+ public static void apply(int gravity, int w, int h, Rect container,
+ int xAdj, int yAdj, Rect outRect) {
+ switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) {
+ case 0:
+ outRect.left = container.left
+ + ((container.right - container.left - w)/2) + xAdj;
+ outRect.right = outRect.left + w;
+ if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
+ == (AXIS_CLIP<<AXIS_X_SHIFT)) {
+ if (outRect.left < container.left) {
+ outRect.left = container.left;
+ }
+ if (outRect.right > container.right) {
+ outRect.right = container.right;
+ }
+ }
+ break;
+ case AXIS_PULL_BEFORE<<AXIS_X_SHIFT:
+ outRect.left = container.left + xAdj;
+ outRect.right = outRect.left + w;
+ if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
+ == (AXIS_CLIP<<AXIS_X_SHIFT)) {
+ if (outRect.right > container.right) {
+ outRect.right = container.right;
+ }
+ }
+ break;
+ case AXIS_PULL_AFTER<<AXIS_X_SHIFT:
+ outRect.right = container.right - xAdj;
+ outRect.left = outRect.right - w;
+ if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
+ == (AXIS_CLIP<<AXIS_X_SHIFT)) {
+ if (outRect.left < container.left) {
+ outRect.left = container.left;
+ }
+ }
+ break;
+ default:
+ outRect.left = container.left + xAdj;
+ outRect.right = container.right + xAdj;
+ break;
+ }
+
+ switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) {
+ case 0:
+ outRect.top = container.top
+ + ((container.bottom - container.top - h)/2) + yAdj;
+ outRect.bottom = outRect.top + h;
+ if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
+ == (AXIS_CLIP<<AXIS_Y_SHIFT)) {
+ if (outRect.top < container.top) {
+ outRect.top = container.top;
+ }
+ if (outRect.bottom > container.bottom) {
+ outRect.bottom = container.bottom;
+ }
+ }
+ break;
+ case AXIS_PULL_BEFORE<<AXIS_Y_SHIFT:
+ outRect.top = container.top + yAdj;
+ outRect.bottom = outRect.top + h;
+ if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
+ == (AXIS_CLIP<<AXIS_Y_SHIFT)) {
+ if (outRect.bottom > container.bottom) {
+ outRect.bottom = container.bottom;
+ }
+ }
+ break;
+ case AXIS_PULL_AFTER<<AXIS_Y_SHIFT:
+ outRect.bottom = container.bottom - yAdj;
+ outRect.top = outRect.bottom - h;
+ if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
+ == (AXIS_CLIP<<AXIS_Y_SHIFT)) {
+ if (outRect.top < container.top) {
+ outRect.top = container.top;
+ }
+ }
+ break;
+ default:
+ outRect.top = container.top + yAdj;
+ outRect.bottom = container.bottom + yAdj;
+ break;
+ }
+ }
+
+ /**
+ * Apply a gravity constant to an object.
+ *
+ * @param gravity The desired placement of the object, as defined by the
+ * constants in this class.
+ * @param w The horizontal size of the object.
+ * @param h The vertical size of the object.
+ * @param container The frame of the containing space, in which the object
+ * will be placed. Should be large enough to contain the
+ * width and height of the object.
+ * @param xAdj Offset to apply to the X axis. If gravity is LEFT this
+ * pushes it to the right; if gravity is RIGHT it pushes it to
+ * the left; if gravity is CENTER_HORIZONTAL it pushes it to the
+ * right or left; otherwise it is ignored.
+ * @param yAdj Offset to apply to the Y axis. If gravity is TOP this pushes
+ * it down; if gravity is BOTTOM it pushes it up; if gravity is
+ * CENTER_VERTICAL it pushes it down or up; otherwise it is
+ * ignored.
+ * @param outRect Receives the computed frame of the object in its
+ * container.
+ * @param layoutDirection The layout direction.
+ *
+ * @see View#LAYOUT_DIRECTION_LTR
+ * @see View#LAYOUT_DIRECTION_RTL
+ */
+ public static void apply(int gravity, int w, int h, Rect container,
+ int xAdj, int yAdj, Rect outRect, int layoutDirection) {
+ int absGravity = getAbsoluteGravity(gravity, layoutDirection);
+ apply(absGravity, w, h, container, xAdj, yAdj, outRect);
+ }
+
+ /**
+ * Apply additional gravity behavior based on the overall "display" that an
+ * object exists in. This can be used after
+ * {@link #apply(int, int, int, Rect, int, int, Rect)} to place the object
+ * within a visible display. By default this moves or clips the object
+ * to be visible in the display; the gravity flags
+ * {@link #DISPLAY_CLIP_HORIZONTAL} and {@link #DISPLAY_CLIP_VERTICAL}
+ * can be used to change this behavior.
+ *
+ * @param gravity Gravity constants to modify the placement within the
+ * display.
+ * @param display The rectangle of the display in which the object is
+ * being placed.
+ * @param inoutObj Supplies the current object position; returns with it
+ * modified if needed to fit in the display.
+ */
+ public static void applyDisplay(int gravity, Rect display, Rect inoutObj) {
+ if ((gravity&DISPLAY_CLIP_VERTICAL) != 0) {
+ if (inoutObj.top < display.top) inoutObj.top = display.top;
+ if (inoutObj.bottom > display.bottom) inoutObj.bottom = display.bottom;
+ } else {
+ int off = 0;
+ if (inoutObj.top < display.top) off = display.top-inoutObj.top;
+ else if (inoutObj.bottom > display.bottom) off = display.bottom-inoutObj.bottom;
+ if (off != 0) {
+ if (inoutObj.height() > (display.bottom-display.top)) {
+ inoutObj.top = display.top;
+ inoutObj.bottom = display.bottom;
+ } else {
+ inoutObj.top += off;
+ inoutObj.bottom += off;
+ }
+ }
+ }
+
+ if ((gravity&DISPLAY_CLIP_HORIZONTAL) != 0) {
+ if (inoutObj.left < display.left) inoutObj.left = display.left;
+ if (inoutObj.right > display.right) inoutObj.right = display.right;
+ } else {
+ int off = 0;
+ if (inoutObj.left < display.left) off = display.left-inoutObj.left;
+ else if (inoutObj.right > display.right) off = display.right-inoutObj.right;
+ if (off != 0) {
+ if (inoutObj.width() > (display.right-display.left)) {
+ inoutObj.left = display.left;
+ inoutObj.right = display.right;
+ } else {
+ inoutObj.left += off;
+ inoutObj.right += off;
+ }
+ }
+ }
+ }
+
+ /**
+ * Apply additional gravity behavior based on the overall "display" that an
+ * object exists in. This can be used after
+ * {@link #apply(int, int, int, Rect, int, int, Rect)} to place the object
+ * within a visible display. By default this moves or clips the object
+ * to be visible in the display; the gravity flags
+ * {@link #DISPLAY_CLIP_HORIZONTAL} and {@link #DISPLAY_CLIP_VERTICAL}
+ * can be used to change this behavior.
+ *
+ * @param gravity Gravity constants to modify the placement within the
+ * display.
+ * @param display The rectangle of the display in which the object is
+ * being placed.
+ * @param inoutObj Supplies the current object position; returns with it
+ * modified if needed to fit in the display.
+ * @param layoutDirection The layout direction.
+ *
+ * @see View#LAYOUT_DIRECTION_LTR
+ * @see View#LAYOUT_DIRECTION_RTL
+ */
+ public static void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) {
+ int absGravity = getAbsoluteGravity(gravity, layoutDirection);
+ applyDisplay(absGravity, display, inoutObj);
+ }
+
+ /**
+ * <p>Indicate whether the supplied gravity has a vertical pull.</p>
+ *
+ * @param gravity the gravity to check for vertical pull
+ * @return true if the supplied gravity has a vertical pull
+ */
+ public static boolean isVertical(int gravity) {
+ return gravity > 0 && (gravity & VERTICAL_GRAVITY_MASK) != 0;
+ }
+
+ /**
+ * <p>Indicate whether the supplied gravity has an horizontal pull.</p>
+ *
+ * @param gravity the gravity to check for horizontal pull
+ * @return true if the supplied gravity has an horizontal pull
+ */
+ public static boolean isHorizontal(int gravity) {
+ return gravity > 0 && (gravity & RELATIVE_HORIZONTAL_GRAVITY_MASK) != 0;
+ }
+
+ /**
+ * <p>Convert script specific gravity to absolute horizontal value.</p>
+ *
+ * if horizontal direction is LTR, then START will set LEFT and END will set RIGHT.
+ * if horizontal direction is RTL, then START will set RIGHT and END will set LEFT.
+ *
+ *
+ * @param gravity The gravity to convert to absolute (horizontal) values.
+ * @param layoutDirection The layout direction.
+ * @return gravity converted to absolute (horizontal) values.
+ */
+ public static int getAbsoluteGravity(int gravity, int layoutDirection) {
+ int result = gravity;
+ // If layout is script specific and gravity is horizontal relative (START or END)
+ if ((result & RELATIVE_LAYOUT_DIRECTION) > 0) {
+ if ((result & Gravity.START) == Gravity.START) {
+ // Remove the START bit
+ result &= ~START;
+ if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+ // Set the RIGHT bit
+ result |= RIGHT;
+ } else {
+ // Set the LEFT bit
+ result |= LEFT;
+ }
+ } else if ((result & Gravity.END) == Gravity.END) {
+ // Remove the END bit
+ result &= ~END;
+ if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+ // Set the LEFT bit
+ result |= LEFT;
+ } else {
+ // Set the RIGHT bit
+ result |= RIGHT;
+ }
+ }
+ // Don't need the script specific bit any more, so remove it as we are converting to
+ // absolute values (LEFT or RIGHT)
+ result &= ~RELATIVE_LAYOUT_DIRECTION;
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ public static String toString(int gravity) {
+ final StringBuilder result = new StringBuilder();
+ if ((gravity & FILL) == FILL) {
+ result.append("FILL").append(' ');
+ } else {
+ if ((gravity & FILL_VERTICAL) == FILL_VERTICAL) {
+ result.append("FILL_VERTICAL").append(' ');
+ } else {
+ if ((gravity & TOP) == TOP) {
+ result.append("TOP").append(' ');
+ }
+ if ((gravity & BOTTOM) == BOTTOM) {
+ result.append("BOTTOM").append(' ');
+ }
+ }
+ if ((gravity & FILL_HORIZONTAL) == FILL_HORIZONTAL) {
+ result.append("FILL_HORIZONTAL").append(' ');
+ } else {
+ if ((gravity & START) == START) {
+ result.append("START").append(' ');
+ } else if ((gravity & LEFT) == LEFT) {
+ result.append("LEFT").append(' ');
+ }
+ if ((gravity & END) == END) {
+ result.append("END").append(' ');
+ } else if ((gravity & RIGHT) == RIGHT) {
+ result.append("RIGHT").append(' ');
+ }
+ }
+ }
+ if ((gravity & CENTER) == CENTER) {
+ result.append("CENTER").append(' ');
+ } else {
+ if ((gravity & CENTER_VERTICAL) == CENTER_VERTICAL) {
+ result.append("CENTER_VERTICAL").append(' ');
+ }
+ if ((gravity & CENTER_HORIZONTAL) == CENTER_HORIZONTAL) {
+ result.append("CENTER_HORIZONTAL").append(' ');
+ }
+ }
+ if (result.length() == 0) {
+ result.append("NO GRAVITY").append(' ');
+ }
+ if ((gravity & DISPLAY_CLIP_VERTICAL) == DISPLAY_CLIP_VERTICAL) {
+ result.append("DISPLAY_CLIP_VERTICAL").append(' ');
+ }
+ if ((gravity & DISPLAY_CLIP_HORIZONTAL) == DISPLAY_CLIP_HORIZONTAL) {
+ result.append("DISPLAY_CLIP_HORIZONTAL").append(' ');
+ }
+ result.deleteCharAt(result.length() - 1);
+ return result.toString();
+ }
+}
diff --git a/android/view/HandlerActionQueue.java b/android/view/HandlerActionQueue.java
new file mode 100644
index 0000000..d016a74
--- /dev/null
+++ b/android/view/HandlerActionQueue.java
@@ -0,0 +1,125 @@
+/*
+ * 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.view;
+
+import android.os.Handler;
+
+import com.android.internal.util.GrowingArrayUtils;
+
+/**
+ * Class used to enqueue pending work from Views when no Handler is attached.
+ *
+ * @hide Exposed for test framework only.
+ */
+public class HandlerActionQueue {
+ private HandlerAction[] mActions;
+ private int mCount;
+
+ public void post(Runnable action) {
+ postDelayed(action, 0);
+ }
+
+ public void postDelayed(Runnable action, long delayMillis) {
+ final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
+
+ synchronized (this) {
+ if (mActions == null) {
+ mActions = new HandlerAction[4];
+ }
+ mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
+ mCount++;
+ }
+ }
+
+ public void removeCallbacks(Runnable action) {
+ synchronized (this) {
+ final int count = mCount;
+ int j = 0;
+
+ final HandlerAction[] actions = mActions;
+ for (int i = 0; i < count; i++) {
+ if (actions[i].matches(action)) {
+ // Remove this action by overwriting it within
+ // this loop or nulling it out later.
+ continue;
+ }
+
+ if (j != i) {
+ // At least one previous entry was removed, so
+ // this one needs to move to the "new" list.
+ actions[j] = actions[i];
+ }
+
+ j++;
+ }
+
+ // The "new" list only has j entries.
+ mCount = j;
+
+ // Null out any remaining entries.
+ for (; j < count; j++) {
+ actions[j] = null;
+ }
+ }
+ }
+
+ public void executeActions(Handler handler) {
+ synchronized (this) {
+ final HandlerAction[] actions = mActions;
+ for (int i = 0, count = mCount; i < count; i++) {
+ final HandlerAction handlerAction = actions[i];
+ handler.postDelayed(handlerAction.action, handlerAction.delay);
+ }
+
+ mActions = null;
+ mCount = 0;
+ }
+ }
+
+ public int size() {
+ return mCount;
+ }
+
+ public Runnable getRunnable(int index) {
+ if (index >= mCount) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mActions[index].action;
+ }
+
+ public long getDelay(int index) {
+ if (index >= mCount) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mActions[index].delay;
+ }
+
+ private static class HandlerAction {
+ final Runnable action;
+ final long delay;
+
+ public HandlerAction(Runnable action, long delay) {
+ this.action = action;
+ this.delay = delay;
+ }
+
+ public boolean matches(Runnable otherAction) {
+ return otherAction == null && action == null
+ || action != null && action.equals(otherAction);
+ }
+ }
+}
diff --git a/android/view/HandlerActionQueue_Delegate.java b/android/view/HandlerActionQueue_Delegate.java
new file mode 100644
index 0000000..e580ed0
--- /dev/null
+++ b/android/view/HandlerActionQueue_Delegate.java
@@ -0,0 +1,37 @@
+/*
+ * 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.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of
+ * {@link HandlerActionQueue}
+ *
+ * Through the layoutlib_create tool, the original methods of ViewRootImpl.RunQueue have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ */
+public class HandlerActionQueue_Delegate {
+
+ @LayoutlibDelegate
+ /*package*/ static void postDelayed(HandlerActionQueue thisQueue, Runnable action, long
+ delayMillis) {
+ // The actual HandlerActionQueue is never run and therefore never cleared. This method
+ // avoids runnables to be added to the RunQueue so they do not leak resources.
+ }
+}
diff --git a/android/view/HapticFeedbackConstants.java b/android/view/HapticFeedbackConstants.java
new file mode 100644
index 0000000..9f63500
--- /dev/null
+++ b/android/view/HapticFeedbackConstants.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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;
+
+/**
+ * Constants to be used to perform haptic feedback effects via
+ * {@link View#performHapticFeedback(int)}
+ */
+public class HapticFeedbackConstants {
+
+ private HapticFeedbackConstants() {}
+
+ /**
+ * The user has performed a long press on an object that is resulting
+ * in an action being performed.
+ */
+ public static final int LONG_PRESS = 0;
+
+ /**
+ * The user has pressed on a virtual on-screen key.
+ */
+ public static final int VIRTUAL_KEY = 1;
+
+ /**
+ * The user has pressed a soft keyboard key.
+ */
+ public static final int KEYBOARD_TAP = 3;
+
+ /**
+ * The user has pressed either an hour or minute tick of a Clock.
+ */
+ public static final int CLOCK_TICK = 4;
+
+ /**
+ * The user has pressed either a day or month or year date of a Calendar.
+ * @hide
+ */
+ public static final int CALENDAR_DATE = 5;
+
+ /**
+ * The user has performed a context click on an object.
+ */
+ public static final int CONTEXT_CLICK = 6;
+
+ /**
+ * The user has pressed a virtual or software keyboard key.
+ */
+ public static final int KEYBOARD_PRESS = KEYBOARD_TAP;
+
+ /**
+ * The user has released a virtual keyboard key.
+ */
+ public static final int KEYBOARD_RELEASE = 7;
+
+ /**
+ * The user has released a virtual key.
+ */
+ public static final int VIRTUAL_KEY_RELEASE = 8;
+
+ /**
+ * The user has performed a selection/insertion handle move on text field.
+ */
+ public static final int TEXT_HANDLE_MOVE = 9;
+
+ /**
+ * The user unlocked the device
+ * @hide
+ */
+ public static final int ENTRY_BUMP = 10;
+
+ /**
+ * The user has moved the dragged object within a droppable area.
+ * @hide
+ */
+ public static final int DRAG_CROSSING = 11;
+
+ /**
+ * The user has started a gesture (e.g. on the soft keyboard).
+ */
+ public static final int GESTURE_START = 12;
+
+ /**
+ * The user has finished a gesture (e.g. on the soft keyboard).
+ */
+ public static final int GESTURE_END = 13;
+
+ /**
+ * The user's squeeze crossed the gesture's initiation threshold.
+ * @hide
+ */
+ public static final int EDGE_SQUEEZE = 14;
+
+ /**
+ * The user's squeeze crossed the gesture's release threshold.
+ * @hide
+ */
+ public static final int EDGE_RELEASE = 15;
+
+ /**
+ * A haptic effect to signal the confirmation or successful completion of a user
+ * interaction.
+ */
+ public static final int CONFIRM = 16;
+
+ /**
+ * A haptic effect to signal the rejection or failure of a user interaction.
+ */
+ public static final int REJECT = 17;
+
+ /**
+ * The phone has booted with safe mode enabled.
+ * This is a private constant. Feel free to renumber as desired.
+ * @hide
+ */
+ public static final int SAFE_MODE_ENABLED = 10001;
+
+ /**
+ * Invocation of the voice assistant via hardware button.
+ * @hide
+ */
+ public static final int ASSISTANT_BUTTON = 10002;
+
+ /**
+ * Flag for {@link View#performHapticFeedback(int, int)
+ * View.performHapticFeedback(int, int)}: Ignore the setting in the
+ * view for whether to perform haptic feedback, do it always.
+ */
+ public static final int FLAG_IGNORE_VIEW_SETTING = 0x0001;
+
+ /**
+ * Flag for {@link View#performHapticFeedback(int, int)
+ * View.performHapticFeedback(int, int)}: Ignore the global setting
+ * for whether to perform haptic feedback, do it always.
+ */
+ public static final int FLAG_IGNORE_GLOBAL_SETTING = 0x0002;
+}
diff --git a/android/view/IWindowManagerImpl.java b/android/view/IWindowManagerImpl.java
new file mode 100644
index 0000000..efa8a9a
--- /dev/null
+++ b/android/view/IWindowManagerImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.res.Configuration;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+
+/**
+ * Basic implementation of {@link IWindowManager} so that {@link Display} (and
+ * {@link Display_Delegate}) can return a valid instance.
+ */
+public class IWindowManagerImpl extends IWindowManager.Default {
+
+ private final Configuration mConfig;
+ private final DisplayMetrics mMetrics;
+ private final int mRotation;
+ private final boolean mHasNavigationBar;
+
+ public IWindowManagerImpl(Configuration config, DisplayMetrics metrics, int rotation,
+ boolean hasNavigationBar) {
+ mConfig = config;
+ mMetrics = metrics;
+ mRotation = rotation;
+ mHasNavigationBar = hasNavigationBar;
+ }
+
+ // custom API.
+
+ public DisplayMetrics getMetrics() {
+ return mMetrics;
+ }
+
+ // ---- implementation of IWindowManager that we care about ----
+
+ @Override
+ public int getDefaultDisplayRotation() throws RemoteException {
+ return mRotation;
+ }
+
+ @Override
+ public boolean hasNavigationBar(int displayId) {
+ // TODO(multi-display): Change it once we need it per display.
+ return mHasNavigationBar;
+ }
+}
diff --git a/android/view/ImeFocusController.java b/android/view/ImeFocusController.java
new file mode 100644
index 0000000..d23a1e5
--- /dev/null
+++ b/android/view/ImeFocusController.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.ImeFocusControllerProto.HAS_IME_FOCUS;
+import static android.view.ImeFocusControllerProto.NEXT_SERVED_VIEW;
+import static android.view.ImeFocusControllerProto.SERVED_VIEW;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.StartInputFlags;
+import com.android.internal.inputmethod.StartInputReason;
+
+import java.util.Objects;
+
+/**
+ * Responsible for IME focus handling inside {@link ViewRootImpl}.
+ * @hide
+ */
+public final class ImeFocusController {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ImeFocusController";
+
+ private final ViewRootImpl mViewRootImpl;
+ private boolean mHasImeFocus = false;
+ private View mServedView;
+ private View mNextServedView;
+ private InputMethodManagerDelegate mDelegate;
+
+ @UiThread
+ ImeFocusController(@NonNull ViewRootImpl viewRootImpl) {
+ mViewRootImpl = viewRootImpl;
+ }
+
+ @NonNull
+ private InputMethodManagerDelegate getImmDelegate() {
+ InputMethodManagerDelegate delegate = mDelegate;
+ if (delegate != null) {
+ return delegate;
+ }
+ delegate = mViewRootImpl.mContext.getSystemService(InputMethodManager.class).getDelegate();
+ mDelegate = delegate;
+ return delegate;
+ }
+
+ /** Called when the view root is moved to a different display. */
+ @UiThread
+ void onMovedToDisplay() {
+ // InputMethodManager managed its instances for different displays. So if the associated
+ // display is changed, the delegate also needs to be refreshed (by getImmDelegate).
+ // See the comment in {@link android.app.SystemServiceRegistry} for InputMethodManager
+ // and {@link android.view.inputmethod.InputMethodManager#forContext}.
+ mDelegate = null;
+ }
+
+ @UiThread
+ void onTraversal(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
+ final boolean hasImeFocus = updateImeFocusable(windowAttribute, false /* force */);
+ if (!hasWindowFocus || isInLocalFocusMode(windowAttribute)) {
+ return;
+ }
+ if (hasImeFocus == mHasImeFocus) {
+ return;
+ }
+ mHasImeFocus = hasImeFocus;
+ if (mHasImeFocus) {
+ onPreWindowFocus(true /* hasWindowFocus */, windowAttribute);
+ onPostWindowFocus(mViewRootImpl.mView.findFocus(), true /* hasWindowFocus */,
+ windowAttribute);
+ }
+ }
+
+ @UiThread
+ void onPreWindowFocus(boolean hasWindowFocus, WindowManager.LayoutParams windowAttribute) {
+ if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
+ return;
+ }
+ if (hasWindowFocus) {
+ getImmDelegate().setCurrentRootView(mViewRootImpl);
+ }
+ }
+
+ @UiThread
+ boolean updateImeFocusable(WindowManager.LayoutParams windowAttribute, boolean force) {
+ final boolean hasImeFocus = WindowManager.LayoutParams.mayUseInputMethod(
+ windowAttribute.flags);
+ if (force) {
+ mHasImeFocus = hasImeFocus;
+ }
+ return hasImeFocus;
+ }
+
+ @UiThread
+ void onPostWindowFocus(View focusedView, boolean hasWindowFocus,
+ WindowManager.LayoutParams windowAttribute) {
+ if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
+ return;
+ }
+ View viewForWindowFocus = focusedView != null ? focusedView : mViewRootImpl.mView;
+ if (DEBUG) {
+ Log.v(TAG, "onWindowFocus: " + viewForWindowFocus
+ + " softInputMode=" + InputMethodDebug.softInputModeToString(
+ windowAttribute.softInputMode));
+ }
+
+ boolean forceFocus = false;
+ final InputMethodManagerDelegate immDelegate = getImmDelegate();
+ if (immDelegate.isRestartOnNextWindowFocus(true /* reset */)) {
+ if (DEBUG) Log.v(TAG, "Restarting due to isRestartOnNextWindowFocus as true");
+ forceFocus = true;
+ }
+
+ // Update mNextServedView when focusedView changed.
+ onViewFocusChanged(viewForWindowFocus, true);
+
+ // Starting new input when the next focused view is same as served view but the currently
+ // active connection (if any) is not associated with it.
+ final boolean nextFocusIsServedView = mServedView == viewForWindowFocus;
+ if (nextFocusIsServedView && !immDelegate.hasActiveConnection(viewForWindowFocus)) {
+ forceFocus = true;
+ }
+
+ immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus,
+ windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
+ }
+
+ public boolean checkFocus(boolean forceNewFocus, boolean startInput) {
+ final InputMethodManagerDelegate immDelegate = getImmDelegate();
+ if (!immDelegate.isCurrentRootView(mViewRootImpl)
+ || (mServedView == mNextServedView && !forceNewFocus)) {
+ return false;
+ }
+ if (DEBUG) Log.v(TAG, "checkFocus: view=" + mServedView
+ + " next=" + mNextServedView
+ + " force=" + forceNewFocus
+ + " package="
+ + (mServedView != null ? mServedView.getContext().getPackageName() : "<none>"));
+
+ // Close the connection when no next served view coming.
+ if (mNextServedView == null) {
+ immDelegate.finishInput();
+ immDelegate.closeCurrentIme();
+ return false;
+ }
+ mServedView = mNextServedView;
+ immDelegate.finishComposingText();
+
+ if (startInput) {
+ immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */,
+ 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
+ }
+ return true;
+ }
+
+ @UiThread
+ void onViewFocusChanged(View view, boolean hasFocus) {
+ if (view == null || view.isTemporarilyDetached()) {
+ return;
+ }
+ if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) {
+ return;
+ }
+ if (!view.hasImeFocus() || !view.hasWindowFocus()) {
+ return;
+ }
+ if (DEBUG) Log.d(TAG, "onViewFocusChanged, view=" + view + ", mServedView=" + mServedView);
+
+ // We don't need to track the next served view when the view lost focus here because:
+ // 1) The current view focus may be cleared temporary when in touch mode, closing input
+ // at this moment isn't the right way.
+ // 2) We only care about the served view change when it focused, since changing input
+ // connection when the focus target changed is reasonable.
+ // 3) Setting the next served view as null when no more served view should be handled in
+ // other special events (e.g. view detached from window or the window dismissed).
+ if (hasFocus) {
+ mNextServedView = view;
+ }
+ mViewRootImpl.dispatchCheckFocus();
+ }
+
+ @UiThread
+ void onViewDetachedFromWindow(View view) {
+ if (!getImmDelegate().isCurrentRootView(view.getViewRootImpl())) {
+ return;
+ }
+ if (mServedView == view) {
+ mNextServedView = null;
+ mViewRootImpl.dispatchCheckFocus();
+ }
+ }
+
+ @UiThread
+ void onWindowDismissed() {
+ final InputMethodManagerDelegate immDelegate = getImmDelegate();
+ if (!immDelegate.isCurrentRootView(mViewRootImpl)) {
+ return;
+ }
+ if (mServedView != null) {
+ immDelegate.finishInput();
+ }
+ immDelegate.setCurrentRootView(null);
+ mHasImeFocus = false;
+ }
+
+ /**
+ * To handle the lifecycle of the input connection when the device interactivity state changed.
+ * (i.e. Calling IMS#onFinishInput when the device screen-off and Calling IMS#onStartInput
+ * when the device screen-on again).
+ */
+ @UiThread
+ public void onInteractiveChanged(boolean interactive) {
+ final InputMethodManagerDelegate immDelegate = getImmDelegate();
+ if (!immDelegate.isCurrentRootView(mViewRootImpl)) {
+ return;
+ }
+ if (interactive) {
+ final View focusedView = mViewRootImpl.mView.findFocus();
+ onViewFocusChanged(focusedView, focusedView != null);
+ } else {
+ mDelegate.finishInputAndReportToIme();
+ }
+ }
+
+ /**
+ * @param windowAttribute {@link WindowManager.LayoutParams} to be checked.
+ * @return Whether the window is in local focus mode or not.
+ */
+ @AnyThread
+ private static boolean isInLocalFocusMode(WindowManager.LayoutParams windowAttribute) {
+ return (windowAttribute.flags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0;
+ }
+
+ int onProcessImeInputStage(Object token, InputEvent event,
+ WindowManager.LayoutParams windowAttribute,
+ InputMethodManager.FinishedInputEventCallback callback) {
+ if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
+ return InputMethodManager.DISPATCH_NOT_HANDLED;
+ }
+ final InputMethodManager imm =
+ mViewRootImpl.mContext.getSystemService(InputMethodManager.class);
+ if (imm == null) {
+ return InputMethodManager.DISPATCH_NOT_HANDLED;
+ }
+ return imm.dispatchInputEvent(event, token, callback, mViewRootImpl.mHandler);
+ }
+
+ /**
+ * A delegate implementing some basic {@link InputMethodManager} APIs.
+ * @hide
+ */
+ public interface InputMethodManagerDelegate {
+ boolean startInput(@StartInputReason int startInputReason, View focusedView,
+ @StartInputFlags int startInputFlags,
+ @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags);
+ void startInputAsyncOnWindowFocusGain(View rootView,
+ @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags,
+ boolean forceNewFocus);
+ void finishInput();
+ void finishInputAndReportToIme();
+ void closeCurrentIme();
+ void finishComposingText();
+ void setCurrentRootView(ViewRootImpl rootView);
+ boolean isCurrentRootView(ViewRootImpl rootView);
+ boolean isRestartOnNextWindowFocus(boolean reset);
+ boolean hasActiveConnection(View view);
+ }
+
+ public View getServedView() {
+ return mServedView;
+ }
+
+ public View getNextServedView() {
+ return mNextServedView;
+ }
+
+ public void setServedView(View view) {
+ mServedView = view;
+ }
+
+ public void setNextServedView(View view) {
+ mNextServedView = view;
+ }
+
+ /**
+ * Indicates whether the view's window has IME focused.
+ */
+ @UiThread
+ boolean hasImeFocus() {
+ return mHasImeFocus;
+ }
+
+ void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(HAS_IME_FOCUS, mHasImeFocus);
+ proto.write(SERVED_VIEW, Objects.toString(mServedView));
+ proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView));
+ proto.end(token);
+ }
+}
diff --git a/android/view/ImeInsetsSourceConsumer.java b/android/view/ImeInsetsSourceConsumer.java
new file mode 100644
index 0000000..0686104
--- /dev/null
+++ b/android/view/ImeInsetsSourceConsumer.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import static android.os.Trace.TRACE_TAG_VIEW;
+import static android.view.ImeInsetsSourceConsumerProto.INSETS_SOURCE_CONSUMER;
+import static android.view.ImeInsetsSourceConsumerProto.IS_REQUESTED_VISIBLE_AWAITING_CONTROL;
+import static android.view.InsetsController.AnimationType;
+import static android.view.InsetsState.ITYPE_IME;
+
+import android.annotation.Nullable;
+import android.inputmethodservice.InputMethodService;
+import android.os.IBinder;
+import android.os.Trace;
+import android.util.proto.ProtoOutputStream;
+import android.view.SurfaceControl.Transaction;
+import android.view.inputmethod.InputMethodManager;
+
+import java.util.function.Supplier;
+
+/**
+ * Controls the visibility and animations of IME window insets source.
+ * @hide
+ */
+public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer {
+
+ /**
+ * Tracks whether we have an outstanding request from the IME to show, but weren't able to
+ * execute it because we didn't have control yet.
+ */
+ private boolean mIsRequestedVisibleAwaitingControl;
+
+ public ImeInsetsSourceConsumer(
+ InsetsState state, Supplier<Transaction> transactionSupplier,
+ InsetsController controller) {
+ super(ITYPE_IME, state, transactionSupplier, controller);
+ }
+
+ @Override
+ public void onWindowFocusGained(boolean hasViewFocus) {
+ super.onWindowFocusGained(hasViewFocus);
+ getImm().registerImeConsumer(this);
+ }
+
+ @Override
+ public void onWindowFocusLost() {
+ super.onWindowFocusLost();
+ getImm().unregisterImeConsumer(this);
+ mIsRequestedVisibleAwaitingControl = false;
+ }
+
+ @Override
+ public void hide() {
+ super.hide();
+ mIsRequestedVisibleAwaitingControl = false;
+ }
+
+ @Override
+ void hide(boolean animationFinished, @AnimationType int animationType) {
+ hide();
+
+ if (animationFinished) {
+ // remove IME surface as IME has finished hide animation.
+ notifyHidden();
+ removeSurface();
+ }
+ }
+
+ /**
+ * Request {@link InputMethodManager} to show the IME.
+ * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}.
+ */
+ @Override
+ public @ShowResult int requestShow(boolean fromIme) {
+ // TODO: ResultReceiver for IME.
+ // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag.
+ if (getControl() == null) {
+ // If control is null, schedule to show IME when control is available.
+ mIsRequestedVisibleAwaitingControl = true;
+ }
+ // If we had a request before to show from IME (tracked with mImeRequestedShow), reaching
+ // this code here means that we now got control, so we can start the animation immediately.
+ // If client window is trying to control IME and IME is already visible, it is immediate.
+ if (fromIme || mState.getSource(getType()).isVisible() && getControl() != null) {
+ return ShowResult.SHOW_IMMEDIATELY;
+ }
+
+ return getImm().requestImeShow(mController.getHost().getWindowToken())
+ ? ShowResult.IME_SHOW_DELAYED : ShowResult.IME_SHOW_FAILED;
+ }
+
+ /**
+ * Notify {@link InputMethodService} that IME window is hidden.
+ */
+ @Override
+ void notifyHidden() {
+ getImm().notifyImeHidden(mController.getHost().getWindowToken());
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0);
+ }
+
+ @Override
+ public void removeSurface() {
+ final IBinder window = mController.getHost().getWindowToken();
+ if (window != null) {
+ getImm().removeImeSurface(window);
+ }
+ }
+
+ @Override
+ public void setControl(@Nullable InsetsSourceControl control, int[] showTypes,
+ int[] hideTypes) {
+ super.setControl(control, showTypes, hideTypes);
+ if (control == null && !mIsRequestedVisibleAwaitingControl) {
+ hide();
+ removeSurface();
+ }
+ if (control != null) {
+ mIsRequestedVisibleAwaitingControl = false;
+ }
+ }
+
+ @Override
+ protected boolean isRequestedVisibleAwaitingControl() {
+ return mIsRequestedVisibleAwaitingControl || isRequestedVisible();
+ }
+
+ @Override
+ public void onPerceptible(boolean perceptible) {
+ super.onPerceptible(perceptible);
+ final IBinder window = mController.getHost().getWindowToken();
+ if (window != null) {
+ getImm().reportPerceptible(window, perceptible);
+ }
+ }
+
+ @Override
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ super.dumpDebug(proto, INSETS_SOURCE_CONSUMER);
+ proto.write(IS_REQUESTED_VISIBLE_AWAITING_CONTROL, mIsRequestedVisibleAwaitingControl);
+ proto.end(token);
+ }
+
+ private InputMethodManager getImm() {
+ return mController.getHost().getInputMethodManager();
+ }
+}
diff --git a/android/view/InflateException.java b/android/view/InflateException.java
new file mode 100644
index 0000000..7b39d33
--- /dev/null
+++ b/android/view/InflateException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * This exception is thrown by an inflater on error conditions.
+ */
+public class InflateException extends RuntimeException {
+
+ public InflateException() {
+ super();
+ }
+
+ public InflateException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public InflateException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public InflateException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git a/android/view/InputApplicationHandle.java b/android/view/InputApplicationHandle.java
new file mode 100644
index 0000000..4abffde
--- /dev/null
+++ b/android/view/InputApplicationHandle.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+
+/**
+ * Functions as a handle for an application that can receive input.
+ * Enables the native input dispatcher to refer indirectly to the window manager's
+ * application window token.
+ * @hide
+ */
+public final class InputApplicationHandle {
+ // Pointer to the native input application handle.
+ // This field is lazily initialized via JNI.
+ @SuppressWarnings("unused")
+ private long ptr;
+
+ // Application name.
+ public final @NonNull String name;
+
+ // Dispatching timeout.
+ public final long dispatchingTimeoutMillis;
+
+ public final @NonNull IBinder token;
+
+ private native void nativeDispose();
+
+ public InputApplicationHandle(@NonNull IBinder token, @NonNull String name,
+ long dispatchingTimeoutMillis) {
+ this.token = token;
+ this.name = name;
+ this.dispatchingTimeoutMillis = dispatchingTimeoutMillis;
+ }
+
+ public InputApplicationHandle(InputApplicationHandle handle) {
+ this.token = handle.token;
+ this.dispatchingTimeoutMillis = handle.dispatchingTimeoutMillis;
+ this.name = handle.name;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nativeDispose();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/android/view/InputChannel.java b/android/view/InputChannel.java
new file mode 100644
index 0000000..f76b1f8
--- /dev/null
+++ b/android/view/InputChannel.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * An input channel specifies the file descriptors used to send input events to
+ * a window in another process. It is Parcelable so that it can be sent
+ * to the process that is to receive events. Only one thread should be reading
+ * from an InputChannel at a time.
+ * @hide
+ */
+public final class InputChannel implements Parcelable {
+ private static final String TAG = "InputChannel";
+
+ private static final boolean DEBUG = false;
+ private static final NativeAllocationRegistry sRegistry =
+ NativeAllocationRegistry.createMalloced(
+ InputChannel.class.getClassLoader(),
+ nativeGetFinalizer());
+
+ @UnsupportedAppUsage
+ public static final @android.annotation.NonNull Parcelable.Creator<InputChannel> CREATOR
+ = new Parcelable.Creator<InputChannel>() {
+ public InputChannel createFromParcel(Parcel source) {
+ InputChannel result = new InputChannel();
+ result.readFromParcel(source);
+ return result;
+ }
+
+ public InputChannel[] newArray(int size) {
+ return new InputChannel[size];
+ }
+ };
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private long mPtr; // used by native code
+
+ private static native long[] nativeOpenInputChannelPair(String name);
+
+ private static native long nativeGetFinalizer();
+ private native void nativeDispose(long channel);
+ private native long nativeReadFromParcel(Parcel parcel);
+ private native void nativeWriteToParcel(Parcel parcel, long channel);
+ private native long nativeDup(long channel);
+ private native IBinder nativeGetToken(long channel);
+
+ private native String nativeGetName(long channel);
+
+ /**
+ * Creates an uninitialized input channel.
+ * It can be initialized by reading from a Parcel or by transferring the state of
+ * another input channel into this one.
+ */
+ @UnsupportedAppUsage
+ public InputChannel() {
+ }
+
+ /**
+ * Set Native input channel object from native space.
+ * @param nativeChannel the native channel object.
+ *
+ * @hide
+ */
+ private void setNativeInputChannel(long nativeChannel) {
+ if (nativeChannel == 0) {
+ throw new IllegalArgumentException("Attempting to set native input channel to null.");
+ }
+ if (mPtr != 0) {
+ throw new IllegalArgumentException("Already has native input channel.");
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "setNativeInputChannel : " + String.format("%x", nativeChannel));
+ }
+ sRegistry.registerNativeAllocation(this, nativeChannel);
+ mPtr = nativeChannel;
+ }
+
+ /**
+ * Creates a new input channel pair. One channel should be provided to the input
+ * dispatcher and the other to the application's input queue.
+ * @param name The descriptive (non-unique) name of the channel pair.
+ * @return A pair of input channels. The first channel is designated as the
+ * server channel and should be used to publish input events. The second channel
+ * is designated as the client channel and should be used to consume input events.
+ */
+ public static InputChannel[] openInputChannelPair(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("name must not be null");
+ }
+
+ if (DEBUG) {
+ Slog.d(TAG, "Opening input channel pair '" + name + "'");
+ }
+ InputChannel channels[] = new InputChannel[2];
+ long[] nativeChannels = nativeOpenInputChannelPair(name);
+ for (int i = 0; i< 2; i++) {
+ channels[i] = new InputChannel();
+ channels[i].setNativeInputChannel(nativeChannels[i]);
+ }
+ return channels;
+ }
+
+ /**
+ * Gets the name of the input channel.
+ * @return The input channel name.
+ */
+ public String getName() {
+ String name = nativeGetName(mPtr);
+ return name != null ? name : "uninitialized";
+ }
+
+ /**
+ * Disposes the input channel.
+ * Explicitly releases the reference this object is holding on the input channel.
+ * When all references are released, the input channel will be closed.
+ */
+ public void dispose() {
+ nativeDispose(mPtr);
+ }
+
+ /**
+ * Release the Java objects hold over the native InputChannel. If other references
+ * still exist in native-land, then the channel may continue to exist.
+ */
+ public void release() {
+ }
+
+ /**
+ * Creates a copy of this instance to the outParameter. This is used to pass an input channel
+ * as an out parameter in a binder call.
+ * @param other The other input channel instance.
+ */
+ public void copyTo(InputChannel outParameter) {
+ if (outParameter == null) {
+ throw new IllegalArgumentException("outParameter must not be null");
+ }
+ if (outParameter.mPtr != 0) {
+ throw new IllegalArgumentException("Other object already has a native input channel.");
+ }
+ outParameter.setNativeInputChannel(nativeDup(mPtr));
+ }
+
+ /**
+ * Duplicates the input channel.
+ */
+ public InputChannel dup() {
+ InputChannel target = new InputChannel();
+ target.setNativeInputChannel(nativeDup(mPtr));
+ return target;
+ }
+
+ @Override
+ public int describeContents() {
+ return Parcelable.CONTENTS_FILE_DESCRIPTOR;
+ }
+
+ public void readFromParcel(Parcel in) {
+ if (in == null) {
+ throw new IllegalArgumentException("in must not be null");
+ }
+ long nativeIn = nativeReadFromParcel(in);
+ if (nativeIn != 0) {
+ setNativeInputChannel(nativeIn);
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (out == null) {
+ throw new IllegalArgumentException("out must not be null");
+ }
+
+ nativeWriteToParcel(out, mPtr);
+
+ if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+ dispose();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ public IBinder getToken() {
+ return nativeGetToken(mPtr);
+ }
+}
diff --git a/android/view/InputDevice.java b/android/view/InputDevice.java
new file mode 100644
index 0000000..4f1354d
--- /dev/null
+++ b/android/view/InputDevice.java
@@ -0,0 +1,1189 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.hardware.BatteryState;
+import android.hardware.SensorManager;
+import android.hardware.input.InputDeviceIdentifier;
+import android.hardware.input.InputManager;
+import android.hardware.lights.LightsManager;
+import android.os.Build;
+import android.os.NullVibrator;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Vibrator;
+import android.os.VibratorManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes the capabilities of a particular input device.
+ * <p>
+ * Each input device may support multiple classes of input. For example, a multi-function
+ * keyboard may compose the capabilities of a standard keyboard together with a track pad mouse
+ * or other pointing device.
+ * </p><p>
+ * Some input devices present multiple distinguishable sources of input.
+ * Applications can query the framework about the characteristics of each distinct source.
+ * </p><p>
+ * As a further wrinkle, different kinds of input sources uses different coordinate systems
+ * to describe motion events. Refer to the comments on the input source constants for
+ * the appropriate interpretation.
+ * </p>
+ */
+public final class InputDevice implements Parcelable {
+ private final int mId;
+ private final int mGeneration;
+ private final int mControllerNumber;
+ private final String mName;
+ private final int mVendorId;
+ private final int mProductId;
+ private final String mDescriptor;
+ private final InputDeviceIdentifier mIdentifier;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private final boolean mIsExternal;
+ private final int mSources;
+ private final int mKeyboardType;
+ private final KeyCharacterMap mKeyCharacterMap;
+ private final boolean mHasVibrator;
+ private final boolean mHasMicrophone;
+ private final boolean mHasButtonUnderPad;
+ private final boolean mHasSensor;
+ private final boolean mHasBattery;
+ private final ArrayList<MotionRange> mMotionRanges = new ArrayList<MotionRange>();
+
+ @GuardedBy("mMotionRanges")
+ private Vibrator mVibrator; // guarded by mMotionRanges during initialization
+
+ @GuardedBy("mMotionRanges")
+ private VibratorManager mVibratorManager;
+
+ @GuardedBy("mMotionRanges")
+ private SensorManager mSensorManager;
+
+ @GuardedBy("mMotionRanges")
+ private BatteryState mBatteryState;
+
+ @GuardedBy("mMotionRanges")
+ private LightsManager mLightsManager;
+
+ /**
+ * A mask for input source classes.
+ *
+ * Each distinct input source constant has one or more input source class bits set to
+ * specify the desired interpretation for its input events.
+ */
+ public static final int SOURCE_CLASS_MASK = 0x000000ff;
+
+ /**
+ * The input source has no class.
+ *
+ * It is up to the application to determine how to handle the device based on the device type.
+ */
+ public static final int SOURCE_CLASS_NONE = 0x00000000;
+
+ /**
+ * The input source has buttons or keys.
+ * Examples: {@link #SOURCE_KEYBOARD}, {@link #SOURCE_DPAD}.
+ *
+ * A {@link KeyEvent} should be interpreted as a button or key press.
+ *
+ * Use {@link #getKeyCharacterMap} to query the device's button and key mappings.
+ */
+ public static final int SOURCE_CLASS_BUTTON = 0x00000001;
+
+ /**
+ * The input source is a pointing device associated with a display.
+ * Examples: {@link #SOURCE_TOUCHSCREEN}, {@link #SOURCE_MOUSE}.
+ *
+ * A {@link MotionEvent} should be interpreted as absolute coordinates in
+ * display units according to the {@link View} hierarchy. Pointer down/up indicated when
+ * the finger touches the display or when the selection button is pressed/released.
+ *
+ * Use {@link #getMotionRange} to query the range of the pointing device. Some devices permit
+ * touches outside the display area so the effective range may be somewhat smaller or larger
+ * than the actual display size.
+ */
+ public static final int SOURCE_CLASS_POINTER = 0x00000002;
+
+ /**
+ * The input source is a trackball navigation device.
+ * Examples: {@link #SOURCE_TRACKBALL}.
+ *
+ * A {@link MotionEvent} should be interpreted as relative movements in device-specific
+ * units used for navigation purposes. Pointer down/up indicates when the selection button
+ * is pressed/released.
+ *
+ * Use {@link #getMotionRange} to query the range of motion.
+ */
+ public static final int SOURCE_CLASS_TRACKBALL = 0x00000004;
+
+ /**
+ * The input source is an absolute positioning device not associated with a display
+ * (unlike {@link #SOURCE_CLASS_POINTER}).
+ *
+ * A {@link MotionEvent} should be interpreted as absolute coordinates in
+ * device-specific surface units.
+ *
+ * Use {@link #getMotionRange} to query the range of positions.
+ */
+ public static final int SOURCE_CLASS_POSITION = 0x00000008;
+
+ /**
+ * The input source is a joystick.
+ *
+ * A {@link MotionEvent} should be interpreted as absolute joystick movements.
+ *
+ * Use {@link #getMotionRange} to query the range of positions.
+ */
+ public static final int SOURCE_CLASS_JOYSTICK = 0x00000010;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SOURCE_CLASS_" }, value = {
+ SOURCE_CLASS_NONE,
+ SOURCE_CLASS_BUTTON,
+ SOURCE_CLASS_POINTER,
+ SOURCE_CLASS_TRACKBALL,
+ SOURCE_CLASS_POSITION,
+ SOURCE_CLASS_JOYSTICK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface InputSourceClass {}
+
+ /**
+ * The input source is unknown.
+ */
+ public static final int SOURCE_UNKNOWN = 0x00000000;
+
+ /**
+ * The input source is a keyboard.
+ *
+ * This source indicates pretty much anything that has buttons. Use
+ * {@link #getKeyboardType()} to determine whether the keyboard has alphabetic keys
+ * and can be used to enter text.
+ *
+ * @see #SOURCE_CLASS_BUTTON
+ */
+ public static final int SOURCE_KEYBOARD = 0x00000100 | SOURCE_CLASS_BUTTON;
+
+ /**
+ * The input source is a DPad.
+ *
+ * @see #SOURCE_CLASS_BUTTON
+ */
+ public static final int SOURCE_DPAD = 0x00000200 | SOURCE_CLASS_BUTTON;
+
+ /**
+ * The input source is a game pad.
+ * (It may also be a {@link #SOURCE_JOYSTICK}).
+ *
+ * @see #SOURCE_CLASS_BUTTON
+ */
+ public static final int SOURCE_GAMEPAD = 0x00000400 | SOURCE_CLASS_BUTTON;
+
+ /**
+ * The input source is a touch screen pointing device.
+ *
+ * @see #SOURCE_CLASS_POINTER
+ */
+ public static final int SOURCE_TOUCHSCREEN = 0x00001000 | SOURCE_CLASS_POINTER;
+
+ /**
+ * The input source is a mouse pointing device.
+ * This code is also used for other mouse-like pointing devices such as trackpads
+ * and trackpoints.
+ *
+ * @see #SOURCE_CLASS_POINTER
+ */
+ public static final int SOURCE_MOUSE = 0x00002000 | SOURCE_CLASS_POINTER;
+
+ /**
+ * The input source is a stylus pointing device.
+ * <p>
+ * Note that this bit merely indicates that an input device is capable of obtaining
+ * input from a stylus. To determine whether a given touch event was produced
+ * by a stylus, examine the tool type returned by {@link MotionEvent#getToolType(int)}
+ * for each individual pointer.
+ * </p><p>
+ * A single touch event may multiple pointers with different tool types,
+ * such as an event that has one pointer with tool type
+ * {@link MotionEvent#TOOL_TYPE_FINGER} and another pointer with tool type
+ * {@link MotionEvent#TOOL_TYPE_STYLUS}. So it is important to examine
+ * the tool type of each pointer, regardless of the source reported
+ * by {@link MotionEvent#getSource()}.
+ * </p>
+ *
+ * @see #SOURCE_CLASS_POINTER
+ */
+ public static final int SOURCE_STYLUS = 0x00004000 | SOURCE_CLASS_POINTER;
+
+ /**
+ * The input device is a Bluetooth stylus.
+ * <p>
+ * Note that this bit merely indicates that an input device is capable of
+ * obtaining input from a Bluetooth stylus. To determine whether a given
+ * touch event was produced by a stylus, examine the tool type returned by
+ * {@link MotionEvent#getToolType(int)} for each individual pointer.
+ * </p><p>
+ * A single touch event may multiple pointers with different tool types,
+ * such as an event that has one pointer with tool type
+ * {@link MotionEvent#TOOL_TYPE_FINGER} and another pointer with tool type
+ * {@link MotionEvent#TOOL_TYPE_STYLUS}. So it is important to examine
+ * the tool type of each pointer, regardless of the source reported
+ * by {@link MotionEvent#getSource()}.
+ * </p><p>
+ * A bluetooth stylus generally receives its pressure and button state
+ * information from the stylus itself, and derives the rest from another
+ * source. For example, a Bluetooth stylus used in conjunction with a
+ * touchscreen would derive its contact position and pointer size from the
+ * touchscreen and may not be any more accurate than other tools such as
+ * fingers.
+ * </p>
+ *
+ * @see #SOURCE_STYLUS
+ * @see #SOURCE_CLASS_POINTER
+ */
+ public static final int SOURCE_BLUETOOTH_STYLUS =
+ 0x00008000 | SOURCE_STYLUS;
+
+ /**
+ * The input source is a trackball.
+ *
+ * @see #SOURCE_CLASS_TRACKBALL
+ */
+ public static final int SOURCE_TRACKBALL = 0x00010000 | SOURCE_CLASS_TRACKBALL;
+
+ /**
+ * The input source is a mouse device whose relative motions should be interpreted as
+ * navigation events.
+ *
+ * @see #SOURCE_CLASS_TRACKBALL
+ */
+ public static final int SOURCE_MOUSE_RELATIVE = 0x00020000 | SOURCE_CLASS_TRACKBALL;
+
+ /**
+ * The input source is a touch pad or digitizer tablet that is not
+ * associated with a display (unlike {@link #SOURCE_TOUCHSCREEN}).
+ *
+ * @see #SOURCE_CLASS_POSITION
+ */
+ public static final int SOURCE_TOUCHPAD = 0x00100000 | SOURCE_CLASS_POSITION;
+
+ /**
+ * The input source is a touch device whose motions should be interpreted as navigation events.
+ *
+ * For example, an upward swipe should be as an upward focus traversal in the same manner as
+ * pressing up on a D-Pad would be. Swipes to the left, right and down should be treated in a
+ * similar manner.
+ *
+ * @see #SOURCE_CLASS_NONE
+ */
+ public static final int SOURCE_TOUCH_NAVIGATION = 0x00200000 | SOURCE_CLASS_NONE;
+
+ /**
+ * The input source is a rotating encoder device whose motions should be interpreted as akin to
+ * those of a scroll wheel.
+ *
+ * @see #SOURCE_CLASS_NONE
+ */
+ public static final int SOURCE_ROTARY_ENCODER = 0x00400000 | SOURCE_CLASS_NONE;
+
+ /**
+ * The input source is a joystick.
+ * (It may also be a {@link #SOURCE_GAMEPAD}).
+ *
+ * @see #SOURCE_CLASS_JOYSTICK
+ */
+ public static final int SOURCE_JOYSTICK = 0x01000000 | SOURCE_CLASS_JOYSTICK;
+
+ /**
+ * The input source is a device connected through HDMI-based bus.
+ *
+ * The key comes in through HDMI-CEC or MHL signal line, and is treated as if it were
+ * generated by a locally connected DPAD or keyboard.
+ */
+ public static final int SOURCE_HDMI = 0x02000000 | SOURCE_CLASS_BUTTON;
+
+ /**
+ * The input source is a sensor associated with the input device.
+ *
+ * @see #SOURCE_CLASS_NONE
+ */
+ public static final int SOURCE_SENSOR = 0x04000000 | SOURCE_CLASS_NONE;
+
+ /**
+ * A special input source constant that is used when filtering input devices
+ * to match devices that provide any type of input source.
+ */
+ public static final int SOURCE_ANY = 0xffffff00;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_X}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_X} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_X = MotionEvent.AXIS_X;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_Y}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_Y} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_Y = MotionEvent.AXIS_Y;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_PRESSURE}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_PRESSURE} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_PRESSURE = MotionEvent.AXIS_PRESSURE;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_SIZE}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_SIZE} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_SIZE = MotionEvent.AXIS_SIZE;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_TOUCH_MAJOR}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_TOUCH_MAJOR} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_TOUCH_MAJOR = MotionEvent.AXIS_TOUCH_MAJOR;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_TOUCH_MINOR}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_TOUCH_MINOR} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_TOUCH_MINOR = MotionEvent.AXIS_TOUCH_MINOR;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_TOOL_MAJOR}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_TOOL_MAJOR} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_TOOL_MAJOR = MotionEvent.AXIS_TOOL_MAJOR;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_TOOL_MINOR}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_TOOL_MINOR} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_TOOL_MINOR = MotionEvent.AXIS_TOOL_MINOR;
+
+ /**
+ * Constant for retrieving the range of values for {@link MotionEvent#AXIS_ORIENTATION}.
+ *
+ * @see #getMotionRange
+ * @deprecated Use {@link MotionEvent#AXIS_ORIENTATION} instead.
+ */
+ @Deprecated
+ public static final int MOTION_RANGE_ORIENTATION = MotionEvent.AXIS_ORIENTATION;
+
+ /**
+ * There is no keyboard.
+ */
+ public static final int KEYBOARD_TYPE_NONE = 0;
+
+ /**
+ * The keyboard is not fully alphabetic. It may be a numeric keypad or an assortment
+ * of buttons that are not mapped as alphabetic keys suitable for text input.
+ */
+ public static final int KEYBOARD_TYPE_NON_ALPHABETIC = 1;
+
+ /**
+ * The keyboard supports a complement of alphabetic keys.
+ */
+ public static final int KEYBOARD_TYPE_ALPHABETIC = 2;
+
+ private static final int MAX_RANGES = 1000;
+
+ private static final int VIBRATOR_ID_ALL = -1;
+
+ public static final @android.annotation.NonNull Parcelable.Creator<InputDevice> CREATOR =
+ new Parcelable.Creator<InputDevice>() {
+ public InputDevice createFromParcel(Parcel in) {
+ return new InputDevice(in);
+ }
+ public InputDevice[] newArray(int size) {
+ return new InputDevice[size];
+ }
+ };
+
+ /**
+ * Called by native code
+ * @hide
+ */
+ @VisibleForTesting
+ public InputDevice(int id, int generation, int controllerNumber, String name, int vendorId,
+ int productId, String descriptor, boolean isExternal, int sources, int keyboardType,
+ KeyCharacterMap keyCharacterMap, boolean hasVibrator, boolean hasMicrophone,
+ boolean hasButtonUnderPad, boolean hasSensor, boolean hasBattery) {
+ mId = id;
+ mGeneration = generation;
+ mControllerNumber = controllerNumber;
+ mName = name;
+ mVendorId = vendorId;
+ mProductId = productId;
+ mDescriptor = descriptor;
+ mIsExternal = isExternal;
+ mSources = sources;
+ mKeyboardType = keyboardType;
+ mKeyCharacterMap = keyCharacterMap;
+ mHasVibrator = hasVibrator;
+ mHasMicrophone = hasMicrophone;
+ mHasButtonUnderPad = hasButtonUnderPad;
+ mHasSensor = hasSensor;
+ mHasBattery = hasBattery;
+ mIdentifier = new InputDeviceIdentifier(descriptor, vendorId, productId);
+ }
+
+ private InputDevice(Parcel in) {
+ mKeyCharacterMap = KeyCharacterMap.CREATOR.createFromParcel(in);
+ mId = in.readInt();
+ mGeneration = in.readInt();
+ mControllerNumber = in.readInt();
+ mName = in.readString();
+ mVendorId = in.readInt();
+ mProductId = in.readInt();
+ mDescriptor = in.readString();
+ mIsExternal = in.readInt() != 0;
+ mSources = in.readInt();
+ mKeyboardType = in.readInt();
+ mHasVibrator = in.readInt() != 0;
+ mHasMicrophone = in.readInt() != 0;
+ mHasButtonUnderPad = in.readInt() != 0;
+ mHasSensor = in.readInt() != 0;
+ mHasBattery = in.readInt() != 0;
+ mIdentifier = new InputDeviceIdentifier(mDescriptor, mVendorId, mProductId);
+
+ int numRanges = in.readInt();
+ if (numRanges > MAX_RANGES) {
+ numRanges = MAX_RANGES;
+ }
+
+ for (int i = 0; i < numRanges; i++) {
+ addMotionRange(in.readInt(), in.readInt(), in.readFloat(), in.readFloat(),
+ in.readFloat(), in.readFloat(), in.readFloat());
+ }
+ }
+
+ /**
+ * Gets information about the input device with the specified id.
+ * @param id The device id.
+ * @return The input device or null if not found.
+ */
+ public static InputDevice getDevice(int id) {
+ return InputManager.getInstance().getInputDevice(id);
+ }
+
+ /**
+ * Gets the ids of all input devices in the system.
+ * @return The input device ids.
+ */
+ public static int[] getDeviceIds() {
+ return InputManager.getInstance().getInputDeviceIds();
+ }
+
+ /**
+ * Gets the input device id.
+ * <p>
+ * Each input device receives a unique id when it is first configured
+ * by the system. The input device id may change when the system is restarted or if the
+ * input device is disconnected, reconnected or reconfigured at any time.
+ * If you require a stable identifier for a device that persists across
+ * boots and reconfigurations, use {@link #getDescriptor()}.
+ * </p>
+ *
+ * @return The input device id.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * The controller number for a given input device.
+ * <p>
+ * Each gamepad or joystick is given a unique, positive controller number when initially
+ * configured by the system. This number may change due to events such as device disconnects /
+ * reconnects or user initiated reassignment. Any change in number will trigger an event that
+ * can be observed by registering an {@link InputManager.InputDeviceListener}.
+ * </p>
+ * <p>
+ * All input devices which are not gamepads or joysticks will be assigned a controller number
+ * of 0.
+ * </p>
+ *
+ * @return The controller number of the device.
+ */
+ public int getControllerNumber() {
+ return mControllerNumber;
+ }
+
+ /**
+ * The set of identifying information for type of input device. This
+ * information can be used by the system to configure appropriate settings
+ * for the device.
+ *
+ * @return The identifier object for this device
+ * @hide
+ */
+ public InputDeviceIdentifier getIdentifier() {
+ return mIdentifier;
+ }
+
+ /**
+ * Gets a generation number for this input device.
+ * The generation number is incremented whenever the device is reconfigured and its
+ * properties may have changed.
+ *
+ * @return The generation number.
+ *
+ * @hide
+ */
+ public int getGeneration() {
+ return mGeneration;
+ }
+
+ /**
+ * Gets the vendor id for the given device, if available.
+ * <p>
+ * A vendor id uniquely identifies the company who manufactured the device. A value of 0 will
+ * be assigned where a vendor id is not available.
+ * </p>
+ *
+ * @return The vendor id of a given device
+ */
+ public int getVendorId() {
+ return mVendorId;
+ }
+
+ /**
+ * Gets the product id for the given device, if available.
+ * <p>
+ * A product id uniquely identifies which product within the address space of a given vendor,
+ * identified by the device's vendor id. A value of 0 will be assigned where a product id is
+ * not available.
+ * </p>
+ *
+ * @return The product id of a given device
+ */
+ public int getProductId() {
+ return mProductId;
+ }
+
+ /**
+ * Gets the input device descriptor, which is a stable identifier for an input device.
+ * <p>
+ * An input device descriptor uniquely identifies an input device. Its value
+ * is intended to be persistent across system restarts, and should not change even
+ * if the input device is disconnected, reconnected or reconfigured at any time.
+ * </p><p>
+ * It is possible for there to be multiple {@link InputDevice} instances that have the
+ * same input device descriptor. This might happen in situations where a single
+ * human input device registers multiple {@link InputDevice} instances (HID collections)
+ * that describe separate features of the device, such as a keyboard that also
+ * has a trackpad. Alternately, it may be that the input devices are simply
+ * indistinguishable, such as two keyboards made by the same manufacturer.
+ * </p><p>
+ * The input device descriptor returned by {@link #getDescriptor} should only be
+ * used when an application needs to remember settings associated with a particular
+ * input device. For all other purposes when referring to a logical
+ * {@link InputDevice} instance at runtime use the id returned by {@link #getId()}.
+ * </p>
+ *
+ * @return The input device descriptor.
+ */
+ public String getDescriptor() {
+ return mDescriptor;
+ }
+
+ /**
+ * Returns true if the device is a virtual input device rather than a real one,
+ * such as the virtual keyboard (see {@link KeyCharacterMap#VIRTUAL_KEYBOARD}).
+ * <p>
+ * Virtual input devices are provided to implement system-level functionality
+ * and should not be seen or configured by users.
+ * </p>
+ *
+ * @return True if the device is virtual.
+ *
+ * @see KeyCharacterMap#VIRTUAL_KEYBOARD
+ */
+ public boolean isVirtual() {
+ return mId < 0;
+ }
+
+ /**
+ * Returns true if the device is external (connected to USB or Bluetooth or some other
+ * peripheral bus), otherwise it is built-in.
+ *
+ * @return True if the device is external.
+ */
+ public boolean isExternal() {
+ return mIsExternal;
+ }
+
+ /**
+ * Returns true if the device is a full keyboard.
+ *
+ * @return True if the device is a full keyboard.
+ *
+ * @hide
+ */
+ public boolean isFullKeyboard() {
+ return (mSources & SOURCE_KEYBOARD) == SOURCE_KEYBOARD
+ && mKeyboardType == KEYBOARD_TYPE_ALPHABETIC;
+ }
+
+ /**
+ * Gets the name of this input device.
+ * @return The input device name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the input sources supported by this input device as a combined bitfield.
+ * @return The supported input sources.
+ */
+ public int getSources() {
+ return mSources;
+ }
+
+ /**
+ * Determines whether the input device supports the given source or sources.
+ *
+ * @param source The input source or sources to check against. This can be a generic device
+ * type such as {@link InputDevice#SOURCE_MOUSE}, a more generic device class, such as
+ * {@link InputDevice#SOURCE_CLASS_POINTER}, or a combination of sources bitwise ORed together.
+ * @return Whether the device can produce all of the given sources.
+ */
+ public boolean supportsSource(int source) {
+ return (mSources & source) == source;
+ }
+
+ /**
+ * Gets the keyboard type.
+ * @return The keyboard type.
+ */
+ public int getKeyboardType() {
+ return mKeyboardType;
+ }
+
+ /**
+ * Gets the key character map associated with this input device.
+ * @return The key character map.
+ */
+ public KeyCharacterMap getKeyCharacterMap() {
+ return mKeyCharacterMap;
+ }
+
+ /**
+ * Gets whether the device is capable of producing the list of keycodes.
+ * @param keys The list of android keycodes to check for.
+ * @return An array of booleans where each member specifies whether the device is capable of
+ * generating the keycode given by the corresponding value at the same index in the keys array.
+ */
+ public boolean[] hasKeys(int... keys) {
+ return InputManager.getInstance().deviceHasKeys(mId, keys);
+ }
+
+ /**
+ * Gets information about the range of values for a particular {@link MotionEvent} axis.
+ * If the device supports multiple sources, the same axis may have different meanings
+ * for each source. Returns information about the first axis found for any source.
+ * To obtain information about the axis for a specific source, use
+ * {@link #getMotionRange(int, int)}.
+ *
+ * @param axis The axis constant.
+ * @return The range of values, or null if the requested axis is not
+ * supported by the device.
+ *
+ * @see MotionEvent#AXIS_X
+ * @see MotionEvent#AXIS_Y
+ */
+ public MotionRange getMotionRange(int axis) {
+ final int numRanges = mMotionRanges.size();
+ for (int i = 0; i < numRanges; i++) {
+ final MotionRange range = mMotionRanges.get(i);
+ if (range.mAxis == axis) {
+ return range;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets information about the range of values for a particular {@link MotionEvent} axis
+ * used by a particular source on the device.
+ * If the device supports multiple sources, the same axis may have different meanings
+ * for each source.
+ *
+ * @param axis The axis constant.
+ * @param source The source for which to return information.
+ * @return The range of values, or null if the requested axis is not
+ * supported by the device.
+ *
+ * @see MotionEvent#AXIS_X
+ * @see MotionEvent#AXIS_Y
+ */
+ public MotionRange getMotionRange(int axis, int source) {
+ final int numRanges = mMotionRanges.size();
+ for (int i = 0; i < numRanges; i++) {
+ final MotionRange range = mMotionRanges.get(i);
+ if (range.mAxis == axis && range.mSource == source) {
+ return range;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the ranges for all axes supported by the device.
+ * @return The motion ranges for the device.
+ *
+ * @see #getMotionRange(int, int)
+ */
+ public List<MotionRange> getMotionRanges() {
+ return mMotionRanges;
+ }
+
+ // Called from native code.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void addMotionRange(int axis, int source,
+ float min, float max, float flat, float fuzz, float resolution) {
+ mMotionRanges.add(new MotionRange(axis, source, min, max, flat, fuzz, resolution));
+ }
+
+ /**
+ * Gets the vibrator service associated with the device, if there is one.
+ * Even if the device does not have a vibrator, the result is never null.
+ * Use {@link Vibrator#hasVibrator} to determine whether a vibrator is
+ * present.
+ *
+ * Note that the vibrator associated with the device may be different from
+ * the system vibrator. To obtain an instance of the system vibrator instead, call
+ * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as argument.
+ *
+ * @return The vibrator service associated with the device, never null.
+ * @deprecated Use {@link #getVibratorManager()} to retrieve the default device vibrator.
+ */
+ @Deprecated
+ public Vibrator getVibrator() {
+ synchronized (mMotionRanges) {
+ if (mVibrator == null) {
+ if (mHasVibrator) {
+ mVibrator = InputManager.getInstance().getInputDeviceVibrator(mId,
+ VIBRATOR_ID_ALL);
+ } else {
+ mVibrator = NullVibrator.getInstance();
+ }
+ }
+ return mVibrator;
+ }
+ }
+
+ /**
+ * Gets the vibrator manager associated with the device.
+ * Even if the device does not have a vibrator manager, the result is never null.
+ * Use {@link VibratorManager#getVibratorIds} to determine whether any vibrator is
+ * present.
+ *
+ * @return The vibrator manager associated with the device, never null.
+ */
+ @NonNull
+ public VibratorManager getVibratorManager() {
+ synchronized (mMotionRanges) {
+ if (mVibratorManager == null) {
+ mVibratorManager = InputManager.getInstance().getInputDeviceVibratorManager(mId);
+ }
+ }
+ return mVibratorManager;
+ }
+
+ /**
+ * Gets the battery state object associated with the device, if there is one.
+ * Even if the device does not have a battery, the result is never null.
+ * Use {@link BatteryState#isPresent} to determine whether a battery is
+ * present.
+ *
+ * @return The battery object associated with the device, never null.
+ */
+ @NonNull
+ public BatteryState getBatteryState() {
+ if (mBatteryState == null) {
+ mBatteryState = InputManager.getInstance().getInputDeviceBatteryState(mId, mHasBattery);
+ }
+ return mBatteryState;
+ }
+
+ /**
+ * Gets the lights manager associated with the device, if there is one.
+ * Even if the device does not have lights, the result is never null.
+ * Use {@link LightsManager#getLights} to determine whether any lights is
+ * present.
+ *
+ * @return The lights manager associated with the device, never null.
+ */
+ public @NonNull LightsManager getLightsManager() {
+ if (mLightsManager == null) {
+ mLightsManager = InputManager.getInstance().getInputDeviceLightsManager(mId);
+ }
+ return mLightsManager;
+ }
+
+ /**
+ * Gets the sensor manager service associated with the input device.
+ * Even if the device does not have a sensor, the result is never null.
+ * Use {@link SensorManager#getSensorList} to get a full list of all supported sensors.
+ *
+ * Note that the sensors associated with the device may be different from
+ * the system sensors, as typically they are builtin sensors physically attached to
+ * input devices.
+ *
+ * @return The sensor manager service associated with the device, never null.
+ */
+ public @NonNull SensorManager getSensorManager() {
+ synchronized (mMotionRanges) {
+ if (mSensorManager == null) {
+ mSensorManager = InputManager.getInstance().getInputDeviceSensorManager(mId);
+ }
+ }
+ return mSensorManager;
+ }
+
+ /**
+ * Returns true if input device is enabled.
+ * @return Whether the input device is enabled.
+ */
+ public boolean isEnabled() {
+ return InputManager.getInstance().isInputDeviceEnabled(mId);
+ }
+
+ /**
+ * Enables the input device.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.DISABLE_INPUT_DEVICE)
+ @TestApi
+ public void enable() {
+ InputManager.getInstance().enableInputDevice(mId);
+ }
+
+ /**
+ * Disables the input device.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.DISABLE_INPUT_DEVICE)
+ @TestApi
+ public void disable() {
+ InputManager.getInstance().disableInputDevice(mId);
+ }
+
+ /**
+ * Reports whether the device has a built-in microphone.
+ * @return Whether the device has a built-in microphone.
+ */
+ public boolean hasMicrophone() {
+ return mHasMicrophone;
+ }
+
+ /**
+ * Reports whether the device has a button under its touchpad
+ * @return Whether the device has a button under its touchpad
+ * @hide
+ */
+ public boolean hasButtonUnderPad() {
+ return mHasButtonUnderPad;
+ }
+
+ /**
+ * Reports whether the device has a sensor.
+ * @return Whether the device has a sensor.
+ * @hide
+ */
+ public boolean hasSensor() {
+ return mHasSensor;
+ }
+
+ /**
+ * Sets the current pointer type.
+ * @param pointerType the type of the pointer icon.
+ * @hide
+ */
+ public void setPointerType(int pointerType) {
+ InputManager.getInstance().setPointerIconType(pointerType);
+ }
+
+ /**
+ * Specifies the current custom pointer.
+ * @param icon the icon data.
+ * @hide
+ */
+ public void setCustomPointerIcon(PointerIcon icon) {
+ InputManager.getInstance().setCustomPointerIcon(icon);
+ }
+
+ /**
+ * Provides information about the range of values for a particular {@link MotionEvent} axis.
+ *
+ * @see InputDevice#getMotionRange(int)
+ */
+ public static final class MotionRange {
+ private int mAxis;
+ private int mSource;
+ private float mMin;
+ private float mMax;
+ private float mFlat;
+ private float mFuzz;
+ private float mResolution;
+
+ private MotionRange(int axis, int source, float min, float max, float flat, float fuzz,
+ float resolution) {
+ mAxis = axis;
+ mSource = source;
+ mMin = min;
+ mMax = max;
+ mFlat = flat;
+ mFuzz = fuzz;
+ mResolution = resolution;
+ }
+
+ /**
+ * Gets the axis id.
+ * @return The axis id.
+ */
+ public int getAxis() {
+ return mAxis;
+ }
+
+ /**
+ * Gets the source for which the axis is defined.
+ * @return The source.
+ */
+ public int getSource() {
+ return mSource;
+ }
+
+
+ /**
+ * Determines whether the event is from the given source.
+ *
+ * @param source The input source to check against. This can be a specific device type,
+ * such as {@link InputDevice#SOURCE_TOUCH_NAVIGATION}, or a more generic device class,
+ * such as {@link InputDevice#SOURCE_CLASS_POINTER}.
+ * @return Whether the event is from the given source.
+ */
+ public boolean isFromSource(int source) {
+ return (getSource() & source) == source;
+ }
+
+ /**
+ * Gets the inclusive minimum value for the axis.
+ * @return The inclusive minimum value.
+ */
+ public float getMin() {
+ return mMin;
+ }
+
+ /**
+ * Gets the inclusive maximum value for the axis.
+ * @return The inclusive maximum value.
+ */
+ public float getMax() {
+ return mMax;
+ }
+
+ /**
+ * Gets the range of the axis (difference between maximum and minimum).
+ * @return The range of values.
+ */
+ public float getRange() {
+ return mMax - mMin;
+ }
+
+ /**
+ * Gets the extent of the center flat position with respect to this axis.
+ * <p>
+ * For example, a flat value of 8 means that the center position is between -8 and +8.
+ * This value is mainly useful for calibrating self-centering devices.
+ * </p>
+ * @return The extent of the center flat position.
+ */
+ public float getFlat() {
+ return mFlat;
+ }
+
+ /**
+ * Gets the error tolerance for input device measurements with respect to this axis.
+ * <p>
+ * For example, a value of 2 indicates that the measured value may be up to +/- 2 units
+ * away from the actual value due to noise and device sensitivity limitations.
+ * </p>
+ * @return The error tolerance.
+ */
+ public float getFuzz() {
+ return mFuzz;
+ }
+
+ /**
+ * Gets the resolution for input device measurements with respect to this axis.
+ * @return The resolution in units per millimeter, or units per radian for rotational axes.
+ */
+ public float getResolution() {
+ return mResolution;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mKeyCharacterMap.writeToParcel(out, flags);
+ out.writeInt(mId);
+ out.writeInt(mGeneration);
+ out.writeInt(mControllerNumber);
+ out.writeString(mName);
+ out.writeInt(mVendorId);
+ out.writeInt(mProductId);
+ out.writeString(mDescriptor);
+ out.writeInt(mIsExternal ? 1 : 0);
+ out.writeInt(mSources);
+ out.writeInt(mKeyboardType);
+ out.writeInt(mHasVibrator ? 1 : 0);
+ out.writeInt(mHasMicrophone ? 1 : 0);
+ out.writeInt(mHasButtonUnderPad ? 1 : 0);
+ out.writeInt(mHasSensor ? 1 : 0);
+ out.writeInt(mHasBattery ? 1 : 0);
+
+ final int numRanges = mMotionRanges.size();
+ out.writeInt(numRanges);
+ for (int i = 0; i < numRanges; i++) {
+ MotionRange range = mMotionRanges.get(i);
+ out.writeInt(range.mAxis);
+ out.writeInt(range.mSource);
+ out.writeFloat(range.mMin);
+ out.writeFloat(range.mMax);
+ out.writeFloat(range.mFlat);
+ out.writeFloat(range.mFuzz);
+ out.writeFloat(range.mResolution);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder description = new StringBuilder();
+ description.append("Input Device ").append(mId).append(": ").append(mName).append("\n");
+ description.append(" Descriptor: ").append(mDescriptor).append("\n");
+ description.append(" Generation: ").append(mGeneration).append("\n");
+ description.append(" Location: ").append(mIsExternal ? "external" : "built-in").append("\n");
+
+ description.append(" Keyboard Type: ");
+ switch (mKeyboardType) {
+ case KEYBOARD_TYPE_NONE:
+ description.append("none");
+ break;
+ case KEYBOARD_TYPE_NON_ALPHABETIC:
+ description.append("non-alphabetic");
+ break;
+ case KEYBOARD_TYPE_ALPHABETIC:
+ description.append("alphabetic");
+ break;
+ }
+ description.append("\n");
+
+ description.append(" Has Vibrator: ").append(mHasVibrator).append("\n");
+
+ description.append(" Has Sensor: ").append(mHasSensor).append("\n");
+
+ description.append(" Has battery: ").append(mHasBattery).append("\n");
+
+ description.append(" Has mic: ").append(mHasMicrophone).append("\n");
+
+ description.append(" Sources: 0x").append(Integer.toHexString(mSources)).append(" (");
+ appendSourceDescriptionIfApplicable(description, SOURCE_KEYBOARD, "keyboard");
+ appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad");
+ appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHSCREEN, "touchscreen");
+ appendSourceDescriptionIfApplicable(description, SOURCE_MOUSE, "mouse");
+ appendSourceDescriptionIfApplicable(description, SOURCE_STYLUS, "stylus");
+ appendSourceDescriptionIfApplicable(description, SOURCE_TRACKBALL, "trackball");
+ appendSourceDescriptionIfApplicable(description, SOURCE_MOUSE_RELATIVE, "mouse_relative");
+ appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHPAD, "touchpad");
+ appendSourceDescriptionIfApplicable(description, SOURCE_JOYSTICK, "joystick");
+ appendSourceDescriptionIfApplicable(description, SOURCE_GAMEPAD, "gamepad");
+ description.append(" )\n");
+
+ final int numAxes = mMotionRanges.size();
+ for (int i = 0; i < numAxes; i++) {
+ MotionRange range = mMotionRanges.get(i);
+ description.append(" ").append(MotionEvent.axisToString(range.mAxis));
+ description.append(": source=0x").append(Integer.toHexString(range.mSource));
+ description.append(" min=").append(range.mMin);
+ description.append(" max=").append(range.mMax);
+ description.append(" flat=").append(range.mFlat);
+ description.append(" fuzz=").append(range.mFuzz);
+ description.append(" resolution=").append(range.mResolution);
+ description.append("\n");
+ }
+ return description.toString();
+ }
+
+ private void appendSourceDescriptionIfApplicable(StringBuilder description, int source,
+ String sourceName) {
+ if ((mSources & source) == source) {
+ description.append(" ");
+ description.append(sourceName);
+ }
+ }
+}
diff --git a/android/view/InputEvent.java b/android/view/InputEvent.java
new file mode 100644
index 0000000..cb9e746
--- /dev/null
+++ b/android/view/InputEvent.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Common base class for input events.
+ */
+public abstract class InputEvent implements Parcelable {
+ /** @hide */
+ protected static final int PARCEL_TOKEN_MOTION_EVENT = 1;
+ /** @hide */
+ protected static final int PARCEL_TOKEN_KEY_EVENT = 2;
+
+ // Next sequence number.
+ private static final AtomicInteger mNextSeq = new AtomicInteger();
+
+ /** @hide */
+ protected int mSeq;
+
+ /** @hide */
+ protected boolean mRecycled;
+
+ private static final boolean TRACK_RECYCLED_LOCATION = false;
+ private RuntimeException mRecycledLocation;
+
+ /*package*/ InputEvent() {
+ mSeq = mNextSeq.getAndIncrement();
+ }
+
+ /**
+ * Gets the id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device
+ * and maps to the default keymap. The other numbers are arbitrary and
+ * you shouldn't depend on the values.
+ *
+ * @return The device id.
+ * @see InputDevice#getDevice
+ */
+ public abstract int getDeviceId();
+
+ /**
+ * Gets the device that this event came from.
+ *
+ * @return The device, or null if unknown.
+ */
+ public final InputDevice getDevice() {
+ return InputDevice.getDevice(getDeviceId());
+ }
+
+ /**
+ * Gets the source of the event.
+ *
+ * @return The event source or {@link InputDevice#SOURCE_UNKNOWN} if unknown.
+ * @see InputDevice#getSources
+ */
+ public abstract int getSource();
+
+ /**
+ * Modifies the source of the event.
+ *
+ * @param source The new source.
+ * @hide
+ */
+ public abstract void setSource(int source);
+
+ /**
+ * Determines whether the event is from the given source.
+ *
+ * @param source The input source to check against. This can be a specific device type, such as
+ * {@link InputDevice#SOURCE_TOUCH_NAVIGATION}, or a more generic device class, such as
+ * {@link InputDevice#SOURCE_CLASS_POINTER}.
+ * @return Whether the event is from the given source.
+ */
+ public boolean isFromSource(int source) {
+ return (getSource() & source) == source;
+ }
+
+ /**
+ * Gets the display id of the event.
+ * @return The display id associated with the event.
+ * @hide
+ */
+ public abstract int getDisplayId();
+
+ /**
+ * Modifies the display id associated with the event
+ * @param displayId
+ * @hide
+ */
+ public abstract void setDisplayId(int displayId);
+ /**
+ * Copies the event.
+ *
+ * @return A deep copy of the event.
+ * @hide
+ */
+ public abstract InputEvent copy();
+
+ /**
+ * Recycles the event.
+ * This method should only be used by the system since applications do not
+ * expect {@link KeyEvent} objects to be recycled, although {@link MotionEvent}
+ * objects are fine. See {@link KeyEvent#recycle()} for details.
+ * @hide
+ */
+ public void recycle() {
+ if (TRACK_RECYCLED_LOCATION) {
+ if (mRecycledLocation != null) {
+ throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation);
+ }
+ mRecycledLocation = new RuntimeException("Last recycled here");
+ } else {
+ if (mRecycled) {
+ throw new RuntimeException(toString() + " recycled twice!");
+ }
+ mRecycled = true;
+ }
+ }
+
+ /**
+ * Conditionally recycled the event if it is appropriate to do so after
+ * dispatching the event to an application.
+ *
+ * If the event is a {@link MotionEvent} then it is recycled.
+ *
+ * If the event is a {@link KeyEvent} then it is NOT recycled, because applications
+ * expect key events to be immutable so once the event has been dispatched to
+ * the application we can no longer recycle it.
+ * @hide
+ */
+ public void recycleIfNeededAfterDispatch() {
+ recycle();
+ }
+
+ /**
+ * Reinitializes the event on reuse (after recycling).
+ * @hide
+ */
+ protected void prepareForReuse() {
+ mRecycled = false;
+ mRecycledLocation = null;
+ mSeq = mNextSeq.getAndIncrement();
+ }
+
+ /**
+ * Gets a private flag that indicates when the system has detected that this input event
+ * may be inconsistent with respect to the sequence of previously delivered input events,
+ * such as when a key up event is sent but the key was not down or when a pointer
+ * move event is sent but the pointer is not down.
+ *
+ * @return True if this event is tainted.
+ * @hide
+ */
+ public abstract boolean isTainted();
+
+ /**
+ * Sets a private flag that indicates when the system has detected that this input event
+ * may be inconsistent with respect to the sequence of previously delivered input events,
+ * such as when a key up event is sent but the key was not down or when a pointer
+ * move event is sent but the pointer is not down.
+ *
+ * @param tainted True if this event is tainted.
+ * @hide
+ */
+ public abstract void setTainted(boolean tainted);
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ */
+ public abstract long getEventTime();
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond (instead of millisecond) precision.
+ * <p>
+ * The value is in nanosecond precision but it may not have nanosecond accuracy.
+ * </p>
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond (instead of millisecond) precision.
+ *
+ * @hide
+ */
+ public abstract long getEventTimeNano();
+
+ /**
+ * Marks the input event as being canceled.
+ *
+ * @hide
+ */
+ public abstract void cancel();
+
+ /**
+ * Gets the unique sequence number of this event.
+ * Every input event that is created or received by a process has a
+ * unique sequence number. Moreover, a new sequence number is obtained
+ * each time an event object is recycled.
+ *
+ * Sequence numbers are only guaranteed to be locally unique within a process.
+ * Sequence numbers are not preserved when events are parceled.
+ *
+ * @return The unique sequence number of this event.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getSequenceNumber() {
+ return mSeq;
+ }
+
+ /**
+ * Gets the ID of this event. This is generated when an event is created and preserved until its
+ * last stage. It won't change just because the event crosses process boundary, but should
+ * change when making a copy with modifications.
+ * <p>
+ * To avoid exposing app usage to other processes this ID is generated from a CSPRNG. Therefore
+ * there isn't 100% guarantee on the uniqueness of this ID, though the chance of ID collisions
+ * is considerably low. The rule of thumb is not to rely on the uniqueness for production logic,
+ * but a good source for tracking an event (e.g. logging and profiling).
+ *
+ * @return The ID of this event.
+ * @hide
+ */
+ public abstract int getId();
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<InputEvent> CREATOR
+ = new Parcelable.Creator<InputEvent>() {
+ public InputEvent createFromParcel(Parcel in) {
+ int token = in.readInt();
+ if (token == PARCEL_TOKEN_KEY_EVENT) {
+ return KeyEvent.createFromParcelBody(in);
+ } else if (token == PARCEL_TOKEN_MOTION_EVENT) {
+ return MotionEvent.createFromParcelBody(in);
+ } else {
+ throw new IllegalStateException("Unexpected input event type token in parcel.");
+ }
+ }
+
+ public InputEvent[] newArray(int size) {
+ return new InputEvent[size];
+ }
+ };
+}
diff --git a/android/view/InputEventAssigner.java b/android/view/InputEventAssigner.java
new file mode 100644
index 0000000..7fac6c5
--- /dev/null
+++ b/android/view/InputEventAssigner.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID;
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+
+/**
+ * Process input events and assign input event id to a specific frame.
+ *
+ * The assigned input event id is determined by where the current gesture is relative to the vsync.
+ * In the middle of the gesture (we already processed some input events, and already received at
+ * least 1 vsync), the latest InputEvent is assigned to the next frame.
+ * If a gesture just started, then the ACTION_DOWN event will be assigned to the next frame.
+ *
+ * Consider the following sequence:
+ * DOWN -> VSYNC 1 -> MOVE 1 -> MOVE 2 -> VSYNC 2.
+ *
+ * For VSYNC 1, we will assign the "DOWN" input event.
+ * For VSYNC 2, we will assign the "MOVE 2" input event.
+ *
+ * Consider another sequence:
+ * DOWN -> MOVE 1 -> MOVE 2 -> VSYNC 1 -> MOVE 3 -> VSYNC 2.
+ *
+ * For VSYNC 1, we will still assign the "DOWN" input event. That means that "MOVE 1" and "MOVE 2"
+ * events are not attributed to any frame.
+ * For VSYNC 2, the "MOVE 3" input event will be assigned.
+ *
+ * @hide
+ */
+public class InputEventAssigner {
+ private static final String TAG = "InputEventAssigner";
+ private boolean mHasUnprocessedDown = false;
+ private int mDownEventId = INVALID_INPUT_EVENT_ID;
+
+ /**
+ * Notify InputEventAssigner that a frame has been processed. We no longer need to keep track of
+ * the DOWN event because a frame has already been produced for it.
+ */
+ public void notifyFrameProcessed() {
+ // Mark completion of this frame. Use newest input event from now on.
+ mHasUnprocessedDown = false;
+ }
+
+ /**
+ * Process the provided input event to determine which event id to assign to the current frame.
+ * @param event the input event currently being processed
+ * @return the id of the input event to use for the current frame
+ */
+ public int processEvent(InputEvent event) {
+ if (event instanceof MotionEvent) {
+ MotionEvent motionEvent = (MotionEvent) event;
+ if (motionEvent.isFromSource(SOURCE_TOUCHSCREEN)) {
+ final int action = motionEvent.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mHasUnprocessedDown = true;
+ mDownEventId = event.getId();
+ }
+ if (mHasUnprocessedDown && action == MotionEvent.ACTION_MOVE) {
+ return mDownEventId;
+ }
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mHasUnprocessedDown = false;
+ }
+ }
+ }
+ return event.getId();
+ }
+}
diff --git a/android/view/InputEventCompatProcessor.java b/android/view/InputEventCompatProcessor.java
new file mode 100644
index 0000000..ff8407a
--- /dev/null
+++ b/android/view/InputEventCompatProcessor.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.os.Build;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Compatibility processor for InputEvents that allows events to be adjusted before and
+ * after it is sent to the application.
+ *
+ * {@hide}
+ */
+public class InputEventCompatProcessor {
+
+ protected Context mContext;
+ protected int mTargetSdkVersion;
+
+ /** List of events to be used to return the processed events */
+ private List<InputEvent> mProcessedEvents;
+
+ public InputEventCompatProcessor(Context context) {
+ mContext = context;
+ mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+ mProcessedEvents = new ArrayList<>();
+ }
+
+ /**
+ * Processes the InputEvent for compatibility before it is sent to the app, allowing for the
+ * generation of more than one event if necessary.
+ *
+ * @param e The InputEvent to process
+ * @return The list of adjusted events, or null if no adjustments are needed. Do not keep a
+ * reference to the output as the list is reused.
+ */
+ public List<InputEvent> processInputEventForCompatibility(InputEvent e) {
+ if (mTargetSdkVersion < Build.VERSION_CODES.M && e instanceof MotionEvent) {
+ mProcessedEvents.clear();
+ MotionEvent motion = (MotionEvent) e;
+ final int mask =
+ MotionEvent.BUTTON_STYLUS_PRIMARY | MotionEvent.BUTTON_STYLUS_SECONDARY;
+ final int buttonState = motion.getButtonState();
+ final int compatButtonState = (buttonState & mask) >> 4;
+ if (compatButtonState != 0) {
+ motion.setButtonState(buttonState | compatButtonState);
+ }
+ mProcessedEvents.add(motion);
+ return mProcessedEvents;
+ }
+ return null;
+ }
+
+ /**
+ * Processes the InputEvent for compatibility before it is finished by calling
+ * InputEventReceiver#finishInputEvent().
+ *
+ * @param e The InputEvent to process
+ * @return The InputEvent to finish, or null if it should not be finished
+ */
+ public InputEvent processInputEventBeforeFinish(InputEvent e) {
+ // No changes needed
+ return e;
+ }
+}
diff --git a/android/view/InputEventConsistencyVerifier.java b/android/view/InputEventConsistencyVerifier.java
new file mode 100644
index 0000000..c0a3cec
--- /dev/null
+++ b/android/view/InputEventConsistencyVerifier.java
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.util.Log;
+
+/**
+ * Checks whether a sequence of input events is self-consistent.
+ * Logs a description of each problem detected.
+ * <p>
+ * When a problem is detected, the event is tainted. This mechanism prevents the same
+ * error from being reported multiple times.
+ * </p>
+ *
+ * @hide
+ */
+public final class InputEventConsistencyVerifier {
+ private static final boolean IS_ENG_BUILD = Build.IS_ENG;
+
+ private static final String EVENT_TYPE_KEY = "KeyEvent";
+ private static final String EVENT_TYPE_TRACKBALL = "TrackballEvent";
+ private static final String EVENT_TYPE_TOUCH = "TouchEvent";
+ private static final String EVENT_TYPE_GENERIC_MOTION = "GenericMotionEvent";
+
+ // The number of recent events to log when a problem is detected.
+ // Can be set to 0 to disable logging recent events but the runtime overhead of
+ // this feature is negligible on current hardware.
+ private static final int RECENT_EVENTS_TO_LOG = 5;
+
+ // The object to which the verifier is attached.
+ private final Object mCaller;
+
+ // Consistency verifier flags.
+ private final int mFlags;
+
+ // Tag for logging which a client can set to help distinguish the output
+ // from different verifiers since several can be active at the same time.
+ // If not provided defaults to the simple class name.
+ private final String mLogTag;
+
+ // The most recently checked event and the nesting level at which it was checked.
+ // This is only set when the verifier is called from a nesting level greater than 0
+ // so that the verifier can detect when it has been asked to verify the same event twice.
+ // It does not make sense to examine the contents of the last event since it may have
+ // been recycled.
+ private int mLastEventSeq;
+ private String mLastEventType;
+ private int mLastNestingLevel;
+
+ // Copy of the most recent events.
+ private InputEvent[] mRecentEvents;
+ private boolean[] mRecentEventsUnhandled;
+ private int mMostRecentEventIndex;
+
+ // Current event and its type.
+ private InputEvent mCurrentEvent;
+ private String mCurrentEventType;
+
+ // Linked list of key state objects.
+ private KeyState mKeyStateList;
+
+ // Current state of the trackball.
+ private boolean mTrackballDown;
+ private boolean mTrackballUnhandled;
+
+ // Bitfield of pointer ids that are currently down.
+ // Assumes that the largest possible pointer id is 31, which is potentially subject to change.
+ // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h)
+ private int mTouchEventStreamPointers;
+
+ // The device id and source of the current stream of touch events.
+ private int mTouchEventStreamDeviceId = -1;
+ private int mTouchEventStreamSource;
+
+ // Set to true when we discover that the touch event stream is inconsistent.
+ // Reset on down or cancel.
+ private boolean mTouchEventStreamIsTainted;
+
+ // Set to true if the touch event stream is partially unhandled.
+ private boolean mTouchEventStreamUnhandled;
+
+ // Set to true if we received hover enter.
+ private boolean mHoverEntered;
+
+ // The bitset of buttons which we've received ACTION_BUTTON_PRESS for.
+ private int mButtonsPressed;
+
+ // The current violation message.
+ private StringBuilder mViolationMessage;
+
+ /**
+ * Indicates that the verifier is intended to act on raw device input event streams.
+ * Disables certain checks for invariants that are established by the input dispatcher
+ * itself as it delivers input events, such as key repeating behavior.
+ */
+ public static final int FLAG_RAW_DEVICE_INPUT = 1 << 0;
+
+ /**
+ * Creates an input consistency verifier.
+ * @param caller The object to which the verifier is attached.
+ * @param flags Flags to the verifier, or 0 if none.
+ */
+ @UnsupportedAppUsage
+ public InputEventConsistencyVerifier(Object caller, int flags) {
+ this(caller, flags, null);
+ }
+
+ /**
+ * Creates an input consistency verifier.
+ * @param caller The object to which the verifier is attached.
+ * @param flags Flags to the verifier, or 0 if none.
+ * @param logTag Tag for logging. If null defaults to the short class name.
+ */
+ public InputEventConsistencyVerifier(Object caller, int flags, String logTag) {
+ this.mCaller = caller;
+ this.mFlags = flags;
+ this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier";
+ }
+
+ /**
+ * Determines whether the instrumentation should be enabled.
+ * @return True if it should be enabled.
+ */
+ @UnsupportedAppUsage
+ public static boolean isInstrumentationEnabled() {
+ return IS_ENG_BUILD;
+ }
+
+ /**
+ * Resets the state of the input event consistency verifier.
+ */
+ public void reset() {
+ mLastEventSeq = -1;
+ mLastNestingLevel = 0;
+ mTrackballDown = false;
+ mTrackballUnhandled = false;
+ mTouchEventStreamPointers = 0;
+ mTouchEventStreamIsTainted = false;
+ mTouchEventStreamUnhandled = false;
+ mHoverEntered = false;
+ mButtonsPressed = 0;
+
+ while (mKeyStateList != null) {
+ final KeyState state = mKeyStateList;
+ mKeyStateList = state.next;
+ state.recycle();
+ }
+ }
+
+ /**
+ * Checks an arbitrary input event.
+ * @param event The event.
+ * @param nestingLevel The nesting level: 0 if called from the base class,
+ * or 1 from a subclass. If the event was already checked by this consistency verifier
+ * at a higher nesting level, it will not be checked again. Used to handle the situation
+ * where a subclass dispatching method delegates to its superclass's dispatching method
+ * and both dispatching methods call into the consistency verifier.
+ */
+ public void onInputEvent(InputEvent event, int nestingLevel) {
+ if (event instanceof KeyEvent) {
+ final KeyEvent keyEvent = (KeyEvent)event;
+ onKeyEvent(keyEvent, nestingLevel);
+ } else {
+ final MotionEvent motionEvent = (MotionEvent)event;
+ if (motionEvent.isTouchEvent()) {
+ onTouchEvent(motionEvent, nestingLevel);
+ } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ onTrackballEvent(motionEvent, nestingLevel);
+ } else {
+ onGenericMotionEvent(motionEvent, nestingLevel);
+ }
+ }
+ }
+
+ /**
+ * Checks a key event.
+ * @param event The event.
+ * @param nestingLevel The nesting level: 0 if called from the base class,
+ * or 1 from a subclass. If the event was already checked by this consistency verifier
+ * at a higher nesting level, it will not be checked again. Used to handle the situation
+ * where a subclass dispatching method delegates to its superclass's dispatching method
+ * and both dispatching methods call into the consistency verifier.
+ */
+ public void onKeyEvent(KeyEvent event, int nestingLevel) {
+ if (!startEvent(event, nestingLevel, EVENT_TYPE_KEY)) {
+ return;
+ }
+
+ try {
+ ensureMetaStateIsNormalized(event.getMetaState());
+
+ final int action = event.getAction();
+ final int deviceId = event.getDeviceId();
+ final int source = event.getSource();
+ final int keyCode = event.getKeyCode();
+ switch (action) {
+ case KeyEvent.ACTION_DOWN: {
+ KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false);
+ if (state != null) {
+ // If the key is already down, ensure it is a repeat.
+ // We don't perform this check when processing raw device input
+ // because the input dispatcher itself is responsible for setting
+ // the key repeat count before it delivers input events.
+ if (state.unhandled) {
+ state.unhandled = false;
+ } else if ((mFlags & FLAG_RAW_DEVICE_INPUT) == 0
+ && event.getRepeatCount() == 0) {
+ problem("ACTION_DOWN but key is already down and this event "
+ + "is not a key repeat.");
+ }
+ } else {
+ addKeyState(deviceId, source, keyCode);
+ }
+ break;
+ }
+ case KeyEvent.ACTION_UP: {
+ KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ true);
+ if (state == null) {
+ problem("ACTION_UP but key was not down.");
+ } else {
+ state.recycle();
+ }
+ break;
+ }
+ case KeyEvent.ACTION_MULTIPLE:
+ break;
+ default:
+ problem("Invalid action " + KeyEvent.actionToString(action)
+ + " for key event.");
+ break;
+ }
+ } finally {
+ finishEvent();
+ }
+ }
+
+ /**
+ * Checks a trackball event.
+ * @param event The event.
+ * @param nestingLevel The nesting level: 0 if called from the base class,
+ * or 1 from a subclass. If the event was already checked by this consistency verifier
+ * at a higher nesting level, it will not be checked again. Used to handle the situation
+ * where a subclass dispatching method delegates to its superclass's dispatching method
+ * and both dispatching methods call into the consistency verifier.
+ */
+ public void onTrackballEvent(MotionEvent event, int nestingLevel) {
+ if (!startEvent(event, nestingLevel, EVENT_TYPE_TRACKBALL)) {
+ return;
+ }
+
+ try {
+ ensureMetaStateIsNormalized(event.getMetaState());
+
+ final int action = event.getAction();
+ final int source = event.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (mTrackballDown && !mTrackballUnhandled) {
+ problem("ACTION_DOWN but trackball is already down.");
+ } else {
+ mTrackballDown = true;
+ mTrackballUnhandled = false;
+ }
+ ensureHistorySizeIsZeroForThisAction(event);
+ ensurePointerCountIsOneForThisAction(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ if (!mTrackballDown) {
+ problem("ACTION_UP but trackball is not down.");
+ } else {
+ mTrackballDown = false;
+ mTrackballUnhandled = false;
+ }
+ ensureHistorySizeIsZeroForThisAction(event);
+ ensurePointerCountIsOneForThisAction(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ ensurePointerCountIsOneForThisAction(event);
+ break;
+ default:
+ problem("Invalid action " + MotionEvent.actionToString(action)
+ + " for trackball event.");
+ break;
+ }
+
+ if (mTrackballDown && event.getPressure() <= 0) {
+ problem("Trackball is down but pressure is not greater than 0.");
+ } else if (!mTrackballDown && event.getPressure() != 0) {
+ problem("Trackball is up but pressure is not equal to 0.");
+ }
+ } else {
+ problem("Source was not SOURCE_CLASS_TRACKBALL.");
+ }
+ } finally {
+ finishEvent();
+ }
+ }
+
+ /**
+ * Checks a touch event.
+ * @param event The event.
+ * @param nestingLevel The nesting level: 0 if called from the base class,
+ * or 1 from a subclass. If the event was already checked by this consistency verifier
+ * at a higher nesting level, it will not be checked again. Used to handle the situation
+ * where a subclass dispatching method delegates to its superclass's dispatching method
+ * and both dispatching methods call into the consistency verifier.
+ */
+ @UnsupportedAppUsage
+ public void onTouchEvent(MotionEvent event, int nestingLevel) {
+ if (!startEvent(event, nestingLevel, EVENT_TYPE_TOUCH)) {
+ return;
+ }
+
+ final int action = event.getAction();
+ final boolean newStream = action == MotionEvent.ACTION_DOWN
+ || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_OUTSIDE;
+ if (newStream && (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled)) {
+ mTouchEventStreamIsTainted = false;
+ mTouchEventStreamUnhandled = false;
+ mTouchEventStreamPointers = 0;
+ }
+ if (mTouchEventStreamIsTainted) {
+ event.setTainted(true);
+ }
+
+ try {
+ ensureMetaStateIsNormalized(event.getMetaState());
+
+ final int deviceId = event.getDeviceId();
+ final int source = event.getSource();
+
+ if (!newStream && mTouchEventStreamDeviceId != -1
+ && (mTouchEventStreamDeviceId != deviceId
+ || mTouchEventStreamSource != source)) {
+ problem("Touch event stream contains events from multiple sources: "
+ + "previous device id " + mTouchEventStreamDeviceId
+ + ", previous source " + Integer.toHexString(mTouchEventStreamSource)
+ + ", new device id " + deviceId
+ + ", new source " + Integer.toHexString(source));
+ }
+ mTouchEventStreamDeviceId = deviceId;
+ mTouchEventStreamSource = source;
+
+ final int pointerCount = event.getPointerCount();
+ if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (mTouchEventStreamPointers != 0) {
+ problem("ACTION_DOWN but pointers are already down. "
+ + "Probably missing ACTION_UP from previous gesture.");
+ }
+ ensureHistorySizeIsZeroForThisAction(event);
+ ensurePointerCountIsOneForThisAction(event);
+ mTouchEventStreamPointers = 1 << event.getPointerId(0);
+ break;
+ case MotionEvent.ACTION_UP:
+ ensureHistorySizeIsZeroForThisAction(event);
+ ensurePointerCountIsOneForThisAction(event);
+ mTouchEventStreamPointers = 0;
+ mTouchEventStreamIsTainted = false;
+ break;
+ case MotionEvent.ACTION_MOVE: {
+ final int expectedPointerCount =
+ Integer.bitCount(mTouchEventStreamPointers);
+ if (pointerCount != expectedPointerCount) {
+ problem("ACTION_MOVE contained " + pointerCount
+ + " pointers but there are currently "
+ + expectedPointerCount + " pointers down.");
+ mTouchEventStreamIsTainted = true;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ mTouchEventStreamPointers = 0;
+ mTouchEventStreamIsTainted = false;
+ break;
+ case MotionEvent.ACTION_OUTSIDE:
+ if (mTouchEventStreamPointers != 0) {
+ problem("ACTION_OUTSIDE but pointers are still down.");
+ }
+ ensureHistorySizeIsZeroForThisAction(event);
+ ensurePointerCountIsOneForThisAction(event);
+ mTouchEventStreamIsTainted = false;
+ break;
+ default: {
+ final int actionMasked = event.getActionMasked();
+ final int actionIndex = event.getActionIndex();
+ if (actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
+ if (mTouchEventStreamPointers == 0) {
+ problem("ACTION_POINTER_DOWN but no other pointers were down.");
+ mTouchEventStreamIsTainted = true;
+ }
+ if (actionIndex < 0 || actionIndex >= pointerCount) {
+ problem("ACTION_POINTER_DOWN index is " + actionIndex
+ + " but the pointer count is " + pointerCount + ".");
+ mTouchEventStreamIsTainted = true;
+ } else {
+ final int id = event.getPointerId(actionIndex);
+ final int idBit = 1 << id;
+ if ((mTouchEventStreamPointers & idBit) != 0) {
+ problem("ACTION_POINTER_DOWN specified pointer id " + id
+ + " which is already down.");
+ mTouchEventStreamIsTainted = true;
+ } else {
+ mTouchEventStreamPointers |= idBit;
+ }
+ }
+ ensureHistorySizeIsZeroForThisAction(event);
+ } else if (actionMasked == MotionEvent.ACTION_POINTER_UP) {
+ if (actionIndex < 0 || actionIndex >= pointerCount) {
+ problem("ACTION_POINTER_UP index is " + actionIndex
+ + " but the pointer count is " + pointerCount + ".");
+ mTouchEventStreamIsTainted = true;
+ } else {
+ final int id = event.getPointerId(actionIndex);
+ final int idBit = 1 << id;
+ if ((mTouchEventStreamPointers & idBit) == 0) {
+ problem("ACTION_POINTER_UP specified pointer id " + id
+ + " which is not currently down.");
+ mTouchEventStreamIsTainted = true;
+ } else {
+ mTouchEventStreamPointers &= ~idBit;
+ }
+ }
+ ensureHistorySizeIsZeroForThisAction(event);
+ } else {
+ problem("Invalid action " + MotionEvent.actionToString(action)
+ + " for touch event.");
+ }
+ break;
+ }
+ }
+ } else {
+ problem("Source was not SOURCE_CLASS_POINTER.");
+ }
+ } finally {
+ finishEvent();
+ }
+ }
+
+ /**
+ * Checks a generic motion event.
+ * @param event The event.
+ * @param nestingLevel The nesting level: 0 if called from the base class,
+ * or 1 from a subclass. If the event was already checked by this consistency verifier
+ * at a higher nesting level, it will not be checked again. Used to handle the situation
+ * where a subclass dispatching method delegates to its superclass's dispatching method
+ * and both dispatching methods call into the consistency verifier.
+ */
+ public void onGenericMotionEvent(MotionEvent event, int nestingLevel) {
+ if (!startEvent(event, nestingLevel, EVENT_TYPE_GENERIC_MOTION)) {
+ return;
+ }
+
+ try {
+ ensureMetaStateIsNormalized(event.getMetaState());
+
+ final int action = event.getAction();
+ final int source = event.getSource();
+ final int buttonState = event.getButtonState();
+ final int actionButton = event.getActionButton();
+ if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ ensurePointerCountIsOneForThisAction(event);
+ mHoverEntered = true;
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ ensurePointerCountIsOneForThisAction(event);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ ensurePointerCountIsOneForThisAction(event);
+ if (!mHoverEntered) {
+ problem("ACTION_HOVER_EXIT without prior ACTION_HOVER_ENTER");
+ }
+ mHoverEntered = false;
+ break;
+ case MotionEvent.ACTION_SCROLL:
+ ensureHistorySizeIsZeroForThisAction(event);
+ ensurePointerCountIsOneForThisAction(event);
+ break;
+ case MotionEvent.ACTION_BUTTON_PRESS:
+ ensureActionButtonIsNonZeroForThisAction(event);
+ if ((mButtonsPressed & actionButton) != 0) {
+ problem("Action button for ACTION_BUTTON_PRESS event is " +
+ actionButton + ", but it has already been pressed and " +
+ "has yet to be released.");
+ }
+
+ mButtonsPressed |= actionButton;
+ // The system will automatically mirror the stylus buttons onto the button
+ // state as the old set of generic buttons for apps targeting pre-M. If
+ // it looks this has happened, go ahead and set the generic buttons as
+ // pressed to prevent spurious errors.
+ if (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY &&
+ (buttonState & MotionEvent.BUTTON_SECONDARY) != 0) {
+ mButtonsPressed |= MotionEvent.BUTTON_SECONDARY;
+ } else if (actionButton == MotionEvent.BUTTON_STYLUS_SECONDARY &&
+ (buttonState & MotionEvent.BUTTON_TERTIARY) != 0) {
+ mButtonsPressed |= MotionEvent.BUTTON_TERTIARY;
+ }
+
+ if (mButtonsPressed != buttonState) {
+ problem(String.format("Reported button state differs from " +
+ "expected button state based on press and release events. " +
+ "Is 0x%08x but expected 0x%08x.",
+ buttonState, mButtonsPressed));
+ }
+ break;
+ case MotionEvent.ACTION_BUTTON_RELEASE:
+ ensureActionButtonIsNonZeroForThisAction(event);
+ if ((mButtonsPressed & actionButton) != actionButton) {
+ problem("Action button for ACTION_BUTTON_RELEASE event is " +
+ actionButton + ", but it was either never pressed or has " +
+ "already been released.");
+ }
+
+ mButtonsPressed &= ~actionButton;
+ // The system will automatically mirror the stylus buttons onto the button
+ // state as the old set of generic buttons for apps targeting pre-M. If
+ // it looks this has happened, go ahead and set the generic buttons as
+ // released to prevent spurious errors.
+ if (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY &&
+ (buttonState & MotionEvent.BUTTON_SECONDARY) == 0) {
+ mButtonsPressed &= ~MotionEvent.BUTTON_SECONDARY;
+ } else if (actionButton == MotionEvent.BUTTON_STYLUS_SECONDARY &&
+ (buttonState & MotionEvent.BUTTON_TERTIARY) == 0) {
+ mButtonsPressed &= ~MotionEvent.BUTTON_TERTIARY;
+ }
+
+ if (mButtonsPressed != buttonState) {
+ problem(String.format("Reported button state differs from " +
+ "expected button state based on press and release events. " +
+ "Is 0x%08x but expected 0x%08x.",
+ buttonState, mButtonsPressed));
+ }
+ break;
+ default:
+ problem("Invalid action for generic pointer event.");
+ break;
+ }
+ } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ ensurePointerCountIsOneForThisAction(event);
+ break;
+ default:
+ problem("Invalid action for generic joystick event.");
+ break;
+ }
+ }
+ } finally {
+ finishEvent();
+ }
+ }
+
+ /**
+ * Notifies the verifier that a given event was unhandled and the rest of the
+ * trace for the event should be ignored.
+ * This method should only be called if the event was previously checked by
+ * the consistency verifier using {@link #onInputEvent} and other methods.
+ * @param event The event.
+ * @param nestingLevel The nesting level: 0 if called from the base class,
+ * or 1 from a subclass. If the event was already checked by this consistency verifier
+ * at a higher nesting level, it will not be checked again. Used to handle the situation
+ * where a subclass dispatching method delegates to its superclass's dispatching method
+ * and both dispatching methods call into the consistency verifier.
+ */
+ @UnsupportedAppUsage
+ public void onUnhandledEvent(InputEvent event, int nestingLevel) {
+ if (nestingLevel != mLastNestingLevel) {
+ return;
+ }
+
+ if (mRecentEventsUnhandled != null) {
+ mRecentEventsUnhandled[mMostRecentEventIndex] = true;
+ }
+
+ if (event instanceof KeyEvent) {
+ final KeyEvent keyEvent = (KeyEvent)event;
+ final int deviceId = keyEvent.getDeviceId();
+ final int source = keyEvent.getSource();
+ final int keyCode = keyEvent.getKeyCode();
+ final KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false);
+ if (state != null) {
+ state.unhandled = true;
+ }
+ } else {
+ final MotionEvent motionEvent = (MotionEvent)event;
+ if (motionEvent.isTouchEvent()) {
+ mTouchEventStreamUnhandled = true;
+ } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ if (mTrackballDown) {
+ mTrackballUnhandled = true;
+ }
+ }
+ }
+ }
+
+ private void ensureMetaStateIsNormalized(int metaState) {
+ final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState);
+ if (normalizedMetaState != metaState) {
+ problem(String.format("Metastate not normalized. Was 0x%08x but expected 0x%08x.",
+ metaState, normalizedMetaState));
+ }
+ }
+
+ private void ensurePointerCountIsOneForThisAction(MotionEvent event) {
+ final int pointerCount = event.getPointerCount();
+ if (pointerCount != 1) {
+ problem("Pointer count is " + pointerCount + " but it should always be 1 for "
+ + MotionEvent.actionToString(event.getAction()));
+ }
+ }
+
+ private void ensureActionButtonIsNonZeroForThisAction(MotionEvent event) {
+ final int actionButton = event.getActionButton();
+ if (actionButton == 0) {
+ problem("No action button set. Action button should always be non-zero for " +
+ MotionEvent.actionToString(event.getAction()));
+
+ }
+ }
+
+ private void ensureHistorySizeIsZeroForThisAction(MotionEvent event) {
+ final int historySize = event.getHistorySize();
+ if (historySize != 0) {
+ problem("History size is " + historySize + " but it should always be 0 for "
+ + MotionEvent.actionToString(event.getAction()));
+ }
+ }
+
+ private boolean startEvent(InputEvent event, int nestingLevel, String eventType) {
+ // Ignore the event if we already checked it at a higher nesting level.
+ final int seq = event.getSequenceNumber();
+ if (seq == mLastEventSeq && nestingLevel < mLastNestingLevel
+ && eventType == mLastEventType) {
+ return false;
+ }
+
+ if (nestingLevel > 0) {
+ mLastEventSeq = seq;
+ mLastEventType = eventType;
+ mLastNestingLevel = nestingLevel;
+ } else {
+ mLastEventSeq = -1;
+ mLastEventType = null;
+ mLastNestingLevel = 0;
+ }
+
+ mCurrentEvent = event;
+ mCurrentEventType = eventType;
+ return true;
+ }
+
+ private void finishEvent() {
+ if (mViolationMessage != null && mViolationMessage.length() != 0) {
+ if (!mCurrentEvent.isTainted()) {
+ // Write a log message only if the event was not already tainted.
+ mViolationMessage.append("\n in ").append(mCaller);
+ mViolationMessage.append("\n ");
+ appendEvent(mViolationMessage, 0, mCurrentEvent, false);
+
+ if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) {
+ mViolationMessage.append("\n -- recent events --");
+ for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) {
+ final int index = (mMostRecentEventIndex + RECENT_EVENTS_TO_LOG - i)
+ % RECENT_EVENTS_TO_LOG;
+ final InputEvent event = mRecentEvents[index];
+ if (event == null) {
+ break;
+ }
+ mViolationMessage.append("\n ");
+ appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]);
+ }
+ }
+
+ Log.d(mLogTag, mViolationMessage.toString());
+
+ // Taint the event so that we do not generate additional violations from it
+ // further downstream.
+ mCurrentEvent.setTainted(true);
+ }
+ mViolationMessage.setLength(0);
+ }
+
+ if (RECENT_EVENTS_TO_LOG != 0) {
+ if (mRecentEvents == null) {
+ mRecentEvents = new InputEvent[RECENT_EVENTS_TO_LOG];
+ mRecentEventsUnhandled = new boolean[RECENT_EVENTS_TO_LOG];
+ }
+ final int index = (mMostRecentEventIndex + 1) % RECENT_EVENTS_TO_LOG;
+ mMostRecentEventIndex = index;
+ if (mRecentEvents[index] != null) {
+ mRecentEvents[index].recycle();
+ }
+ mRecentEvents[index] = mCurrentEvent.copy();
+ mRecentEventsUnhandled[index] = false;
+ }
+
+ mCurrentEvent = null;
+ mCurrentEventType = null;
+ }
+
+ private static void appendEvent(StringBuilder message, int index,
+ InputEvent event, boolean unhandled) {
+ message.append(index).append(": sent at ").append(event.getEventTimeNano());
+ message.append(", ");
+ if (unhandled) {
+ message.append("(unhandled) ");
+ }
+ message.append(event);
+ }
+
+ private void problem(String message) {
+ if (mViolationMessage == null) {
+ mViolationMessage = new StringBuilder();
+ }
+ if (mViolationMessage.length() == 0) {
+ mViolationMessage.append(mCurrentEventType).append(": ");
+ } else {
+ mViolationMessage.append("\n ");
+ }
+ mViolationMessage.append(message);
+ }
+
+ private KeyState findKeyState(int deviceId, int source, int keyCode, boolean remove) {
+ KeyState last = null;
+ KeyState state = mKeyStateList;
+ while (state != null) {
+ if (state.deviceId == deviceId && state.source == source
+ && state.keyCode == keyCode) {
+ if (remove) {
+ if (last != null) {
+ last.next = state.next;
+ } else {
+ mKeyStateList = state.next;
+ }
+ state.next = null;
+ }
+ return state;
+ }
+ last = state;
+ state = state.next;
+ }
+ return null;
+ }
+
+ private void addKeyState(int deviceId, int source, int keyCode) {
+ KeyState state = KeyState.obtain(deviceId, source, keyCode);
+ state.next = mKeyStateList;
+ mKeyStateList = state;
+ }
+
+ private static final class KeyState {
+ private static Object mRecycledListLock = new Object();
+ private static KeyState mRecycledList;
+
+ public KeyState next;
+ public int deviceId;
+ public int source;
+ public int keyCode;
+ public boolean unhandled;
+
+ private KeyState() {
+ }
+
+ public static KeyState obtain(int deviceId, int source, int keyCode) {
+ KeyState state;
+ synchronized (mRecycledListLock) {
+ state = mRecycledList;
+ if (state != null) {
+ mRecycledList = state.next;
+ } else {
+ state = new KeyState();
+ }
+ }
+ state.deviceId = deviceId;
+ state.source = source;
+ state.keyCode = keyCode;
+ state.unhandled = false;
+ return state;
+ }
+
+ public void recycle() {
+ synchronized (mRecycledListLock) {
+ next = mRecycledList;
+ mRecycledList = next;
+ }
+ }
+ }
+}
diff --git a/android/view/InputEventReceiver.java b/android/view/InputEventReceiver.java
new file mode 100644
index 0000000..25dda5b
--- /dev/null
+++ b/android/view/InputEventReceiver.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.os.Trace;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import dalvik.system.CloseGuard;
+
+import java.io.PrintWriter;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides a low-level mechanism for an application to receive input events.
+ * @hide
+ */
+public abstract class InputEventReceiver {
+ private static final String TAG = "InputEventReceiver";
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private long mReceiverPtr;
+
+ // We keep references to the input channel and message queue objects here so that
+ // they are not GC'd while the native peer of the receiver is using them.
+ private InputChannel mInputChannel;
+ private MessageQueue mMessageQueue;
+
+ // Map from InputEvent sequence numbers to dispatcher sequence numbers.
+ private final SparseIntArray mSeqMap = new SparseIntArray();
+
+ private static native long nativeInit(WeakReference<InputEventReceiver> receiver,
+ InputChannel inputChannel, MessageQueue messageQueue);
+ private static native void nativeDispose(long receiverPtr);
+ private static native void nativeFinishInputEvent(long receiverPtr, int seq, boolean handled);
+ private static native void nativeReportTimeline(long receiverPtr, int inputEventId,
+ long gpuCompletedTime, long presentTime);
+ private static native boolean nativeConsumeBatchedInputEvents(long receiverPtr,
+ long frameTimeNanos);
+ private static native String nativeDump(long receiverPtr, String prefix);
+
+ /**
+ * Creates an input event receiver bound to the specified input channel.
+ *
+ * @param inputChannel The input channel.
+ * @param looper The looper to use when invoking callbacks.
+ */
+ public InputEventReceiver(InputChannel inputChannel, Looper looper) {
+ if (inputChannel == null) {
+ throw new IllegalArgumentException("inputChannel must not be null");
+ }
+ if (looper == null) {
+ throw new IllegalArgumentException("looper must not be null");
+ }
+
+ mInputChannel = inputChannel;
+ mMessageQueue = looper.getQueue();
+ mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
+ inputChannel, mMessageQueue);
+
+ mCloseGuard.open("dispose");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Disposes the receiver.
+ * Must be called on the same Looper thread to which the receiver is attached.
+ */
+ public void dispose() {
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (mReceiverPtr != 0) {
+ nativeDispose(mReceiverPtr);
+ mReceiverPtr = 0;
+ }
+
+ if (mInputChannel != null) {
+ mInputChannel.dispose();
+ mInputChannel = null;
+ }
+ mMessageQueue = null;
+ Reference.reachabilityFence(this);
+ }
+
+ /**
+ * Called when an input event is received.
+ * The recipient should process the input event and then call {@link #finishInputEvent}
+ * to indicate whether the event was handled. No new input events will be received
+ * until {@link #finishInputEvent} is called.
+ *
+ * @param event The input event that was received.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void onInputEvent(InputEvent event) {
+ finishInputEvent(event, false);
+ }
+
+ /**
+ * Called when a focus event is received.
+ *
+ * @param hasFocus if true, the window associated with this input channel has just received
+ * focus
+ * if false, the window associated with this input channel has just lost focus
+ * @param inTouchMode if true, the device is in touch mode
+ * if false, the device is not in touch mode
+ */
+ // Called from native code.
+ public void onFocusEvent(boolean hasFocus, boolean inTouchMode) {
+ }
+
+ /**
+ * Called when a Pointer Capture event is received.
+ *
+ * @param pointerCaptureEnabled if true, the window associated with this input channel has just
+ * received Pointer Capture
+ * if false, the window associated with this input channel has just
+ * lost Pointer Capture
+ * @see View#requestPointerCapture()
+ * @see View#releasePointerCapture()
+ */
+ // Called from native code.
+ public void onPointerCaptureEvent(boolean pointerCaptureEnabled) {
+ }
+
+ /**
+ * Called when a drag event is received, from native code.
+ *
+ * @param isExiting if false, the window associated with this input channel has just received
+ * drag
+ * if true, the window associated with this input channel has just lost drag
+ */
+ public void onDragEvent(boolean isExiting, float x, float y) {
+ }
+
+ /**
+ * Called when a batched input event is pending.
+ *
+ * The batched input event will continue to accumulate additional movement
+ * samples until the recipient calls {@link #consumeBatchedInputEvents} or
+ * an event is received that ends the batch and causes it to be consumed
+ * immediately (such as a pointer up event).
+ * @param source The source of the batched event.
+ */
+ public void onBatchedInputEventPending(int source) {
+ consumeBatchedInputEvents(-1);
+ }
+
+ /**
+ * Finishes an input event and indicates whether it was handled.
+ * Must be called on the same Looper thread to which the receiver is attached.
+ *
+ * @param event The input event that was finished.
+ * @param handled True if the event was handled.
+ */
+ public final void finishInputEvent(InputEvent event, boolean handled) {
+ if (event == null) {
+ throw new IllegalArgumentException("event must not be null");
+ }
+ if (mReceiverPtr == 0) {
+ Log.w(TAG, "Attempted to finish an input event but the input event "
+ + "receiver has already been disposed.");
+ } else {
+ int index = mSeqMap.indexOfKey(event.getSequenceNumber());
+ if (index < 0) {
+ Log.w(TAG, "Attempted to finish an input event that is not in progress.");
+ } else {
+ int seq = mSeqMap.valueAt(index);
+ mSeqMap.removeAt(index);
+ nativeFinishInputEvent(mReceiverPtr, seq, handled);
+ }
+ }
+ event.recycleIfNeededAfterDispatch();
+ }
+
+ /**
+ * Report the timing / latency information for a specific input event.
+ */
+ public final void reportTimeline(int inputEventId, long gpuCompletedTime, long presentTime) {
+ Trace.traceBegin(Trace.TRACE_TAG_INPUT, "reportTimeline");
+ nativeReportTimeline(mReceiverPtr, inputEventId, gpuCompletedTime, presentTime);
+ Trace.traceEnd(Trace.TRACE_TAG_INPUT);
+ }
+
+ /**
+ * Consumes all pending batched input events.
+ * Must be called on the same Looper thread to which the receiver is attached.
+ *
+ * This method forces all batched input events to be delivered immediately.
+ * Should be called just before animating or drawing a new frame in the UI.
+ *
+ * @param frameTimeNanos The time in the {@link System#nanoTime()} time base
+ * when the current display frame started rendering, or -1 if unknown.
+ *
+ * @return Whether a batch was consumed
+ */
+ public final boolean consumeBatchedInputEvents(long frameTimeNanos) {
+ if (mReceiverPtr == 0) {
+ Log.w(TAG, "Attempted to consume batched input events but the input event "
+ + "receiver has already been disposed.");
+ } else {
+ return nativeConsumeBatchedInputEvents(mReceiverPtr, frameTimeNanos);
+ }
+ return false;
+ }
+
+ /**
+ * @return Returns a token to identify the input channel.
+ */
+ public IBinder getToken() {
+ if (mInputChannel == null) {
+ return null;
+ }
+ return mInputChannel.getToken();
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void dispatchInputEvent(int seq, InputEvent event) {
+ mSeqMap.put(event.getSequenceNumber(), seq);
+ onInputEvent(event);
+ }
+
+ /**
+ * Dump the state of this InputEventReceiver to the writer.
+ * @param prefix the prefix (typically whitespace padding) to append in front of each line
+ * @param writer the writer where the dump should be written
+ */
+ public void dump(String prefix, PrintWriter writer) {
+ writer.println(prefix + getClass().getName());
+ writer.println(prefix + " mInputChannel: " + mInputChannel);
+ writer.println(prefix + " mSeqMap: " + mSeqMap);
+ writer.println(prefix + " mReceiverPtr:\n" + nativeDump(mReceiverPtr, prefix + " "));
+ }
+
+ /**
+ * Factory for InputEventReceiver
+ */
+ public interface Factory {
+ /**
+ * Create a new InputReceiver for a given inputChannel
+ */
+ InputEventReceiver createInputEventReceiver(InputChannel inputChannel, Looper looper);
+ }
+}
diff --git a/android/view/InputEventSender.java b/android/view/InputEventSender.java
new file mode 100644
index 0000000..d144218
--- /dev/null
+++ b/android/view/InputEventSender.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides a low-level mechanism for an application to send input events.
+ * @hide
+ */
+public abstract class InputEventSender {
+ private static final String TAG = "InputEventSender";
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private long mSenderPtr;
+
+ // We keep references to the input channel and message queue objects here so that
+ // they are not GC'd while the native peer of the receiver is using them.
+ private InputChannel mInputChannel;
+ private MessageQueue mMessageQueue;
+
+ private static native long nativeInit(WeakReference<InputEventSender> sender,
+ InputChannel inputChannel, MessageQueue messageQueue);
+ private static native void nativeDispose(long senderPtr);
+ private static native boolean nativeSendKeyEvent(long senderPtr, int seq, KeyEvent event);
+ private static native boolean nativeSendMotionEvent(long senderPtr, int seq, MotionEvent event);
+
+ /**
+ * Creates an input event sender bound to the specified input channel.
+ *
+ * @param inputChannel The input channel.
+ * @param looper The looper to use when invoking callbacks.
+ */
+ public InputEventSender(InputChannel inputChannel, Looper looper) {
+ if (inputChannel == null) {
+ throw new IllegalArgumentException("inputChannel must not be null");
+ }
+ if (looper == null) {
+ throw new IllegalArgumentException("looper must not be null");
+ }
+
+ mInputChannel = inputChannel;
+ mMessageQueue = looper.getQueue();
+ mSenderPtr = nativeInit(new WeakReference<InputEventSender>(this),
+ inputChannel, mMessageQueue);
+
+ mCloseGuard.open("dispose");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Disposes the receiver.
+ */
+ public void dispose() {
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (mSenderPtr != 0) {
+ nativeDispose(mSenderPtr);
+ mSenderPtr = 0;
+ }
+ mInputChannel = null;
+ mMessageQueue = null;
+ }
+
+ /**
+ * Called when an input event is finished.
+ *
+ * @param seq The input event sequence number.
+ * @param handled True if the input event was handled.
+ */
+ public void onInputEventFinished(int seq, boolean handled) {
+ }
+
+ /**
+ * Called when timeline is sent to the publisher.
+ *
+ * @param inputEventId The id of the input event that caused the frame being reported
+ * @param gpuCompletedTime The time when the frame left the app process
+ * @param presentTime The time when the frame was presented on screen
+ */
+ public void onTimelineReported(int inputEventId, long gpuCompletedTime, long presentTime) {
+ }
+
+ /**
+ * Sends an input event.
+ * Must be called on the same Looper thread to which the sender is attached.
+ *
+ * @param seq The input event sequence number.
+ * @param event The input event to send.
+ * @return True if the entire event was sent successfully. May return false
+ * if the input channel buffer filled before all samples were dispatched.
+ */
+ public final boolean sendInputEvent(int seq, InputEvent event) {
+ if (event == null) {
+ throw new IllegalArgumentException("event must not be null");
+ }
+ if (mSenderPtr == 0) {
+ Log.w(TAG, "Attempted to send an input event but the input event "
+ + "sender has already been disposed.");
+ return false;
+ }
+
+ if (event instanceof KeyEvent) {
+ return nativeSendKeyEvent(mSenderPtr, seq, (KeyEvent)event);
+ } else {
+ return nativeSendMotionEvent(mSenderPtr, seq, (MotionEvent)event);
+ }
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void dispatchInputEventFinished(int seq, boolean handled) {
+ onInputEventFinished(seq, handled);
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ private void dispatchTimelineReported(
+ int inputEventId, long gpuCompletedTime, long presentTime) {
+ onTimelineReported(inputEventId, gpuCompletedTime, presentTime);
+ }
+}
diff --git a/android/view/InputFilter.java b/android/view/InputFilter.java
new file mode 100644
index 0000000..b18c5cd
--- /dev/null
+++ b/android/view/InputFilter.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+
+/**
+ * Filters input events before they are dispatched to the system.
+ * <p>
+ * At most one input filter can be installed by calling
+ * {@link WindowManagerService#setInputFilter}. When an input filter is installed, the
+ * system's behavior changes as follows:
+ * <ul>
+ * <li>Input events are first delivered to the {@link WindowManagerPolicy}
+ * interception methods before queuing as usual. This critical step takes care of managing
+ * the power state of the device and handling wake keys.</li>
+ * <li>Input events are then asynchronously delivered to the input filter's
+ * {@link #onInputEvent(InputEvent)} method instead of being enqueued for dispatch to
+ * applications as usual. The input filter only receives input events that were
+ * generated by an input device; the input filter will not receive input events that were
+ * injected into the system by other means, such as by instrumentation.</li>
+ * <li>The input filter processes and optionally transforms the stream of events. For example,
+ * it may transform a sequence of motion events representing an accessibility gesture into
+ * a different sequence of motion events, key presses or other system-level interactions.
+ * The input filter can send events to be dispatched by calling
+ * {@link #sendInputEvent(InputEvent)} and passing appropriate policy flags for the
+ * input event.</li>
+ * </ul>
+ * </p>
+ * <h3>The importance of input event consistency</h3>
+ * <p>
+ * The input filter mechanism is very low-level. At a minimum, it needs to ensure that it
+ * sends an internally consistent stream of input events to the dispatcher. There are
+ * very important invariants to be maintained.
+ * </p><p>
+ * For example, if a key down is sent, a corresponding key up should also be sent eventually.
+ * Likewise, for touch events, each pointer must individually go down with
+ * {@link MotionEvent#ACTION_DOWN} or {@link MotionEvent#ACTION_POINTER_DOWN} and then
+ * individually go up with {@link MotionEvent#ACTION_POINTER_UP} or {@link MotionEvent#ACTION_UP}
+ * and the sequence of pointer ids used must be consistent throughout the gesture.
+ * </p><p>
+ * Sometimes a filter may wish to cancel a previously dispatched key or motion. It should
+ * use {@link KeyEvent#FLAG_CANCELED} or {@link MotionEvent#ACTION_CANCEL} accordingly.
+ * </p><p>
+ * The input filter must take into account the fact that the input events coming from different
+ * devices or even different sources all consist of distinct streams of input.
+ * Use {@link InputEvent#getDeviceId()} and {@link InputEvent#getSource()} to identify
+ * the source of the event and its semantics. There may be multiple sources of keys,
+ * touches and other input: they must be kept separate.
+ * </p>
+ * <h3>Policy flags</h3>
+ * <p>
+ * Input events received from the dispatcher and sent to the dispatcher have policy flags
+ * associated with them. Policy flags control some functions of the dispatcher.
+ * </p><p>
+ * The early policy interception decides whether an input event should be delivered
+ * to applications or dropped. The policy indicates its decision by setting the
+ * {@link WindowManagerPolicyConstants#FLAG_PASS_TO_USER} policy flag. The input filter may
+ * sometimes receive events that do not have this flag set. It should take note of
+ * the fact that the policy intends to drop the event, clean up its state, and
+ * then send appropriate cancellation events to the dispatcher if needed.
+ * </p><p>
+ * For example, suppose the input filter is processing a gesture and one of the touch events
+ * it receives does not have the {@link WindowManagerPolicyConstants#FLAG_PASS_TO_USER} flag set.
+ * The input filter should clear its internal state about the gesture and then send key or
+ * motion events to the dispatcher to cancel any keys or pointers that are down.
+ * </p><p>
+ * Corollary: Events that get sent to the dispatcher should usually include the
+ * {@link WindowManagerPolicyConstants#FLAG_PASS_TO_USER} flag. Otherwise, they will be dropped!
+ * </p><p>
+ * It may be prudent to disable automatic key repeating for synthetic key events
+ * by setting the {@link WindowManagerPolicyConstants#FLAG_DISABLE_KEY_REPEAT} policy flag.
+ * </p>
+ *
+ * @hide
+ */
+public abstract class InputFilter extends IInputFilter.Stub {
+ private static final int MSG_INSTALL = 1;
+ private static final int MSG_UNINSTALL = 2;
+ private static final int MSG_INPUT_EVENT = 3;
+
+ // Consistency verifiers for debugging purposes.
+ private final InputEventConsistencyVerifier mInboundInputEventConsistencyVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled() ?
+ new InputEventConsistencyVerifier(this,
+ InputEventConsistencyVerifier.FLAG_RAW_DEVICE_INPUT,
+ "InputFilter#InboundInputEventConsistencyVerifier") : null;
+ private final InputEventConsistencyVerifier mOutboundInputEventConsistencyVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled() ?
+ new InputEventConsistencyVerifier(this,
+ InputEventConsistencyVerifier.FLAG_RAW_DEVICE_INPUT,
+ "InputFilter#OutboundInputEventConsistencyVerifier") : null;
+
+ private final H mH;
+
+ private IInputFilterHost mHost;
+
+ /**
+ * Creates the input filter.
+ *
+ * @param looper The looper to run callbacks on.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public InputFilter(Looper looper) {
+ mH = new H(looper);
+ }
+
+ /**
+ * Called when the input filter is installed.
+ * This method is guaranteed to be non-reentrant.
+ *
+ * @param host The input filter host environment.
+ */
+ public final void install(IInputFilterHost host) {
+ mH.obtainMessage(MSG_INSTALL, host).sendToTarget();
+ }
+
+ /**
+ * Called when the input filter is uninstalled.
+ * This method is guaranteed to be non-reentrant.
+ */
+ public final void uninstall() {
+ mH.obtainMessage(MSG_UNINSTALL).sendToTarget();
+ }
+
+ /**
+ * Called to enqueue the input event for filtering.
+ * The event will be recycled after the input filter processes it.
+ * This method is guaranteed to be non-reentrant.
+ *
+ * @param event The input event to enqueue.
+ */
+ final public void filterInputEvent(InputEvent event, int policyFlags) {
+ mH.obtainMessage(MSG_INPUT_EVENT, policyFlags, 0, event).sendToTarget();
+ }
+
+ /**
+ * Sends an input event to the dispatcher.
+ *
+ * @param event The input event to publish.
+ * @param policyFlags The input event policy flags.
+ */
+ public void sendInputEvent(InputEvent event, int policyFlags) {
+ if (event == null) {
+ throw new IllegalArgumentException("event must not be null");
+ }
+ if (mHost == null) {
+ throw new IllegalStateException("Cannot send input event because the input filter " +
+ "is not installed.");
+ }
+ if (mOutboundInputEventConsistencyVerifier != null) {
+ mOutboundInputEventConsistencyVerifier.onInputEvent(event, 0);
+ }
+ try {
+ mHost.sendInputEvent(event, policyFlags);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ }
+
+ /**
+ * Called when an input event has been received from the dispatcher.
+ * <p>
+ * The default implementation sends the input event back to the dispatcher, unchanged.
+ * </p><p>
+ * The event will be recycled when this method returns. If you want to keep it around,
+ * make a copy!
+ * </p>
+ *
+ * @param event The input event that was received.
+ * @param policyFlags The input event policy flags.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void onInputEvent(InputEvent event, int policyFlags) {
+ sendInputEvent(event, policyFlags);
+ }
+
+ /**
+ * Called when the filter is installed into the dispatch pipeline.
+ * <p>
+ * This method is called before the input filter receives any input events.
+ * The input filter should take this opportunity to prepare itself.
+ * </p>
+ */
+ public void onInstalled() {
+ }
+
+ /**
+ * Called when the filter is uninstalled from the dispatch pipeline.
+ * <p>
+ * This method is called after the input filter receives its last input event.
+ * The input filter should take this opportunity to clean up.
+ * </p>
+ */
+ public void onUninstalled() {
+ }
+
+ private final class H extends Handler {
+ public H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INSTALL:
+ mHost = (IInputFilterHost) msg.obj;
+ if (mInboundInputEventConsistencyVerifier != null) {
+ mInboundInputEventConsistencyVerifier.reset();
+ }
+ if (mOutboundInputEventConsistencyVerifier != null) {
+ mOutboundInputEventConsistencyVerifier.reset();
+ }
+ onInstalled();
+ break;
+
+ case MSG_UNINSTALL:
+ try {
+ onUninstalled();
+ } finally {
+ mHost = null;
+ }
+ break;
+
+ case MSG_INPUT_EVENT: {
+ final InputEvent event = (InputEvent)msg.obj;
+ try {
+ if (mInboundInputEventConsistencyVerifier != null) {
+ mInboundInputEventConsistencyVerifier.onInputEvent(event, 0);
+ }
+ onInputEvent(event, msg.arg1);
+ } finally {
+ event.recycle();
+ }
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/android/view/InputMonitor.java b/android/view/InputMonitor.java
new file mode 100644
index 0000000..ad1f201
--- /dev/null
+++ b/android/view/InputMonitor.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * An {@code InputMonitor} allows privileged applications and components to monitor streams of
+ * {@link InputEvent}s without having to be the designated recipient for the event.
+ *
+ * For example, focus dispatched events would normally only go to the focused window on the
+ * targeted display, but an {@code InputMonitor} will also receive a copy of that event if they're
+ * registered to monitor that type of event on the targeted display.
+ *
+ * @hide
+ */
+@DataClass(genToString = true)
+public final class InputMonitor implements Parcelable {
+ private static final String TAG = "InputMonitor";
+
+ private static final boolean DEBUG = false;
+
+ @NonNull
+ private final InputChannel mInputChannel;
+ @NonNull
+ private final IInputMonitorHost mHost;
+
+
+ /**
+ * Takes all of the current pointer events streams that are currently being sent to this
+ * monitor and generates appropriate cancellations for the windows that would normally get
+ * them.
+ *
+ * This method should be used with caution as unexpected pilfering can break fundamental user
+ * interactions.
+ */
+ public void pilferPointers() {
+ try {
+ mHost.pilferPointers();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Disposes the input monitor.
+ *
+ * Explicitly release all of the resources this monitor is holding on to (e.g. the
+ * InputChannel). Once this method is called, this monitor and any resources it's provided may
+ * no longer be used.
+ */
+ public void dispose() {
+ mInputChannel.dispose();
+ try {
+ mHost.dispose();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+
+
+ // Code below generated by codegen v1.0.7.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/InputMonitor.java
+
+
+ @DataClass.Generated.Member
+ public InputMonitor(
+ @NonNull InputChannel inputChannel,
+ @NonNull IInputMonitorHost host) {
+ this.mInputChannel = inputChannel;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInputChannel);
+ this.mHost = host;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHost);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public @NonNull InputChannel getInputChannel() {
+ return mInputChannel;
+ }
+
+ @DataClass.Generated.Member
+ public @NonNull IInputMonitorHost getHost() {
+ return mHost;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InputMonitor { " +
+ "inputChannel = " + mInputChannel + ", " +
+ "host = " + mHost +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedObject(mInputChannel, flags);
+ dest.writeStrongInterface(mHost);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InputMonitor(Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ InputChannel inputChannel = (InputChannel) in.readTypedObject(InputChannel.CREATOR);
+ IInputMonitorHost host = IInputMonitorHost.Stub.asInterface(in.readStrongBinder());
+
+ this.mInputChannel = inputChannel;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInputChannel);
+ this.mHost = host;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHost);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InputMonitor> CREATOR
+ = new Parcelable.Creator<InputMonitor>() {
+ @Override
+ public InputMonitor[] newArray(int size) {
+ return new InputMonitor[size];
+ }
+
+ @Override
+ public InputMonitor createFromParcel(Parcel in) {
+ return new InputMonitor(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1571177265149L,
+ codegenVersion = "1.0.7",
+ sourceFile = "frameworks/base/core/java/android/view/InputMonitor.java",
+ inputSignatures = "private static final java.lang.String TAG\nprivate static final boolean DEBUG\nprivate final @android.annotation.NonNull android.view.InputChannel mInputChannel\nprivate final @android.annotation.NonNull android.view.IInputMonitorHost mHost\npublic void pilferPointers()\npublic void dispose()\nclass InputMonitor extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true)")
+ @Deprecated
+ private void __metadata() {}
+
+}
diff --git a/android/view/InputQueue.java b/android/view/InputQueue.java
new file mode 100644
index 0000000..7accb66
--- /dev/null
+++ b/android/view/InputQueue.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.LongSparseArray;
+import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * An input queue provides a mechanism for an application to receive incoming
+ * input events. Currently only usable from native code.
+ */
+public final class InputQueue {
+ private final LongSparseArray<ActiveInputEvent> mActiveEventArray =
+ new LongSparseArray<ActiveInputEvent>(20);
+ private final Pool<ActiveInputEvent> mActiveInputEventPool =
+ new SimplePool<ActiveInputEvent>(20);
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private long mPtr;
+
+ private static native long nativeInit(WeakReference<InputQueue> weakQueue,
+ MessageQueue messageQueue);
+ private static native long nativeSendKeyEvent(long ptr, KeyEvent e, boolean preDispatch);
+ private static native long nativeSendMotionEvent(long ptr, MotionEvent e);
+ private static native void nativeDispose(long ptr);
+
+ /** @hide */
+ public InputQueue() {
+ mPtr = nativeInit(new WeakReference<InputQueue>(this), Looper.myQueue());
+
+ mCloseGuard.open("dispose");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /** @hide */
+ public void dispose() {
+ dispose(false);
+ }
+
+ /** @hide */
+ public void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (mPtr != 0) {
+ nativeDispose(mPtr);
+ mPtr = 0;
+ }
+ }
+
+ /** @hide */
+ public long getNativePtr() {
+ return mPtr;
+ }
+
+ /** @hide */
+ public void sendInputEvent(InputEvent e, Object token, boolean predispatch,
+ FinishedInputEventCallback callback) {
+ ActiveInputEvent event = obtainActiveInputEvent(token, callback);
+ long id;
+ if (e instanceof KeyEvent) {
+ id = nativeSendKeyEvent(mPtr, (KeyEvent) e, predispatch);
+ } else {
+ id = nativeSendMotionEvent(mPtr, (MotionEvent) e);
+ }
+ mActiveEventArray.put(id, event);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void finishInputEvent(long id, boolean handled) {
+ int index = mActiveEventArray.indexOfKey(id);
+ if (index >= 0) {
+ ActiveInputEvent e = mActiveEventArray.valueAt(index);
+ mActiveEventArray.removeAt(index);
+ e.mCallback.onFinishedInputEvent(e.mToken, handled);
+ recycleActiveInputEvent(e);
+ }
+ }
+
+ private ActiveInputEvent obtainActiveInputEvent(Object token,
+ FinishedInputEventCallback callback) {
+ ActiveInputEvent e = mActiveInputEventPool.acquire();
+ if (e == null) {
+ e = new ActiveInputEvent();
+ }
+ e.mToken = token;
+ e.mCallback = callback;
+ return e;
+ }
+
+ private void recycleActiveInputEvent(ActiveInputEvent e) {
+ e.recycle();
+ mActiveInputEventPool.release(e);
+ }
+
+ private final class ActiveInputEvent {
+ public Object mToken;
+ public FinishedInputEventCallback mCallback;
+
+ public void recycle() {
+ mToken = null;
+ mCallback = null;
+ }
+ }
+
+ /**
+ * Interface to receive notification of when an InputQueue is associated
+ * and dissociated with a thread.
+ */
+ public static interface Callback {
+ /**
+ * Called when the given InputQueue is now associated with the
+ * thread making this call, so it can start receiving events from it.
+ */
+ void onInputQueueCreated(InputQueue queue);
+
+ /**
+ * Called when the given InputQueue is no longer associated with
+ * the thread and thus not dispatching events.
+ */
+ void onInputQueueDestroyed(InputQueue queue);
+ }
+
+ /** @hide */
+ public static interface FinishedInputEventCallback {
+ void onFinishedInputEvent(Object token, boolean handled);
+ }
+
+}
diff --git a/android/view/InputStageBenchmark.java b/android/view/InputStageBenchmark.java
new file mode 100644
index 0000000..b45dbcf
--- /dev/null
+++ b/android/view/InputStageBenchmark.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.PerfTestActivity;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class InputStageBenchmark {
+ @Parameterized.Parameters(name = "mShowIme({0}), mHandlePreIme({1})")
+ public static Collection cases() {
+ return Arrays.asList(new Object[][] {
+ { false /* no ime */, false /* skip preime */},
+ { true /* show ime */, false /* skip preime */},
+ { true /* show ime */, true /* handle preime */}
+ });
+ }
+
+ @Rule
+ public final ActivityTestRule<PerfTestActivity> mActivityRule =
+ new ActivityTestRule<>(PerfTestActivity.class);
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Parameterized.Parameter(0)
+ public boolean mShowIme;
+ @Parameterized.Parameter(1)
+ public boolean mHandlePreIme;
+
+ private Instrumentation mInstrumentation;
+ private Window mWindow;
+ private CountDownLatch mWaitForReceiveInput;
+ private static final long TIMEOUT_MS = 5000;
+
+ class InstrumentedView extends View {
+ InstrumentedView(Context context) {
+ super(context);
+ setFocusable(true);
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if (mHandlePreIme) {
+ mWaitForReceiveInput.countDown();
+ }
+ return mHandlePreIme;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ mWaitForReceiveInput.countDown();
+ return true;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ mWaitForReceiveInput.countDown();
+ return true;
+ }
+ }
+
+ class InstrumentedEditText extends EditText {
+ InstrumentedEditText(Context context) {
+ super(context);
+ setFocusable(true);
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if (mHandlePreIme) {
+ mWaitForReceiveInput.countDown();
+ }
+ return mHandlePreIme;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ mWaitForReceiveInput.countDown();
+ return true;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ mWaitForReceiveInput.countDown();
+ return true;
+ }
+ }
+
+ private CountDownLatch showSoftKeyboard(View view) {
+ final CountDownLatch waitForIme = new CountDownLatch(1);
+ view.setOnApplyWindowInsetsListener((v, insets) -> {
+ if (insets.isVisible(WindowInsets.Type.ime())) {
+ waitForIme.countDown();
+ }
+ return insets;
+ });
+
+ assertTrue("Failed to request focus.", view.requestFocus());
+ final InputMethodManager imm =
+ mActivityRule.getActivity().getSystemService(InputMethodManager.class);
+ imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
+
+ return waitForIme;
+ }
+
+ @Before
+ public void setUp() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ final Activity activity = mActivityRule.getActivity();
+
+ final CountDownLatch[] waitForIme = new CountDownLatch[1];
+ mInstrumentation.runOnMainSync(() -> {
+ mWindow = mActivityRule.getActivity().getWindow();
+
+ if (mShowIme) {
+ final EditText edit = new InstrumentedEditText(activity);
+ mWindow.setContentView(edit);
+ waitForIme[0] = showSoftKeyboard(edit);
+ } else {
+ final View v = new InstrumentedView(activity);
+ // set FLAG_LOCAL_FOCUS_MODE to prevent delivering input events to the ime
+ // in ImeInputStage.
+ mWindow.addFlags(WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE);
+ mWindow.setContentView(v);
+ assertTrue("Failed to request focus.", v.requestFocus());
+ }
+ });
+ if (waitForIme[0] != null) {
+ try {
+ assertTrue("Failed to show InputMethod.",
+ waitForIme[0].await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void injectInputEvent(InputEvent event) {
+ mWaitForReceiveInput = new CountDownLatch(1);
+ mInstrumentation.runOnMainSync(() -> mWindow.injectInputEvent(event));
+ try {
+ mWaitForReceiveInput.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testKeyEvent() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ final KeyEvent eventDown =
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACKSLASH);
+ injectInputEvent(eventDown);
+
+ state.pauseTiming();
+ final KeyEvent eventUp =
+ new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACKSLASH);
+ injectInputEvent(eventUp);
+ state.resumeTiming();
+ }
+ }
+
+ @Test
+ public void testMotionEvent() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final Rect contentFrame = new Rect();
+ mInstrumentation.runOnMainSync(() ->
+ mWindow.getDecorView().getBoundsOnScreen(contentFrame));
+ final int x = contentFrame.centerX();
+ final int y = contentFrame.centerY();
+ final long eventTime = SystemClock.uptimeMillis();
+
+ while (state.keepRunning()) {
+ final MotionEvent eventDown = MotionEvent.obtain(eventTime, eventTime,
+ MotionEvent.ACTION_DOWN, x, y,
+ 1.0f /* pressure */, 1.0f /* size */, 0 /* metaState */,
+ 1.0f /* xPrecision */, 1.0f /* yPrecision */,
+ 0 /* deviceId */, 0 /* edgeFlags */,
+ InputDevice.SOURCE_TOUCHSCREEN, DEFAULT_DISPLAY);
+ injectInputEvent(eventDown);
+
+ state.pauseTiming();
+ final MotionEvent eventUp = MotionEvent.obtain(eventTime, eventTime,
+ MotionEvent.ACTION_UP, x, y,
+ 1.0f /* pressure */, 1.0f /* size */, 0 /* metaState */,
+ 1.0f /* xPrecision */, 1.0f /* yPrecision */,
+ 0 /* deviceId */, 0 /* edgeFlags */,
+ InputDevice.SOURCE_TOUCHSCREEN, DEFAULT_DISPLAY);
+ injectInputEvent(eventUp);
+ state.resumeTiming();
+ }
+ }
+}
diff --git a/android/view/InputWindowHandle.java b/android/view/InputWindowHandle.java
new file mode 100644
index 0000000..5a34a92
--- /dev/null
+++ b/android/view/InputWindowHandle.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.annotation.Nullable;
+import android.graphics.Region;
+import android.os.IBinder;
+import android.os.TouchOcclusionMode;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Functions as a handle for a window that can receive input.
+ * Enables the native input dispatcher to refer indirectly to the window manager's window state.
+ * @hide
+ */
+public final class InputWindowHandle {
+ // Pointer to the native input window handle.
+ // This field is lazily initialized via JNI.
+ @SuppressWarnings("unused")
+ private long ptr;
+
+ // The input application handle.
+ public InputApplicationHandle inputApplicationHandle;
+
+ // The token associates input data with a window and its input channel. The client input
+ // channel and the server input channel will both contain this token.
+ public IBinder token;
+
+ // The window name.
+ public String name;
+
+ // Window layout params attributes. (WindowManager.LayoutParams)
+ public int layoutParamsFlags;
+ public int layoutParamsType;
+
+ // Dispatching timeout.
+ public long dispatchingTimeoutMillis;
+
+ // Window frame.
+ public int frameLeft;
+ public int frameTop;
+ public int frameRight;
+ public int frameBottom;
+
+ public int surfaceInset;
+
+ // Global scaling factor applied to touch events when they are dispatched
+ // to the window
+ public float scaleFactor;
+
+ // Window touchable region.
+ public final Region touchableRegion = new Region();
+
+ // Window is visible.
+ public boolean visible;
+
+ // Window can be focused.
+ public boolean focusable;
+
+ // Window has wallpaper. (window is the current wallpaper target)
+ public boolean hasWallpaper;
+
+ // Input event dispatching is paused.
+ public boolean paused;
+
+ // Window is trusted overlay.
+ public boolean trustedOverlay;
+
+ // What effect this window has on touch occlusion if it lets touches pass through
+ // By default windows will block touches if they are untrusted and from a different UID due to
+ // security concerns
+ public int touchOcclusionMode = TouchOcclusionMode.BLOCK_UNTRUSTED;
+
+ // Id of process and user that owns the window.
+ public int ownerPid;
+ public int ownerUid;
+
+ // Owner package of the window
+ public String packageName;
+
+ // Window input features.
+ public int inputFeatures;
+
+ // Display this input is on.
+ public int displayId;
+
+ // If this value is set to a valid display ID, it indicates this window is a portal which
+ // transports the touch of this window to the display indicated by portalToDisplayId.
+ public int portalToDisplayId = INVALID_DISPLAY;
+
+ /**
+ * Crops the touchable region to the bounds of the surface provided.
+ *
+ * This can be used in cases where the window is not
+ * {@link android.view.WindowManager#FLAG_NOT_TOUCH_MODAL} but should be constrained to the
+ * bounds of a parent window. That is the window should receive touch events outside its
+ * window but be limited to its stack bounds, such as in the case of split screen.
+ */
+ public WeakReference<SurfaceControl> touchableRegionSurfaceControl = new WeakReference<>(null);
+
+ /**
+ * Replace {@link touchableRegion} with the bounds of {@link touchableRegionSurfaceControl}. If
+ * the handle is {@code null}, the bounds of the surface associated with this window is used
+ * as the touchable region.
+ */
+ public boolean replaceTouchableRegionWithCrop;
+
+ private native void nativeDispose();
+
+ public InputWindowHandle(InputApplicationHandle inputApplicationHandle, int displayId) {
+ this.inputApplicationHandle = inputApplicationHandle;
+ this.displayId = displayId;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(name != null ? name : "")
+ .append(", frame=[").append(frameLeft).append(",").append(frameTop).append(",")
+ .append(frameRight).append(",").append(frameBottom).append("]")
+ .append(", touchableRegion=").append(touchableRegion)
+ .append(", visible=").append(visible)
+ .toString();
+
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nativeDispose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Set the window touchable region to the bounds of {@link touchableRegionBounds} ignoring any
+ * touchable region provided.
+ *
+ * @param bounds surface to set the touchable region to. Set to {@code null} to set the bounds
+ * to the current surface.
+ */
+ public void replaceTouchableRegionWithCrop(@Nullable SurfaceControl bounds) {
+ setTouchableRegionCrop(bounds);
+ replaceTouchableRegionWithCrop = true;
+ }
+
+ /**
+ * Crop the window touchable region to the bounds of the surface provided.
+ */
+ public void setTouchableRegionCrop(@Nullable SurfaceControl bounds) {
+ touchableRegionSurfaceControl = new WeakReference<>(bounds);
+ }
+}
diff --git a/android/view/InsetsAnimationControlCallbacks.java b/android/view/InsetsAnimationControlCallbacks.java
new file mode 100644
index 0000000..3431c3e
--- /dev/null
+++ b/android/view/InsetsAnimationControlCallbacks.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowInsetsAnimation.Bounds;
+
+/**
+ * Provide an interface to let InsetsAnimationControlImpl call back into its owner.
+ * @hide
+ */
+public interface InsetsAnimationControlCallbacks {
+
+ /**
+ * Executes the necessary code to start the animation in the correct order, including:
+ * <ul>
+ * <li>Dispatch {@link WindowInsetsAnimation.Callback#onPrepare}</li>
+ * <li>Update insets state and run layout according to {@code layoutDuringAnimation}</li>
+ * <li>Dispatch {@link WindowInsetsAnimation.Callback#onStart}</li>
+ * <li>Dispatch {@link WindowInsetsAnimationControlListener#onReady}</li>
+ * </ul>
+ */
+ void startAnimation(InsetsAnimationControlImpl controller,
+ WindowInsetsAnimationControlListener listener, int types,
+ WindowInsetsAnimation animation,
+ Bounds bounds);
+
+ /**
+ * Schedule the apply by posting the animation callback.
+ *
+ * @param runner The runner that requested applying insets
+ */
+ void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner);
+
+ /**
+ * Finish the final steps after the animation.
+ * @param runner The runner used to run the animation.
+ * @param shown {@code true} if the insets are shown.
+ */
+ void notifyFinished(InsetsAnimationControlRunner runner, boolean shown);
+
+ /**
+ * Apply the new params to the surface.
+ * @param params The {@link android.view.SyncRtSurfaceTransactionApplier.SurfaceParams} to
+ * apply.
+ */
+ void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... params);
+
+ /**
+ * Post a message to release the Surface, guaranteed to happen after all
+ * previous calls to applySurfaceParams.
+ */
+ void releaseSurfaceControlFromRt(SurfaceControl sc);
+
+ /**
+ * Reports that the perceptibility of the given types has changed to the given value.
+ *
+ * A type is perceptible if it is not (almost) entirely off-screen and not (almost) entirely
+ * transparent.
+ *
+ * @param types the (public) types whose perceptibility has changed
+ * @param perceptible true, if the types are now perceptible, false if they are not perceptible
+ */
+ void reportPerceptible(@InsetsType int types, boolean perceptible);
+}
diff --git a/android/view/InsetsAnimationControlImpl.java b/android/view/InsetsAnimationControlImpl.java
new file mode 100644
index 0000000..17b3020
--- /dev/null
+++ b/android/view/InsetsAnimationControlImpl.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.InsetsAnimationControlImplProto.CURRENT_ALPHA;
+import static android.view.InsetsAnimationControlImplProto.IS_CANCELLED;
+import static android.view.InsetsAnimationControlImplProto.IS_FINISHED;
+import static android.view.InsetsAnimationControlImplProto.PENDING_ALPHA;
+import static android.view.InsetsAnimationControlImplProto.PENDING_FRACTION;
+import static android.view.InsetsAnimationControlImplProto.PENDING_INSETS;
+import static android.view.InsetsAnimationControlImplProto.SHOWN_ON_FINISH;
+import static android.view.InsetsAnimationControlImplProto.TMP_MATRIX;
+import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
+import static android.view.InsetsController.AnimationType;
+import static android.view.InsetsController.DEBUG;
+import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
+import static android.view.InsetsController.LayoutInsetsDuringAnimation;
+import static android.view.InsetsState.ISIDE_BOTTOM;
+import static android.view.InsetsState.ISIDE_FLOATING;
+import static android.view.InsetsState.ISIDE_LEFT;
+import static android.view.InsetsState.ISIDE_RIGHT;
+import static android.view.InsetsState.ISIDE_TOP;
+import static android.view.InsetsState.ITYPE_IME;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.Nullable;
+import android.content.res.CompatibilityInfo;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.SparseSetArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsState.InternalInsetsSide;
+import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowInsetsAnimation.Bounds;
+import android.view.WindowManager.LayoutParams;
+import android.view.animation.Interpolator;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Implements {@link WindowInsetsAnimationController}
+ * @hide
+ */
+@VisibleForTesting
+public class InsetsAnimationControlImpl implements WindowInsetsAnimationController,
+ InsetsAnimationControlRunner {
+
+ private static final String TAG = "InsetsAnimationCtrlImpl";
+
+ private final Rect mTmpFrame = new Rect();
+
+ private final WindowInsetsAnimationControlListener mListener;
+ private final SparseArray<InsetsSourceControl> mControls;
+ private final SparseSetArray<InsetsSourceControl> mSideControlsMap = new SparseSetArray<>();
+
+ /** @see WindowInsetsAnimationController#getHiddenStateInsets */
+ private final Insets mHiddenInsets;
+
+ /** @see WindowInsetsAnimationController#getShownStateInsets */
+ private final Insets mShownInsets;
+ private final Matrix mTmpMatrix = new Matrix();
+ private final InsetsState mInitialInsetsState;
+ private final @AnimationType int mAnimationType;
+ private final @LayoutInsetsDuringAnimation int mLayoutInsetsDuringAnimation;
+ private final @InsetsType int mTypes;
+ private @InsetsType int mControllingTypes;
+ private final InsetsAnimationControlCallbacks mController;
+ private final WindowInsetsAnimation mAnimation;
+ /** @see WindowInsetsAnimationController#hasZeroInsetsIme */
+ private final boolean mHasZeroInsetsIme;
+ private final CompatibilityInfo.Translator mTranslator;
+ private Insets mCurrentInsets;
+ private Insets mPendingInsets;
+ private float mPendingFraction;
+ private boolean mFinished;
+ private boolean mCancelled;
+ private boolean mShownOnFinish;
+ private float mCurrentAlpha = 1.0f;
+ private float mPendingAlpha = 1.0f;
+ @VisibleForTesting(visibility = PACKAGE)
+ public boolean mReadyDispatched;
+ private Boolean mPerceptible;
+
+ @VisibleForTesting
+ public InsetsAnimationControlImpl(SparseArray<InsetsSourceControl> controls,
+ @Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener,
+ @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
+ Interpolator interpolator, @AnimationType int animationType,
+ @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
+ CompatibilityInfo.Translator translator) {
+ mControls = controls;
+ mListener = listener;
+ mTypes = types;
+ mControllingTypes = types;
+ mController = controller;
+ mInitialInsetsState = new InsetsState(state, true /* copySources */);
+ if (frame != null) {
+ final SparseIntArray typeSideMap = new SparseIntArray();
+ mCurrentInsets = getInsetsFromState(mInitialInsetsState, frame, null /* typeSideMap */);
+ mHiddenInsets = calculateInsets(mInitialInsetsState, frame, controls, false /* shown */,
+ null /* typeSideMap */);
+ mShownInsets = calculateInsets(mInitialInsetsState, frame, controls, true /* shown */,
+ typeSideMap);
+ mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsInternalType(ITYPE_IME);
+ if (mHasZeroInsetsIme) {
+ // IME has shownInsets of ZERO, and can't map to a side by default.
+ // Map zero insets IME to bottom, making it a special case of bottom insets.
+ typeSideMap.put(ITYPE_IME, ISIDE_BOTTOM);
+ }
+ buildSideControlsMap(typeSideMap, mSideControlsMap, controls);
+ } else {
+ // Passing a null frame indicates the caller wants to play the insets animation anyway,
+ // no matter the source provides insets to the frame or not.
+ mCurrentInsets = calculateInsets(mInitialInsetsState, controls, true /* shown */);
+ mHiddenInsets = calculateInsets(null, controls, false /* shown */);
+ mShownInsets = calculateInsets(null, controls, true /* shown */);
+ mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsInternalType(ITYPE_IME);
+ buildSideControlsMap(mSideControlsMap, controls);
+ }
+ mPendingInsets = mCurrentInsets;
+
+ mAnimation = new WindowInsetsAnimation(mTypes, interpolator,
+ durationMs);
+ mAnimation.setAlpha(getCurrentAlpha());
+ mAnimationType = animationType;
+ mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
+ mTranslator = translator;
+ mController.startAnimation(this, listener, types, mAnimation,
+ new Bounds(mHiddenInsets, mShownInsets));
+ }
+
+ private boolean calculatePerceptible(Insets currentInsets, float currentAlpha) {
+ return 100 * currentInsets.left >= 5 * (mShownInsets.left - mHiddenInsets.left)
+ && 100 * currentInsets.top >= 5 * (mShownInsets.top - mHiddenInsets.top)
+ && 100 * currentInsets.right >= 5 * (mShownInsets.right - mHiddenInsets.right)
+ && 100 * currentInsets.bottom >= 5 * (mShownInsets.bottom - mHiddenInsets.bottom)
+ && currentAlpha >= 0.5f;
+ }
+
+ @Override
+ public boolean hasZeroInsetsIme() {
+ return mHasZeroInsetsIme;
+ }
+
+ @Override
+ public Insets getHiddenStateInsets() {
+ return mHiddenInsets;
+ }
+
+ @Override
+ public Insets getShownStateInsets() {
+ return mShownInsets;
+ }
+
+ @Override
+ public Insets getCurrentInsets() {
+ return mCurrentInsets;
+ }
+
+ @Override
+ public float getCurrentAlpha() {
+ return mCurrentAlpha;
+ }
+
+ @Override
+ @InsetsType public int getTypes() {
+ return mTypes;
+ }
+
+ @Override
+ public int getControllingTypes() {
+ return mControllingTypes;
+ }
+
+ @Override
+ public void notifyControlRevoked(@InsetsType int types) {
+ mControllingTypes &= ~types;
+ }
+
+ @Override
+ public void updateSurfacePosition(SparseArray<InsetsSourceControl> controls) {
+ for (int i = controls.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl control = controls.valueAt(i);
+ final InsetsSourceControl c = mControls.get(control.getType());
+ if (c == null) {
+ continue;
+ }
+ final Point position = control.getSurfacePosition();
+ c.setSurfacePosition(position.x, position.y);
+ }
+ }
+
+ @Override
+ public @AnimationType int getAnimationType() {
+ return mAnimationType;
+ }
+
+ @Override
+ public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) {
+ setInsetsAndAlpha(insets, alpha, fraction, false /* allowWhenFinished */);
+ }
+
+ private void setInsetsAndAlpha(Insets insets, float alpha, float fraction,
+ boolean allowWhenFinished) {
+ if (!allowWhenFinished && mFinished) {
+ throw new IllegalStateException(
+ "Can't change insets on an animation that is finished.");
+ }
+ if (mCancelled) {
+ throw new IllegalStateException(
+ "Can't change insets on an animation that is cancelled.");
+ }
+ mPendingFraction = sanitize(fraction);
+ mPendingInsets = sanitize(insets);
+ mPendingAlpha = sanitize(alpha);
+ mController.scheduleApplyChangeInsets(this);
+ boolean perceptible = calculatePerceptible(mPendingInsets, mPendingAlpha);
+ if (mPerceptible == null || perceptible != mPerceptible) {
+ mController.reportPerceptible(mTypes, perceptible);
+ mPerceptible = perceptible;
+ }
+ }
+
+ @VisibleForTesting
+ /**
+ * @return Whether the finish callback of this animation should be invoked.
+ */
+ public boolean applyChangeInsets(@Nullable InsetsState outState) {
+ if (mCancelled) {
+ if (DEBUG) Log.d(TAG, "applyChangeInsets canceled");
+ return false;
+ }
+ final Insets offset = Insets.subtract(mShownInsets, mPendingInsets);
+ ArrayList<SurfaceParams> params = new ArrayList<>();
+ updateLeashesForSide(ISIDE_LEFT, offset.left, mPendingInsets.left, params, outState,
+ mPendingAlpha);
+ updateLeashesForSide(ISIDE_TOP, offset.top, mPendingInsets.top, params, outState,
+ mPendingAlpha);
+ updateLeashesForSide(ISIDE_RIGHT, offset.right, mPendingInsets.right, params, outState,
+ mPendingAlpha);
+ updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, mPendingInsets.bottom, params, outState,
+ mPendingAlpha);
+
+ mController.applySurfaceParams(params.toArray(new SurfaceParams[params.size()]));
+ mCurrentInsets = mPendingInsets;
+ mAnimation.setFraction(mPendingFraction);
+ mCurrentAlpha = mPendingAlpha;
+ mAnimation.setAlpha(mPendingAlpha);
+ if (mFinished) {
+ if (DEBUG) Log.d(TAG, String.format(
+ "notifyFinished shown: %s, currentAlpha: %f, currentInsets: %s",
+ mShownOnFinish, mCurrentAlpha, mCurrentInsets));
+ mController.notifyFinished(this, mShownOnFinish);
+ releaseLeashes();
+ }
+ if (DEBUG) Log.d(TAG, "Animation finished abruptly.");
+ return mFinished;
+ }
+
+ private void releaseLeashes() {
+ for (int i = mControls.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl c = mControls.valueAt(i);
+ if (c == null) continue;
+ c.release(mController::releaseSurfaceControlFromRt);
+ }
+ }
+
+ @Override
+ public void finish(boolean shown) {
+ if (mCancelled || mFinished) {
+ if (DEBUG) Log.d(TAG, "Animation already canceled or finished, not notifying.");
+ return;
+ }
+ mShownOnFinish = shown;
+ mFinished = true;
+ setInsetsAndAlpha(shown ? mShownInsets : mHiddenInsets, mPendingAlpha, 1f /* fraction */,
+ true /* allowWhenFinished */);
+
+ if (DEBUG) Log.d(TAG, "notify control request finished for types: " + mTypes);
+ mListener.onFinished(this);
+ }
+
+ @Override
+ @VisibleForTesting
+ public float getCurrentFraction() {
+ return mAnimation.getFraction();
+ }
+
+ @Override
+ public void cancel() {
+ if (mFinished) {
+ return;
+ }
+ mPendingInsets = mLayoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN
+ ? mShownInsets : mHiddenInsets;
+ mPendingAlpha = 1f;
+ applyChangeInsets(null);
+ mCancelled = true;
+ mListener.onCancelled(mReadyDispatched ? this : null);
+ if (DEBUG) Log.d(TAG, "notify Control request cancelled for types: " + mTypes);
+
+ releaseLeashes();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return mFinished;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+
+ @Override
+ public WindowInsetsAnimation getAnimation() {
+ return mAnimation;
+ }
+
+ @Override
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(IS_CANCELLED, mCancelled);
+ proto.write(IS_FINISHED, mFinished);
+ proto.write(TMP_MATRIX, Objects.toString(mTmpMatrix));
+ proto.write(PENDING_INSETS, Objects.toString(mPendingInsets));
+ proto.write(PENDING_FRACTION, mPendingFraction);
+ proto.write(SHOWN_ON_FINISH, mShownOnFinish);
+ proto.write(CURRENT_ALPHA, mCurrentAlpha);
+ proto.write(PENDING_ALPHA, mPendingAlpha);
+ proto.end(token);
+ }
+
+ SparseArray<InsetsSourceControl> getControls() {
+ return mControls;
+ }
+
+ private Insets getInsetsFromState(InsetsState state, Rect frame,
+ @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
+ return state.calculateInsets(frame, null /* ignoringVisibilityState */,
+ false /* isScreenRound */, false /* alwaysConsumeSystemBars */,
+ LayoutParams.SOFT_INPUT_ADJUST_RESIZE /* legacySoftInputMode*/,
+ 0 /* legacyWindowFlags */, 0 /* legacySystemUiFlags */, TYPE_APPLICATION,
+ WINDOWING_MODE_UNDEFINED, typeSideMap).getInsets(mTypes);
+ }
+
+ /** Computes the insets relative to the given frame. */
+ private Insets calculateInsets(InsetsState state, Rect frame,
+ SparseArray<InsetsSourceControl> controls, boolean shown,
+ @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
+ for (int i = controls.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl control = controls.valueAt(i);
+ if (control == null) {
+ // control may be null if it got revoked.
+ continue;
+ }
+ state.getSource(control.getType()).setVisible(shown);
+ }
+ return getInsetsFromState(state, frame, typeSideMap);
+ }
+
+ /** Computes the insets from the insets hints of controls. */
+ private Insets calculateInsets(InsetsState state, SparseArray<InsetsSourceControl> controls,
+ boolean shownOrCurrent) {
+ Insets insets = Insets.NONE;
+ if (!shownOrCurrent) {
+ return insets;
+ }
+ for (int i = controls.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl control = controls.valueAt(i);
+ if (control == null) {
+ // control may be null if it got revoked.
+ continue;
+ }
+ if (state == null || state.getSource(control.getType()).isVisible()) {
+ insets = Insets.max(insets, control.getInsetsHint());
+ }
+ }
+ return insets;
+ }
+
+ private Insets sanitize(Insets insets) {
+ if (insets == null) {
+ insets = getCurrentInsets();
+ }
+ if (hasZeroInsetsIme()) {
+ return insets;
+ }
+ return Insets.max(Insets.min(insets, mShownInsets), mHiddenInsets);
+ }
+
+ private static float sanitize(float alpha) {
+ return alpha >= 1 ? 1 : (alpha <= 0 ? 0 : alpha);
+ }
+
+ private void updateLeashesForSide(@InternalInsetsSide int side, int offset, int inset,
+ ArrayList<SurfaceParams> surfaceParams, @Nullable InsetsState outState, float alpha) {
+ final ArraySet<InsetsSourceControl> controls = mSideControlsMap.get(side);
+ if (controls == null) {
+ return;
+ }
+ // TODO: Implement behavior when inset spans over multiple types
+ for (int i = controls.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl control = controls.valueAt(i);
+ final InsetsSource source = mInitialInsetsState.getSource(control.getType());
+ final SurfaceControl leash = control.getLeash();
+
+ mTmpMatrix.setTranslate(control.getSurfacePosition().x, control.getSurfacePosition().y);
+ mTmpFrame.set(source.getFrame());
+ addTranslationToMatrix(side, offset, mTmpMatrix, mTmpFrame);
+
+ final boolean visible = mHasZeroInsetsIme && side == ISIDE_BOTTOM
+ ? (mAnimationType == ANIMATION_TYPE_SHOW || !mFinished)
+ : inset != 0;
+
+ if (outState != null) {
+ outState.getSource(source.getType()).setVisible(visible);
+ outState.getSource(source.getType()).setFrame(mTmpFrame);
+ }
+
+ // If the system is controlling the insets source, the leash can be null.
+ if (leash != null) {
+ SurfaceParams params = new SurfaceParams.Builder(leash)
+ .withAlpha(alpha)
+ .withMatrix(mTmpMatrix)
+ .withVisibility(visible)
+ .build();
+ surfaceParams.add(params);
+ }
+ }
+ }
+
+ private void addTranslationToMatrix(@InternalInsetsSide int side, int offset, Matrix m,
+ Rect frame) {
+ final float surfaceOffset = mTranslator != null
+ ? mTranslator.translateLengthInAppWindowToScreen(offset) : offset;
+ switch (side) {
+ case ISIDE_LEFT:
+ m.postTranslate(-surfaceOffset, 0);
+ frame.offset(-offset, 0);
+ break;
+ case ISIDE_TOP:
+ m.postTranslate(0, -surfaceOffset);
+ frame.offset(0, -offset);
+ break;
+ case ISIDE_RIGHT:
+ m.postTranslate(surfaceOffset, 0);
+ frame.offset(offset, 0);
+ break;
+ case ISIDE_BOTTOM:
+ m.postTranslate(0, surfaceOffset);
+ frame.offset(0, offset);
+ break;
+ }
+ }
+
+ private static void buildSideControlsMap(SparseIntArray typeSideMap,
+ SparseSetArray<InsetsSourceControl> sideControlsMap,
+ SparseArray<InsetsSourceControl> controls) {
+ for (int i = typeSideMap.size() - 1; i >= 0; i--) {
+ final int type = typeSideMap.keyAt(i);
+ final int side = typeSideMap.valueAt(i);
+ final InsetsSourceControl control = controls.get(type);
+ if (control == null) {
+ // If the types that we are controlling are less than the types that the system has,
+ // there can be some null controllers.
+ continue;
+ }
+ sideControlsMap.add(side, control);
+ }
+ }
+
+ private static void buildSideControlsMap(
+ SparseSetArray<InsetsSourceControl> sideControlsMap,
+ SparseArray<InsetsSourceControl> controls) {
+ for (int i = controls.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl control = controls.valueAt(i);
+ if (control == null) {
+ // control may be null if it got revoked.
+ continue;
+ }
+ @InternalInsetsSide int side = InsetsState.getInsetSide(control.getInsetsHint());
+ if (side == ISIDE_FLOATING && control.getType() == ITYPE_IME) {
+ side = ISIDE_BOTTOM;
+ }
+ sideControlsMap.add(side, control);
+ }
+ }
+}
diff --git a/android/view/InsetsAnimationControlRunner.java b/android/view/InsetsAnimationControlRunner.java
new file mode 100644
index 0000000..1cb00e3
--- /dev/null
+++ b/android/view/InsetsAnimationControlRunner.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsController.AnimationType;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets.Type.InsetsType;
+
+/**
+ * Interface representing a runner for an insets animation.
+ *
+ * @hide
+ */
+public interface InsetsAnimationControlRunner {
+
+ /**
+ * @return The {@link InsetsType} the animation of this runner controls.
+ */
+ @InsetsType int getTypes();
+
+ /**
+ * @return The {@link InsetsType} the animation of this runner is controlling. This can be
+ * changed if a control is revoked.
+ */
+ @InsetsType int getControllingTypes();
+
+ /**
+ * Notifies {@link InsetsType types} of control are getting revoked.
+ */
+ void notifyControlRevoked(@InsetsType int types);
+
+ /**
+ * Updates the surface positions of the controls owned by this runner if there is any.
+ *
+ * @param controls An array of {@link InsetsSourceControl} that the caller newly receives.
+ */
+ void updateSurfacePosition(SparseArray<InsetsSourceControl> controls);
+
+ /**
+ * Cancels the animation.
+ */
+ void cancel();
+
+ /**
+ * @return The animation this runner is running.
+ */
+ WindowInsetsAnimation getAnimation();
+
+ /**
+ * @return Whether {@link #getTypes()} maps to a specific {@link InternalInsetsType}.
+ */
+ default boolean controlsInternalType(@InternalInsetsType int type) {
+ return InsetsState.toInternalType(getTypes()).contains(type);
+ }
+
+ /**
+ * @return The animation type this runner is running.
+ */
+ @AnimationType int getAnimationType();
+
+ /**
+ *
+ * Export the state of classes that implement this interface into a protocol buffer
+ * output stream.
+ *
+ * @param proto Stream to write the state to
+ * @param fieldId FieldId of the implementation class
+ */
+ void dumpDebug(ProtoOutputStream proto, long fieldId);
+}
diff --git a/android/view/InsetsAnimationThread.java b/android/view/InsetsAnimationThread.java
new file mode 100644
index 0000000..cdf9733
--- /dev/null
+++ b/android/view/InsetsAnimationThread.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Trace;
+
+/**
+ * Thread to be used for inset animations to be running off the main thread.
+ * @hide
+ */
+public class InsetsAnimationThread extends HandlerThread {
+
+ private static InsetsAnimationThread sInstance;
+ private static Handler sHandler;
+
+ private InsetsAnimationThread() {
+ // TODO: Should this use higher priority?
+ super("InsetsAnimations");
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new InsetsAnimationThread();
+ sInstance.start();
+ sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_VIEW);
+ sHandler = new Handler(sInstance.getLooper());
+ }
+ }
+
+ public static void release() {
+ synchronized (InsetsAnimationThread.class) {
+ if (sInstance == null) {
+ return;
+ }
+ sInstance.getLooper().quitSafely();
+ sInstance = null;
+ sHandler = null;
+ }
+ }
+
+ public static InsetsAnimationThread get() {
+ synchronized (InsetsAnimationThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+
+ public static Handler getHandler() {
+ synchronized (InsetsAnimationThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+}
diff --git a/android/view/InsetsAnimationThreadControlRunner.java b/android/view/InsetsAnimationThreadControlRunner.java
new file mode 100644
index 0000000..691e638
--- /dev/null
+++ b/android/view/InsetsAnimationThreadControlRunner.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsController.DEBUG;
+import static android.view.SyncRtSurfaceTransactionApplier.applyParams;
+
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.content.res.CompatibilityInfo;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Trace;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsController.AnimationType;
+import android.view.InsetsController.LayoutInsetsDuringAnimation;
+import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowInsetsAnimation.Bounds;
+import android.view.animation.Interpolator;
+
+/**
+ * Insets animation runner that uses {@link InsetsAnimationThread} to run the animation off from the
+ * main thread.
+ *
+ * @hide
+ */
+public class InsetsAnimationThreadControlRunner implements InsetsAnimationControlRunner {
+
+ private static final String TAG = "InsetsAnimThreadRunner";
+ private final InsetsAnimationControlImpl mControl;
+ private final InsetsAnimationControlCallbacks mOuterCallbacks;
+ private final Handler mMainThreadHandler;
+ private final InsetsAnimationControlCallbacks mCallbacks =
+ new InsetsAnimationControlCallbacks() {
+
+ private final float[] mTmpFloat9 = new float[9];
+
+ @Override
+ @UiThread
+ public void startAnimation(InsetsAnimationControlImpl controller,
+ WindowInsetsAnimationControlListener listener, int types,
+ WindowInsetsAnimation animation, Bounds bounds) {
+ // Animation will be started in constructor already.
+ }
+
+ @Override
+ public void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner) {
+ synchronized (mControl) {
+ // This reads the surface position on the animation thread, but the surface position
+ // would be updated on the UI thread, so we need this critical section.
+ // See: updateSurfacePosition.
+ mControl.applyChangeInsets(null /* outState */);
+ }
+ }
+
+ @Override
+ public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) {
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW,
+ "InsetsAsyncAnimation: " + WindowInsets.Type.toString(runner.getTypes()),
+ runner.getTypes());
+ releaseControls(mControl.getControls());
+ mMainThreadHandler.post(() ->
+ mOuterCallbacks.notifyFinished(InsetsAnimationThreadControlRunner.this, shown));
+ }
+
+ @Override
+ public void applySurfaceParams(SurfaceParams... params) {
+ if (DEBUG) Log.d(TAG, "applySurfaceParams");
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (int i = params.length - 1; i >= 0; i--) {
+ SyncRtSurfaceTransactionApplier.SurfaceParams surfaceParams = params[i];
+ applyParams(t, surfaceParams, mTmpFloat9);
+ }
+ t.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
+ t.apply();
+ t.close();
+ }
+
+ @Override
+ public void releaseSurfaceControlFromRt(SurfaceControl sc) {
+ if (DEBUG) Log.d(TAG, "releaseSurfaceControlFromRt");
+ // Since we don't push the SurfaceParams to the RT we can release directly
+ sc.release();
+ }
+
+ @Override
+ public void reportPerceptible(int types, boolean perceptible) {
+ mMainThreadHandler.post(() -> mOuterCallbacks.reportPerceptible(types, perceptible));
+ }
+ };
+
+ @UiThread
+ public InsetsAnimationThreadControlRunner(SparseArray<InsetsSourceControl> controls,
+ @Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener,
+ @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
+ Interpolator interpolator, @AnimationType int animationType,
+ @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
+ CompatibilityInfo.Translator translator, Handler mainThreadHandler) {
+ mMainThreadHandler = mainThreadHandler;
+ mOuterCallbacks = controller;
+ mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types,
+ mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
+ translator);
+ InsetsAnimationThread.getHandler().post(() -> {
+ if (mControl.isCancelled()) {
+ return;
+ }
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW,
+ "InsetsAsyncAnimation: " + WindowInsets.Type.toString(types), types);
+ listener.onReady(mControl, types);
+ });
+ }
+
+ private void releaseControls(SparseArray<InsetsSourceControl> controls) {
+ for (int i = controls.size() - 1; i >= 0; i--) {
+ controls.valueAt(i).release(SurfaceControl::release);
+ }
+ }
+
+ @Override
+ @UiThread
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ mControl.dumpDebug(proto, fieldId);
+ }
+
+ @Override
+ @UiThread
+ public int getTypes() {
+ return mControl.getTypes();
+ }
+
+ @Override
+ @UiThread
+ public int getControllingTypes() {
+ return mControl.getControllingTypes();
+ }
+
+ @Override
+ @UiThread
+ public void notifyControlRevoked(@InsetsType int types) {
+ mControl.notifyControlRevoked(types);
+ }
+
+ @Override
+ @UiThread
+ public void updateSurfacePosition(SparseArray<InsetsSourceControl> controls) {
+ synchronized (mControl) {
+ // This is called from the UI thread, however, the surface position will be used on the
+ // animation thread, so we need this critical section. See: scheduleApplyChangeInsets.
+ mControl.updateSurfacePosition(controls);
+ }
+ }
+
+ @Override
+ @UiThread
+ public void cancel() {
+ InsetsAnimationThread.getHandler().post(mControl::cancel);
+ }
+
+ @Override
+ @UiThread
+ public WindowInsetsAnimation getAnimation() {
+ return mControl.getAnimation();
+ }
+
+ @Override
+ public int getAnimationType() {
+ return mControl.getAnimationType();
+ }
+}
diff --git a/android/view/InsetsController.java b/android/view/InsetsController.java
new file mode 100644
index 0000000..6f915c9
--- /dev/null
+++ b/android/view/InsetsController.java
@@ -0,0 +1,1632 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.Trace.TRACE_TAG_VIEW;
+import static android.view.InsetsControllerProto.CONTROL;
+import static android.view.InsetsControllerProto.STATE;
+import static android.view.InsetsState.ITYPE_CAPTION_BAR;
+import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsState.toInternalType;
+import static android.view.InsetsState.toPublicType;
+import static android.view.WindowInsets.Type.all;
+import static android.view.WindowInsets.Type.ime;
+
+import android.animation.AnimationHandler;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.CompatibilityInfo;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Trace;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.imetracing.ImeTracing;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsSourceConsumer.ShowResult;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.SurfaceControl.Transaction;
+import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowInsetsAnimation.Bounds;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+/**
+ * Implements {@link WindowInsetsController} on the client.
+ * @hide
+ */
+public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks {
+
+ private int mTypesBeingCancelled;
+
+ public interface Host {
+
+ Handler getHandler();
+
+ /**
+ * Notifies host that {@link InsetsController#getState()} has changed.
+ */
+ void notifyInsetsChanged();
+
+ void dispatchWindowInsetsAnimationPrepare(@NonNull WindowInsetsAnimation animation);
+ Bounds dispatchWindowInsetsAnimationStart(
+ @NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds);
+ WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets,
+ @NonNull List<WindowInsetsAnimation> runningAnimations);
+ void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation);
+
+ /**
+ * Requests host to apply surface params in synchronized manner.
+ */
+ void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params);
+
+ /**
+ * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean)
+ */
+ void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible,
+ boolean hasControl);
+
+ /**
+ * Called when insets have been modified by the client and should be reported back to WM.
+ */
+ void onInsetsModified(InsetsState insetsState);
+
+ /**
+ * @return Whether the host has any callbacks it wants to synchronize the animations with.
+ * If there are no callbacks, the animation will be off-loaded to another thread and
+ * slightly different animation curves are picked.
+ */
+ boolean hasAnimationCallbacks();
+
+ /**
+ * @see WindowInsetsController#setSystemBarsAppearance
+ */
+ void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask);
+
+ /**
+ * @see WindowInsetsController#getSystemBarsAppearance()
+ */
+ @Appearance int getSystemBarsAppearance();
+
+ default boolean isSystemBarsAppearanceControlled() {
+ return false;
+ }
+
+ /**
+ * @see WindowInsetsController#setSystemBarsBehavior
+ */
+ void setSystemBarsBehavior(@Behavior int behavior);
+
+ /**
+ * @see WindowInsetsController#getSystemBarsBehavior
+ */
+ @Behavior int getSystemBarsBehavior();
+
+ default boolean isSystemBarsBehaviorControlled() {
+ return false;
+ }
+
+ /**
+ * Releases a surface and ensure that this is done after {@link #applySurfaceParams} has
+ * finished applying params.
+ */
+ void releaseSurfaceControlFromRt(SurfaceControl surfaceControl);
+
+ /**
+ * If this host is a view hierarchy, adds a pre-draw runnable to ensure proper ordering as
+ * described in {@link WindowInsetsAnimation.Callback#onPrepare}.
+ *
+ * If this host isn't a view hierarchy, the runnable can be executed immediately.
+ */
+ void addOnPreDrawRunnable(Runnable r);
+
+ /**
+ * Adds a runnbale to be executed during {@link Choreographer#CALLBACK_INSETS_ANIMATION}
+ * phase.
+ */
+ void postInsetsAnimationCallback(Runnable r);
+
+ /**
+ * Obtains {@link InputMethodManager} instance from host.
+ */
+ InputMethodManager getInputMethodManager();
+
+ /**
+ * @return title of the rootView, if it has one.
+ * Note: this method is for debugging purposes only.
+ */
+ @Nullable
+ String getRootViewTitle();
+
+ /** @see ViewRootImpl#dipToPx */
+ int dipToPx(int dips);
+
+ /**
+ * @return token associated with the host, if it has one.
+ */
+ @Nullable
+ IBinder getWindowToken();
+
+ /**
+ * @return Translator associated with the host, if it has one.
+ */
+ @Nullable
+ default CompatibilityInfo.Translator getTranslator() {
+ return null;
+ }
+ }
+
+ private static final String TAG = "InsetsController";
+ private static final int ANIMATION_DURATION_MOVE_IN_MS = 275;
+ private static final int ANIMATION_DURATION_MOVE_OUT_MS = 340;
+ private static final int ANIMATION_DURATION_FADE_IN_MS = 500;
+ private static final int ANIMATION_DURATION_FADE_OUT_MS = 1500;
+
+ private static final int ANIMATION_DELAY_DIM_MS = 500;
+
+ private static final int ANIMATION_DURATION_SYNC_IME_MS = 285;
+ private static final int ANIMATION_DURATION_UNSYNC_IME_MS = 200;
+
+ private static final int PENDING_CONTROL_TIMEOUT_MS = 2000;
+
+ private static final Interpolator SYSTEM_BARS_INSETS_INTERPOLATOR =
+ new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+ private static final Interpolator SYSTEM_BARS_ALPHA_INTERPOLATOR =
+ new PathInterpolator(0.3f, 0f, 1f, 1f);
+ private static final Interpolator SYSTEM_BARS_DIM_INTERPOLATOR = alphaFraction -> {
+ // While playing dim animation, alphaFraction is changed from 1f to 0f. Here changes it to
+ // time-based fraction for computing delay and interpolation.
+ float fraction = 1 - alphaFraction;
+ final float fractionDelay = (float) ANIMATION_DELAY_DIM_MS / ANIMATION_DURATION_FADE_OUT_MS;
+ if (fraction <= fractionDelay) {
+ return 1f;
+ } else {
+ float innerFraction = (fraction - fractionDelay) / (1f - fractionDelay);
+ return 1f - SYSTEM_BARS_ALPHA_INTERPOLATOR.getInterpolation(innerFraction);
+ }
+ };
+ private static final Interpolator SYNC_IME_INTERPOLATOR =
+ new PathInterpolator(0.2f, 0f, 0f, 1f);
+ private static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR =
+ new PathInterpolator(0, 0, 0.2f, 1f);
+ private static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR =
+ new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+ /** The amount IME will move up/down when animating in floating mode. */
+ private static final int FLOATING_IME_BOTTOM_INSET_DP = -80;
+
+ static final boolean DEBUG = false;
+ static final boolean WARN = false;
+
+ /**
+ * Layout mode during insets animation: The views should be laid out as if the changing inset
+ * types are fully shown. Before starting the animation, {@link View#onApplyWindowInsets} will
+ * be called as if the changing insets types are shown, which will result in the views being
+ * laid out as if the insets are fully shown.
+ */
+ public static final int LAYOUT_INSETS_DURING_ANIMATION_SHOWN = 0;
+
+ /**
+ * Layout mode during insets animation: The views should be laid out as if the changing inset
+ * types are fully hidden. Before starting the animation, {@link View#onApplyWindowInsets} will
+ * be called as if the changing insets types are hidden, which will result in the views being
+ * laid out as if the insets are fully hidden.
+ */
+ public static final int LAYOUT_INSETS_DURING_ANIMATION_HIDDEN = 1;
+
+ /**
+ * Determines the behavior of how the views should be laid out during an insets animation that
+ * is controlled by the application by calling {@link #controlWindowInsetsAnimation}.
+ * <p>
+ * When the animation is system-initiated, the layout mode is always chosen such that the
+ * pre-animation layout will represent the opposite of the starting state, i.e. when insets
+ * are appearing, {@link #LAYOUT_INSETS_DURING_ANIMATION_SHOWN} will be used. When insets
+ * are disappearing, {@link #LAYOUT_INSETS_DURING_ANIMATION_HIDDEN} will be used.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {LAYOUT_INSETS_DURING_ANIMATION_SHOWN,
+ LAYOUT_INSETS_DURING_ANIMATION_HIDDEN})
+ @interface LayoutInsetsDuringAnimation {
+ }
+
+ /** Not running an animation. */
+ @VisibleForTesting
+ public static final int ANIMATION_TYPE_NONE = -1;
+
+ /** Running animation will show insets */
+ @VisibleForTesting
+ public static final int ANIMATION_TYPE_SHOW = 0;
+
+ /** Running animation will hide insets */
+ @VisibleForTesting
+ public static final int ANIMATION_TYPE_HIDE = 1;
+
+ /** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */
+ @VisibleForTesting
+ public static final int ANIMATION_TYPE_USER = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {ANIMATION_TYPE_NONE, ANIMATION_TYPE_SHOW, ANIMATION_TYPE_HIDE,
+ ANIMATION_TYPE_USER})
+ @interface AnimationType {
+ }
+
+ /**
+ * Translation animation evaluator.
+ */
+ private static TypeEvaluator<Insets> sEvaluator = (fraction, startValue, endValue) -> Insets.of(
+ (int) (startValue.left + fraction * (endValue.left - startValue.left)),
+ (int) (startValue.top + fraction * (endValue.top - startValue.top)),
+ (int) (startValue.right + fraction * (endValue.right - startValue.right)),
+ (int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom)));
+
+ /**
+ * The default implementation of listener, to be used by InsetsController and InsetsPolicy to
+ * animate insets.
+ */
+ public static class InternalAnimationControlListener
+ implements WindowInsetsAnimationControlListener {
+
+ private WindowInsetsAnimationController mController;
+ private ValueAnimator mAnimator;
+ private final boolean mShow;
+ private final boolean mHasAnimationCallbacks;
+ private final @InsetsType int mRequestedTypes;
+ private final @Behavior int mBehavior;
+ private final long mDurationMs;
+ private final boolean mDisable;
+ private final int mFloatingImeBottomInset;
+
+ private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+ new ThreadLocal<AnimationHandler>() {
+ @Override
+ protected AnimationHandler initialValue() {
+ AnimationHandler handler = new AnimationHandler();
+ handler.setProvider(new SfVsyncFrameCallbackProvider());
+ return handler;
+ }
+ };
+
+ public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks,
+ @InsetsType int requestedTypes, @Behavior int behavior, boolean disable,
+ int floatingImeBottomInset) {
+ mShow = show;
+ mHasAnimationCallbacks = hasAnimationCallbacks;
+ mRequestedTypes = requestedTypes;
+ mBehavior = behavior;
+ mDurationMs = calculateDurationMs();
+ mDisable = disable;
+ mFloatingImeBottomInset = floatingImeBottomInset;
+ }
+
+ @Override
+ public void onReady(WindowInsetsAnimationController controller, int types) {
+ mController = controller;
+ if (DEBUG) Log.d(TAG, "default animation onReady types: " + types);
+
+ if (mDisable) {
+ onAnimationFinish();
+ return;
+ }
+ mAnimator = ValueAnimator.ofFloat(0f, 1f);
+ mAnimator.setDuration(mDurationMs);
+ mAnimator.setInterpolator(new LinearInterpolator());
+ Insets hiddenInsets = controller.getHiddenStateInsets();
+ // IME with zero insets is a special case: it will animate-in from offscreen and end
+ // with final insets of zero and vice-versa.
+ hiddenInsets = controller.hasZeroInsetsIme()
+ ? Insets.of(hiddenInsets.left, hiddenInsets.top, hiddenInsets.right,
+ mFloatingImeBottomInset)
+ : hiddenInsets;
+ Insets start = mShow
+ ? hiddenInsets
+ : controller.getShownStateInsets();
+ Insets end = mShow
+ ? controller.getShownStateInsets()
+ : hiddenInsets;
+ Interpolator insetsInterpolator = getInsetsInterpolator();
+ Interpolator alphaInterpolator = getAlphaInterpolator();
+ mAnimator.addUpdateListener(animation -> {
+ float rawFraction = animation.getAnimatedFraction();
+ float alphaFraction = mShow
+ ? rawFraction
+ : 1 - rawFraction;
+ float insetsFraction = insetsInterpolator.getInterpolation(rawFraction);
+ controller.setInsetsAndAlpha(
+ sEvaluator.evaluate(insetsFraction, start, end),
+ alphaInterpolator.getInterpolation(alphaFraction),
+ rawFraction);
+ if (DEBUG) Log.d(TAG, "Default animation setInsetsAndAlpha fraction: "
+ + insetsFraction);
+ });
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onAnimationFinish();
+ }
+ });
+ if (!mHasAnimationCallbacks) {
+ mAnimator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
+ }
+ mAnimator.start();
+ }
+
+ @Override
+ public void onFinished(WindowInsetsAnimationController controller) {
+ if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:"
+ + Type.toString(mRequestedTypes));
+ }
+
+ @Override
+ public void onCancelled(WindowInsetsAnimationController controller) {
+ // Animator can be null when it is cancelled before onReady() completes.
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:"
+ + mRequestedTypes);
+ }
+
+ protected Interpolator getInsetsInterpolator() {
+ if ((mRequestedTypes & ime()) != 0) {
+ if (mHasAnimationCallbacks) {
+ return SYNC_IME_INTERPOLATOR;
+ } else if (mShow) {
+ return LINEAR_OUT_SLOW_IN_INTERPOLATOR;
+ } else {
+ return FAST_OUT_LINEAR_IN_INTERPOLATOR;
+ }
+ } else {
+ if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
+ return SYSTEM_BARS_INSETS_INTERPOLATOR;
+ } else {
+ // Makes insets stay at the shown position.
+ return input -> mShow ? 1f : 0f;
+ }
+ }
+ }
+
+ Interpolator getAlphaInterpolator() {
+ if ((mRequestedTypes & ime()) != 0) {
+ if (mHasAnimationCallbacks) {
+ return input -> 1f;
+ } else if (mShow) {
+
+ // Alpha animation takes half the time with linear interpolation;
+ return input -> Math.min(1f, 2 * input);
+ } else {
+ return FAST_OUT_LINEAR_IN_INTERPOLATOR;
+ }
+ } else {
+ if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
+ return input -> 1f;
+ } else {
+ if (mShow) {
+ return SYSTEM_BARS_ALPHA_INTERPOLATOR;
+ } else {
+ return SYSTEM_BARS_DIM_INTERPOLATOR;
+ }
+ }
+ }
+ }
+
+ protected void onAnimationFinish() {
+ mController.finish(mShow);
+ if (DEBUG) Log.d(TAG, "onAnimationFinish showOnFinish: " + mShow);
+ }
+
+ /**
+ * To get the animation duration in MS.
+ */
+ public long getDurationMs() {
+ return mDurationMs;
+ }
+
+ private long calculateDurationMs() {
+ if ((mRequestedTypes & ime()) != 0) {
+ if (mHasAnimationCallbacks) {
+ return ANIMATION_DURATION_SYNC_IME_MS;
+ } else {
+ return ANIMATION_DURATION_UNSYNC_IME_MS;
+ }
+ } else {
+ if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
+ return mShow ? ANIMATION_DURATION_MOVE_IN_MS : ANIMATION_DURATION_MOVE_OUT_MS;
+ } else {
+ return mShow ? ANIMATION_DURATION_FADE_IN_MS : ANIMATION_DURATION_FADE_OUT_MS;
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a running animation
+ */
+ private static class RunningAnimation {
+
+ RunningAnimation(InsetsAnimationControlRunner runner, int type) {
+ this.runner = runner;
+ this.type = type;
+ }
+
+ final InsetsAnimationControlRunner runner;
+ final @AnimationType int type;
+
+ /**
+ * Whether {@link WindowInsetsAnimation.Callback#onStart(WindowInsetsAnimation, Bounds)} has
+ * been dispatched already for this animation.
+ */
+ boolean startDispatched;
+ }
+
+ /**
+ * Represents a control request that we had to defer because we are waiting for the IME to
+ * process our show request.
+ */
+ private static class PendingControlRequest {
+
+ PendingControlRequest(@InsetsType int types, WindowInsetsAnimationControlListener listener,
+ long durationMs, Interpolator interpolator, @AnimationType int animationType,
+ @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
+ CancellationSignal cancellationSignal, boolean useInsetsAnimationThread) {
+ this.types = types;
+ this.listener = listener;
+ this.durationMs = durationMs;
+ this.interpolator = interpolator;
+ this.animationType = animationType;
+ this.layoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
+ this.cancellationSignal = cancellationSignal;
+ this.useInsetsAnimationThread = useInsetsAnimationThread;
+ }
+
+ final @InsetsType int types;
+ final WindowInsetsAnimationControlListener listener;
+ final long durationMs;
+ final Interpolator interpolator;
+ final @AnimationType int animationType;
+ final @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation;
+ final CancellationSignal cancellationSignal;
+ final boolean useInsetsAnimationThread;
+ }
+
+ /** The local state */
+ private final InsetsState mState = new InsetsState();
+
+ /** The state dispatched from server */
+ private final InsetsState mLastDispatchedState = new InsetsState();
+
+ // TODO: Use other class to represent the requested visibility of each type, because the
+ // display frame and the frame in each source are not used.
+ /** The requested visibilities sent to server */
+ private final InsetsState mRequestedState = new InsetsState();
+
+ private final Rect mFrame = new Rect();
+ private final BiFunction<InsetsController, Integer, InsetsSourceConsumer> mConsumerCreator;
+ private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
+ private final Host mHost;
+ private final Handler mHandler;
+
+ private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
+ private final ArrayList<RunningAnimation> mRunningAnimations = new ArrayList<>();
+ private final ArrayList<InsetsAnimationControlImpl> mTmpFinishedControls = new ArrayList<>();
+ private final ArraySet<InsetsSourceConsumer> mRequestedVisibilityChanged = new ArraySet<>();
+ private WindowInsets mLastInsets;
+
+ private boolean mAnimCallbackScheduled;
+
+ private final Runnable mAnimCallback;
+
+ /** Pending control request that is waiting on IME to be ready to be shown */
+ private PendingControlRequest mPendingImeControlRequest;
+
+ private int mWindowType;
+ private int mLastLegacySoftInputMode;
+ private int mLastLegacyWindowFlags;
+ private int mLastLegacySystemUiFlags;
+ private int mLastWindowingMode;
+ private boolean mStartingAnimation;
+ private int mCaptionInsetsHeight = 0;
+ private boolean mAnimationsDisabled;
+
+ private Runnable mPendingControlTimeout = this::abortPendingImeControlRequest;
+ private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
+ = new ArrayList<>();
+
+ /** Set of inset types for which an animation was started since last resetting this field */
+ private @InsetsType int mLastStartedAnimTypes;
+
+ /** Set of inset types which cannot be controlled by the user animation */
+ private @InsetsType int mDisabledUserAnimationInsetsTypes;
+
+ private Runnable mInvokeControllableInsetsChangedListeners =
+ this::invokeControllableInsetsChangedListeners;
+
+ public InsetsController(Host host) {
+ this(host, (controller, type) -> {
+ if (type == ITYPE_IME) {
+ return new ImeInsetsSourceConsumer(controller.mState, Transaction::new, controller);
+ } else {
+ return new InsetsSourceConsumer(type, controller.mState, Transaction::new,
+ controller);
+ }
+ }, host.getHandler());
+ }
+
+ @VisibleForTesting
+ public InsetsController(Host host,
+ BiFunction<InsetsController, Integer, InsetsSourceConsumer> consumerCreator,
+ Handler handler) {
+ mHost = host;
+ mConsumerCreator = consumerCreator;
+ mHandler = handler;
+ mAnimCallback = () -> {
+ mAnimCallbackScheduled = false;
+ if (mRunningAnimations.isEmpty()) {
+ return;
+ }
+
+ final List<WindowInsetsAnimation> runningAnimations = new ArrayList<>();
+ final InsetsState state = new InsetsState(mState, true /* copySources */);
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ RunningAnimation runningAnimation = mRunningAnimations.get(i);
+ if (DEBUG) Log.d(TAG, "Running animation type: " + runningAnimation.type);
+ InsetsAnimationControlRunner runner = runningAnimation.runner;
+ if (runner instanceof InsetsAnimationControlImpl) {
+ InsetsAnimationControlImpl control = (InsetsAnimationControlImpl) runner;
+
+ // Keep track of running animation to be dispatched. Aggregate it here such that
+ // if it gets finished within applyChangeInsets we still dispatch it to
+ // onProgress.
+ if (runningAnimation.startDispatched) {
+ runningAnimations.add(control.getAnimation());
+ }
+
+ if (control.applyChangeInsets(state)) {
+ mTmpFinishedControls.add(control);
+ }
+ }
+ }
+
+ WindowInsets insets = state.calculateInsets(mFrame, mState /* ignoringVisibilityState*/,
+ mLastInsets.isRound(), mLastInsets.shouldAlwaysConsumeSystemBars(),
+ mLastLegacySoftInputMode, mLastLegacyWindowFlags, mLastLegacySystemUiFlags,
+ mWindowType, mLastWindowingMode, null /* typeSideMap */);
+ mHost.dispatchWindowInsetsAnimationProgress(insets,
+ Collections.unmodifiableList(runningAnimations));
+ if (DEBUG) {
+ for (WindowInsetsAnimation anim : runningAnimations) {
+ Log.d(TAG, String.format("Running animation type: %d, progress: %f",
+ anim.getTypeMask(), anim.getInterpolatedFraction()));
+ }
+ }
+
+ for (int i = mTmpFinishedControls.size() - 1; i >= 0; i--) {
+ dispatchAnimationEnd(mTmpFinishedControls.get(i).getAnimation());
+ }
+ mTmpFinishedControls.clear();
+ };
+ }
+
+ @VisibleForTesting
+ public void onFrameChanged(Rect frame) {
+ if (mFrame.equals(frame)) {
+ return;
+ }
+ mHost.notifyInsetsChanged();
+ mFrame.set(frame);
+ }
+
+ @Override
+ public InsetsState getState() {
+ return mState;
+ }
+
+ @Override
+ public boolean isRequestedVisible(int type) {
+ return getSourceConsumer(type).isRequestedVisible();
+ }
+
+ public InsetsState getLastDispatchedState() {
+ return mLastDispatchedState;
+ }
+
+ @VisibleForTesting
+ public boolean onStateChanged(InsetsState state) {
+ boolean stateChanged = !mState.equals(state, true /* excludingCaptionInsets */,
+ false /* excludeInvisibleIme */)
+ || !captionInsetsUnchanged();
+ if (!stateChanged && mLastDispatchedState.equals(state)) {
+ return false;
+ }
+ if (DEBUG) Log.d(TAG, "onStateChanged: " + state);
+ mLastDispatchedState.set(state, true /* copySources */);
+
+ final InsetsState lastState = new InsetsState(mState, true /* copySources */);
+ updateState(state);
+ applyLocalVisibilityOverride();
+
+ if (!mState.equals(lastState, false /* excludingCaptionInsets */,
+ true /* excludeInvisibleIme */)) {
+ if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged");
+ mHost.notifyInsetsChanged();
+ }
+ return true;
+ }
+
+ private void updateState(InsetsState newState) {
+ mState.setDisplayFrame(newState.getDisplayFrame());
+ mState.setDisplayCutout(newState.getDisplayCutout());
+ mState.setRoundedCorners(newState.getRoundedCorners());
+ mState.setPrivacyIndicatorBounds(newState.getPrivacyIndicatorBounds());
+ @InsetsType int disabledUserAnimationTypes = 0;
+ @InsetsType int[] cancelledUserAnimationTypes = {0};
+ for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
+ InsetsSource source = newState.peekSource(type);
+ if (source == null) continue;
+ @AnimationType int animationType = getAnimationType(type);
+ if (!source.isUserControllable()) {
+ @InsetsType int insetsType = toPublicType(type);
+ // The user animation is not allowed when visible frame is empty.
+ disabledUserAnimationTypes |= insetsType;
+ if (animationType == ANIMATION_TYPE_USER) {
+ // Existing user animation needs to be cancelled.
+ animationType = ANIMATION_TYPE_NONE;
+ cancelledUserAnimationTypes[0] |= insetsType;
+ }
+ }
+ getSourceConsumer(type).updateSource(source, animationType);
+ }
+ for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
+ // Only update the server side insets here.
+ if (type == ITYPE_CAPTION_BAR) continue;
+ InsetsSource source = mState.peekSource(type);
+ if (source == null) continue;
+ if (newState.peekSource(type) == null) {
+ mState.removeSource(type);
+ }
+ }
+
+ updateDisabledUserAnimationTypes(disabledUserAnimationTypes);
+
+ if (cancelledUserAnimationTypes[0] != 0) {
+ mHandler.post(() -> show(cancelledUserAnimationTypes[0]));
+ }
+ }
+
+ private void updateDisabledUserAnimationTypes(@InsetsType int disabledUserAnimationTypes) {
+ @InsetsType int diff = mDisabledUserAnimationInsetsTypes ^ disabledUserAnimationTypes;
+ if (diff != 0) {
+ for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
+ InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+ if (consumer.getControl() != null
+ && (toPublicType(consumer.getType()) & diff) != 0) {
+ mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
+ mHandler.post(mInvokeControllableInsetsChangedListeners);
+ break;
+ }
+ }
+ mDisabledUserAnimationInsetsTypes = disabledUserAnimationTypes;
+ }
+ }
+
+ private boolean captionInsetsUnchanged() {
+ if (mState.peekSource(ITYPE_CAPTION_BAR) == null
+ && mCaptionInsetsHeight == 0) {
+ return true;
+ }
+ if (mState.peekSource(ITYPE_CAPTION_BAR) != null
+ && mCaptionInsetsHeight
+ == mState.peekSource(ITYPE_CAPTION_BAR).getFrame().height()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @see InsetsState#calculateInsets
+ */
+ @VisibleForTesting
+ public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars,
+ int windowType, int windowingMode, int legacySoftInputMode, int legacyWindowFlags,
+ int legacySystemUiFlags) {
+ mWindowType = windowType;
+ mLastWindowingMode = windowingMode;
+ mLastLegacySoftInputMode = legacySoftInputMode;
+ mLastLegacyWindowFlags = legacyWindowFlags;
+ mLastLegacySystemUiFlags = legacySystemUiFlags;
+ mLastInsets = mState.calculateInsets(mFrame, null /* ignoringVisibilityState*/,
+ isScreenRound, alwaysConsumeSystemBars, legacySoftInputMode, legacyWindowFlags,
+ legacySystemUiFlags, windowType, windowingMode, null /* typeSideMap */);
+ return mLastInsets;
+ }
+
+ /**
+ * @see InsetsState#calculateVisibleInsets(Rect, int)
+ */
+ public Rect calculateVisibleInsets(@SoftInputModeFlags int softInputMode) {
+ return mState.calculateVisibleInsets(mFrame, softInputMode);
+ }
+
+ /**
+ * Called when the server has dispatched us a new set of inset controls.
+ */
+ public void onControlsChanged(InsetsSourceControl[] activeControls) {
+ if (activeControls != null) {
+ for (InsetsSourceControl activeControl : activeControls) {
+ if (activeControl != null) {
+ // TODO(b/122982984): Figure out why it can be null.
+ mTmpControlArray.put(activeControl.getType(), activeControl);
+ }
+ }
+ }
+
+ boolean requestedStateStale = false;
+ final int[] showTypes = new int[1];
+ final int[] hideTypes = new int[1];
+
+ // Ensure to update all existing source consumers
+ for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
+ final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+ final InsetsSourceControl control = mTmpControlArray.get(consumer.getType());
+
+ // control may be null, but we still need to update the control to null if it got
+ // revoked.
+ consumer.setControl(control, showTypes, hideTypes);
+ }
+
+ // Ensure to create source consumers if not available yet.
+ for (int i = mTmpControlArray.size() - 1; i >= 0; i--) {
+ final InsetsSourceControl control = mTmpControlArray.valueAt(i);
+ final @InternalInsetsType int type = control.getType();
+ final InsetsSourceConsumer consumer = getSourceConsumer(type);
+ consumer.setControl(control, showTypes, hideTypes);
+
+ if (!requestedStateStale) {
+ final boolean requestedVisible = consumer.isRequestedVisible();
+
+ // We might have changed our requested visibilities while we don't have the control,
+ // so we need to update our requested state once we have control. Otherwise, our
+ // requested state at the server side might be incorrect.
+ final boolean requestedVisibilityChanged =
+ requestedVisible != mRequestedState.getSourceOrDefaultVisibility(type);
+
+ // The IME client visibility will be reset by insets source provider while updating
+ // control, so if IME is requested visible, we need to send the request to server.
+ final boolean imeRequestedVisible = type == ITYPE_IME && requestedVisible;
+
+ requestedStateStale = requestedVisibilityChanged || imeRequestedVisible;
+ }
+ }
+
+ if (mTmpControlArray.size() > 0) {
+ // Update surface positions for animations.
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ mRunningAnimations.get(i).runner.updateSurfacePosition(mTmpControlArray);
+ }
+ }
+ mTmpControlArray.clear();
+
+ // Do not override any animations that the app started in the OnControllableInsetsChanged
+ // listeners.
+ int animatingTypes = invokeControllableInsetsChangedListeners();
+ showTypes[0] &= ~animatingTypes;
+ hideTypes[0] &= ~animatingTypes;
+
+ if (showTypes[0] != 0) {
+ applyAnimation(showTypes[0], true /* show */, false /* fromIme */);
+ }
+ if (hideTypes[0] != 0) {
+ applyAnimation(hideTypes[0], false /* show */, false /* fromIme */);
+ }
+
+ // InsetsSourceConsumer#setControl might change the requested visibility.
+ updateRequestedVisibility();
+ }
+
+ @Override
+ public void show(@InsetsType int types) {
+ show(types, false /* fromIme */);
+ }
+
+ @VisibleForTesting
+ public void show(@InsetsType int types, boolean fromIme) {
+ if ((types & ime()) != 0) {
+ Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")");
+ }
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump("InsetsController#show",
+ mHost.getInputMethodManager(), null /* icProto */);
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0);
+ } else {
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
+ // Handle pending request ready in case there was one set.
+ if (fromIme && mPendingImeControlRequest != null) {
+ PendingControlRequest pendingRequest = mPendingImeControlRequest;
+ mPendingImeControlRequest = null;
+ mHandler.removeCallbacks(mPendingControlTimeout);
+
+ // We are about to playing the default animation. Passing a null frame indicates the
+ // controlled types should be animated regardless of the frame.
+ controlAnimationUnchecked(
+ pendingRequest.types, pendingRequest.cancellationSignal,
+ pendingRequest.listener, null /* frame */,
+ true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
+ pendingRequest.animationType,
+ pendingRequest.layoutInsetsDuringAnimation,
+ pendingRequest.useInsetsAnimationThread);
+ return;
+ }
+
+ // TODO: Support a ResultReceiver for IME.
+ // TODO(b/123718661): Make show() work for multi-session IME.
+ int typesReady = 0;
+ final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
+ for (int i = internalTypes.size() - 1; i >= 0; i--) {
+ @InternalInsetsType int internalType = internalTypes.valueAt(i);
+ @AnimationType int animationType = getAnimationType(internalType);
+ InsetsSourceConsumer consumer = getSourceConsumer(internalType);
+ if (consumer.isRequestedVisible() && animationType == ANIMATION_TYPE_NONE
+ || animationType == ANIMATION_TYPE_SHOW) {
+ // no-op: already shown or animating in (because window visibility is
+ // applied before starting animation).
+ if (DEBUG) Log.d(TAG, String.format(
+ "show ignored for type: %d animType: %d requestedVisible: %s",
+ consumer.getType(), animationType, consumer.isRequestedVisible()));
+ continue;
+ }
+ if (fromIme && animationType == ANIMATION_TYPE_USER) {
+ // App is already controlling the IME, don't cancel it.
+ continue;
+ }
+ typesReady |= InsetsState.toPublicType(consumer.getType());
+ }
+ if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
+ applyAnimation(typesReady, true /* show */, fromIme);
+ }
+
+ @Override
+ public void hide(@InsetsType int types) {
+ hide(types, false /* fromIme */);
+ }
+
+ @VisibleForTesting
+ public void hide(@InsetsType int types, boolean fromIme) {
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump("InsetsController#hide",
+ mHost.getInputMethodManager(), null /* icProto */);
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0);
+ } else {
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0);
+ }
+ int typesReady = 0;
+ final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
+ for (int i = internalTypes.size() - 1; i >= 0; i--) {
+ @InternalInsetsType int internalType = internalTypes.valueAt(i);
+ @AnimationType int animationType = getAnimationType(internalType);
+ InsetsSourceConsumer consumer = getSourceConsumer(internalType);
+ if (!consumer.isRequestedVisible() && animationType == ANIMATION_TYPE_NONE
+ || animationType == ANIMATION_TYPE_HIDE) {
+ // no-op: already hidden or animating out.
+ continue;
+ }
+ typesReady |= InsetsState.toPublicType(consumer.getType());
+ }
+ applyAnimation(typesReady, false /* show */, fromIme /* fromIme */);
+ }
+
+ @Override
+ public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
+ @Nullable Interpolator interpolator,
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull WindowInsetsAnimationControlListener listener) {
+ controlWindowInsetsAnimation(types, cancellationSignal, listener,
+ false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER);
+ }
+
+ private void controlWindowInsetsAnimation(@InsetsType int types,
+ @Nullable CancellationSignal cancellationSignal,
+ WindowInsetsAnimationControlListener listener,
+ boolean fromIme, long durationMs, @Nullable Interpolator interpolator,
+ @AnimationType int animationType) {
+ if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0) {
+ listener.onCancelled(null);
+ return;
+ }
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InsetsController#controlWindowInsetsAnimation",
+ mHost.getInputMethodManager(), null /* icProto */);
+ }
+
+ controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
+ interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
+ false /* useInsetsAnimationThread */);
+ }
+
+ private void controlAnimationUnchecked(@InsetsType int types,
+ @Nullable CancellationSignal cancellationSignal,
+ WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
+ long durationMs, Interpolator interpolator,
+ @AnimationType int animationType,
+ @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
+ boolean useInsetsAnimationThread) {
+ if ((types & mTypesBeingCancelled) != 0) {
+ throw new IllegalStateException("Cannot start a new insets animation of "
+ + Type.toString(types)
+ + " while an existing " + Type.toString(mTypesBeingCancelled)
+ + " is being cancelled.");
+ }
+ if (animationType == ANIMATION_TYPE_USER) {
+ final @InsetsType int disabledTypes = types & mDisabledUserAnimationInsetsTypes;
+ if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes);
+ types &= ~mDisabledUserAnimationInsetsTypes;
+
+ if (fromIme && (disabledTypes & ime()) != 0
+ && !mState.getSource(ITYPE_IME).isVisible()) {
+ // We've requested IMM to show IME, but the IME is not controllable. We need to
+ // cancel the request.
+ getSourceConsumer(ITYPE_IME).hide(true, animationType);
+ }
+ }
+ if (types == 0) {
+ // nothing to animate.
+ listener.onCancelled(null);
+ updateRequestedVisibility();
+ if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
+ return;
+ }
+ cancelExistingControllers(types);
+ if (DEBUG) Log.d(TAG, "controlAnimation types: " + types);
+ mLastStartedAnimTypes |= types;
+
+ final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
+ final SparseArray<InsetsSourceControl> controls = new SparseArray<>();
+
+ Pair<Integer, Boolean> typesReadyPair = collectSourceControls(
+ fromIme, internalTypes, controls, animationType);
+ int typesReady = typesReadyPair.first;
+ boolean imeReady = typesReadyPair.second;
+ if (DEBUG) Log.d(TAG, String.format(
+ "controlAnimationUnchecked, typesReady: %s imeReady: %s", typesReady, imeReady));
+ if (!imeReady) {
+ // IME isn't ready, all requested types will be animated once IME is ready
+ abortPendingImeControlRequest();
+ final PendingControlRequest request = new PendingControlRequest(types,
+ listener, durationMs,
+ interpolator, animationType, layoutInsetsDuringAnimation, cancellationSignal,
+ useInsetsAnimationThread);
+ mPendingImeControlRequest = request;
+ mHandler.postDelayed(mPendingControlTimeout, PENDING_CONTROL_TIMEOUT_MS);
+ if (DEBUG) Log.d(TAG, "Ime not ready. Create pending request");
+ if (cancellationSignal != null) {
+ cancellationSignal.setOnCancelListener(() -> {
+ if (mPendingImeControlRequest == request) {
+ if (DEBUG) Log.d(TAG,
+ "Cancellation signal abortPendingImeControlRequest");
+ abortPendingImeControlRequest();
+ }
+ });
+ }
+ updateRequestedVisibility();
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ return;
+ }
+
+ if (typesReady == 0) {
+ if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
+ listener.onCancelled(null);
+ updateRequestedVisibility();
+ return;
+ }
+
+
+ final InsetsAnimationControlRunner runner = useInsetsAnimationThread
+ ? new InsetsAnimationThreadControlRunner(controls,
+ frame, mState, listener, typesReady, this, durationMs, interpolator,
+ animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
+ mHost.getHandler())
+ : new InsetsAnimationControlImpl(controls,
+ frame, mState, listener, typesReady, this, durationMs, interpolator,
+ animationType, layoutInsetsDuringAnimation, mHost.getTranslator());
+ if ((typesReady & WindowInsets.Type.ime()) != 0) {
+ ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl",
+ mHost.getInputMethodManager(), null /* icProto */);
+ }
+ mRunningAnimations.add(new RunningAnimation(runner, animationType));
+ if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ + useInsetsAnimationThread);
+ if (cancellationSignal != null) {
+ cancellationSignal.setOnCancelListener(() -> {
+ cancelAnimation(runner, true /* invokeCallback */);
+ });
+ } else {
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.pendingAnim", 0);
+ }
+ if (layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN) {
+ showDirectly(types, fromIme);
+ } else {
+ hideDirectly(types, false /* animationFinished */, animationType, fromIme);
+ }
+ updateRequestedVisibility();
+ }
+
+ /**
+ * @return Pair of (types ready to animate, IME ready to animate).
+ */
+ private Pair<Integer, Boolean> collectSourceControls(boolean fromIme,
+ ArraySet<Integer> internalTypes, SparseArray<InsetsSourceControl> controls,
+ @AnimationType int animationType) {
+ int typesReady = 0;
+ boolean imeReady = true;
+ for (int i = internalTypes.size() - 1; i >= 0; i--) {
+ final InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i));
+ boolean show = animationType == ANIMATION_TYPE_SHOW
+ || animationType == ANIMATION_TYPE_USER;
+ boolean canRun = false;
+ if (show) {
+ // Show request
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump(
+ "ImeInsetsSourceConsumer#requestShow", mHost.getInputMethodManager(),
+ null /* icProto */);
+ }
+ switch(consumer.requestShow(fromIme)) {
+ case ShowResult.SHOW_IMMEDIATELY:
+ canRun = true;
+ break;
+ case ShowResult.IME_SHOW_DELAYED:
+ imeReady = false;
+ if (DEBUG) Log.d(TAG, "requestShow IME_SHOW_DELAYED");
+ break;
+ case ShowResult.IME_SHOW_FAILED:
+ if (WARN) Log.w(TAG, "requestShow IME_SHOW_FAILED. fromIme: "
+ + fromIme);
+ // IME cannot be shown (since it didn't have focus), proceed
+ // with animation of other types.
+ break;
+ }
+ } else {
+ // Hide request
+ // TODO: Move notifyHidden() to beginning of the hide animation
+ // (when visibility actually changes using hideDirectly()).
+ if (!fromIme) {
+ consumer.notifyHidden();
+ }
+ canRun = true;
+ }
+ if (!canRun) {
+ if (WARN) Log.w(TAG, String.format(
+ "collectSourceControls can't continue show for type: %s fromIme: %b",
+ InsetsState.typeToString(consumer.getType()), fromIme));
+ continue;
+ }
+ final InsetsSourceControl control = consumer.getControl();
+ if (control != null && control.getLeash() != null) {
+ controls.put(consumer.getType(), new InsetsSourceControl(control));
+ typesReady |= toPublicType(consumer.getType());
+ } else if (animationType == ANIMATION_TYPE_SHOW) {
+ if (DEBUG) Log.d(TAG, "collectSourceControls no control for show(). fromIme: "
+ + fromIme);
+ // We don't have a control at the moment. However, we still want to update requested
+ // visibility state such that in case we get control, we can apply show animation.
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InsetsSourceConsumer#show", mHost.getInputMethodManager(),
+ null /* icProto */);
+ }
+ consumer.show(fromIme);
+ } else if (animationType == ANIMATION_TYPE_HIDE) {
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InsetsSourceConsumer#hide", mHost.getInputMethodManager(),
+ null /* icProto */);
+ }
+ consumer.hide();
+ }
+ }
+ return new Pair<>(typesReady, imeReady);
+ }
+
+ private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode(
+ @InsetsType int types) {
+
+ final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
+
+ // Generally, we want to layout the opposite of the current state. This is to make animation
+ // callbacks easy to use: The can capture the layout values and then treat that as end-state
+ // during the animation.
+ //
+ // However, if controlling multiple sources, we want to treat it as shown if any of the
+ // types is currently hidden.
+ for (int i = internalTypes.size() - 1; i >= 0; i--) {
+ InsetsSourceConsumer consumer = mSourceConsumers.get(internalTypes.valueAt(i));
+ if (consumer == null) {
+ continue;
+ }
+ if (!consumer.isRequestedVisible()) {
+ return LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
+ }
+ }
+ return LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
+ }
+
+ private void cancelExistingControllers(@InsetsType int types) {
+ final int originalmTypesBeingCancelled = mTypesBeingCancelled;
+ mTypesBeingCancelled |= types;
+ try {
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
+ if ((control.getTypes() & types) != 0) {
+ cancelAnimation(control, true /* invokeCallback */);
+ }
+ }
+ if ((types & ime()) != 0) {
+ abortPendingImeControlRequest();
+ }
+ } finally {
+ mTypesBeingCancelled = originalmTypesBeingCancelled;
+ }
+ }
+
+ private void abortPendingImeControlRequest() {
+ if (mPendingImeControlRequest != null) {
+ mPendingImeControlRequest.listener.onCancelled(null);
+ mPendingImeControlRequest = null;
+ mHandler.removeCallbacks(mPendingControlTimeout);
+ if (DEBUG) Log.d(TAG, "abortPendingImeControlRequest");
+ }
+ }
+
+ @VisibleForTesting
+ @Override
+ public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) {
+ cancelAnimation(runner, false /* invokeCallback */);
+ if (DEBUG) Log.d(TAG, "notifyFinished. shown: " + shown);
+ if (shown) {
+ showDirectly(runner.getTypes(), true /* fromIme */);
+ } else {
+ hideDirectly(runner.getTypes(), true /* animationFinished */,
+ runner.getAnimationType(), true /* fromIme */);
+ }
+ }
+
+ @Override
+ public void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params) {
+ mHost.applySurfaceParams(params);
+ }
+
+ void notifyControlRevoked(InsetsSourceConsumer consumer) {
+ final @InsetsType int types = toPublicType(consumer.getType());
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
+ control.notifyControlRevoked(types);
+ if (control.getControllingTypes() == 0) {
+ cancelAnimation(control, true /* invokeCallback */);
+ }
+ }
+ if (consumer.getType() == ITYPE_IME) {
+ abortPendingImeControlRequest();
+ }
+ }
+
+ private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
+ if (DEBUG) Log.d(TAG, String.format("cancelAnimation of types: %d, animType: %d",
+ control.getTypes(), control.getAnimationType()));
+ if (invokeCallback) {
+ control.cancel();
+ }
+ boolean stateChanged = false;
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ RunningAnimation runningAnimation = mRunningAnimations.get(i);
+ if (runningAnimation.runner == control) {
+ mRunningAnimations.remove(i);
+ ArraySet<Integer> types = toInternalType(control.getTypes());
+ for (int j = types.size() - 1; j >= 0; j--) {
+ if (types.valueAt(j) == ITYPE_IME) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InsetsSourceConsumer#notifyAnimationFinished",
+ mHost.getInputMethodManager(), null /* icProto */);
+ }
+ stateChanged |= getSourceConsumer(types.valueAt(j)).notifyAnimationFinished();
+ }
+ if (invokeCallback) {
+ dispatchAnimationEnd(runningAnimation.runner.getAnimation());
+ }
+ break;
+ }
+ }
+ if (stateChanged) {
+ mHost.notifyInsetsChanged();
+ }
+ }
+
+ private void applyLocalVisibilityOverride() {
+ for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
+ final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+ consumer.applyLocalVisibilityOverride();
+ }
+ }
+
+ @VisibleForTesting
+ public @NonNull InsetsSourceConsumer getSourceConsumer(@InternalInsetsType int type) {
+ InsetsSourceConsumer controller = mSourceConsumers.get(type);
+ if (controller != null) {
+ return controller;
+ }
+ controller = mConsumerCreator.apply(this, type);
+ mSourceConsumers.put(type, controller);
+ return controller;
+ }
+
+ @VisibleForTesting
+ public void notifyVisibilityChanged() {
+ mHost.notifyInsetsChanged();
+ }
+
+ /**
+ * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean)
+ */
+ public void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible,
+ boolean hasControl) {
+ mHost.updateCompatSysUiVisibility(type, visible, hasControl);
+ }
+
+ /**
+ * Called when current window gains focus.
+ */
+ public void onWindowFocusGained(boolean hasViewFocused) {
+ getSourceConsumer(ITYPE_IME).onWindowFocusGained(hasViewFocused);
+ }
+
+ /**
+ * Called when current window loses focus.
+ */
+ public void onWindowFocusLost() {
+ getSourceConsumer(ITYPE_IME).onWindowFocusLost();
+ }
+
+ @VisibleForTesting
+ public @AnimationType int getAnimationType(@InternalInsetsType int type) {
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
+ if (control.controlsInternalType(type)) {
+ return mRunningAnimations.get(i).type;
+ }
+ }
+ return ANIMATION_TYPE_NONE;
+ }
+
+ @VisibleForTesting
+ public void onRequestedVisibilityChanged(InsetsSourceConsumer consumer) {
+ mRequestedVisibilityChanged.add(consumer);
+ }
+
+ /**
+ * Sends the requested visibilities to window manager if any of them is changed.
+ */
+ private void updateRequestedVisibility() {
+ boolean changed = false;
+ for (int i = mRequestedVisibilityChanged.size() - 1; i >= 0; i--) {
+ final InsetsSourceConsumer consumer = mRequestedVisibilityChanged.valueAt(i);
+ final @InternalInsetsType int type = consumer.getType();
+ if (type == ITYPE_CAPTION_BAR) {
+ continue;
+ }
+ final boolean requestedVisible = consumer.isRequestedVisible();
+ if (requestedVisible != mRequestedState.getSourceOrDefaultVisibility(type)) {
+ mRequestedState.getSource(type).setVisible(requestedVisible);
+ changed = true;
+ }
+ }
+ mRequestedVisibilityChanged.clear();
+ if (!changed) {
+ return;
+ }
+ mHost.onInsetsModified(mRequestedState);
+ }
+
+ InsetsState getRequestedVisibility() {
+ return mRequestedState;
+ }
+
+ @VisibleForTesting
+ public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) {
+ // TODO(b/166736352): We should only skip the animation of specific types, not all types.
+ boolean skipAnim = false;
+ if ((types & ime()) != 0) {
+ final InsetsSourceConsumer consumer = mSourceConsumers.get(ITYPE_IME);
+ final InsetsSourceControl imeControl = consumer != null ? consumer.getControl() : null;
+ // Skip showing animation once that made by system for some reason.
+ // (e.g. starting window with IME snapshot)
+ if (imeControl != null) {
+ skipAnim = imeControl.getAndClearSkipAnimationOnce() && show
+ && consumer.hasViewFocusWhenWindowFocusGain();
+ }
+ }
+ applyAnimation(types, show, fromIme, skipAnim);
+ }
+
+ @VisibleForTesting
+ public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
+ boolean skipAnim) {
+ if (types == 0) {
+ // nothing to animate.
+ if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
+ return;
+ }
+
+ boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks();
+ final InternalAnimationControlListener listener = new InternalAnimationControlListener(
+ show, hasAnimationCallbacks, types, mHost.getSystemBarsBehavior(),
+ skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP));
+
+ // We are about to playing the default animation (show/hide). Passing a null frame indicates
+ // the controlled types should be animated regardless of the frame.
+ controlAnimationUnchecked(
+ types, null /* cancellationSignal */, listener, null /* frame */, fromIme,
+ listener.getDurationMs(), listener.getInsetsInterpolator(),
+ show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
+ show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
+ !hasAnimationCallbacks /* useInsetsAnimationThread */);
+ }
+
+ private void hideDirectly(
+ @InsetsType int types, boolean animationFinished, @AnimationType int animationType,
+ boolean fromIme) {
+ if ((types & ime()) != 0) {
+ ImeTracing.getInstance().triggerClientDump("InsetsController#hideDirectly",
+ mHost.getInputMethodManager(), null /* icProto */);
+ }
+ final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
+ for (int i = internalTypes.size() - 1; i >= 0; i--) {
+ getSourceConsumer(internalTypes.valueAt(i)).hide(animationFinished, animationType);
+ }
+ updateRequestedVisibility();
+
+ if (fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0);
+ }
+ }
+
+ private void showDirectly(@InsetsType int types, boolean fromIme) {
+ if ((types & ime()) != 0) {
+ ImeTracing.getInstance().triggerClientDump("InsetsController#showDirectly",
+ mHost.getInputMethodManager(), null /* icProto */);
+ }
+ final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
+ for (int i = internalTypes.size() - 1; i >= 0; i--) {
+ getSourceConsumer(internalTypes.valueAt(i)).show(false /* fromIme */);
+ }
+ updateRequestedVisibility();
+
+ if (fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0);
+ }
+ }
+
+ /**
+ * Cancel on-going animation to show/hide {@link InsetsType}.
+ */
+ @VisibleForTesting
+ public void cancelExistingAnimations() {
+ cancelExistingControllers(all());
+ }
+
+ void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.println("InsetsController:");
+ mState.dump(prefix + " ", pw);
+ }
+
+ void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ mState.dumpDebug(proto, STATE);
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner;
+ runner.dumpDebug(proto, CONTROL);
+ }
+ proto.end(token);
+ }
+
+ @VisibleForTesting
+ @Override
+ public void startAnimation(InsetsAnimationControlImpl controller,
+ WindowInsetsAnimationControlListener listener, int types,
+ WindowInsetsAnimation animation, Bounds bounds) {
+ mHost.dispatchWindowInsetsAnimationPrepare(animation);
+ mHost.addOnPreDrawRunnable(() -> {
+ if (controller.isCancelled()) {
+ if (WARN) Log.w(TAG, "startAnimation canceled before preDraw");
+ return;
+ }
+ Trace.asyncTraceBegin(TRACE_TAG_VIEW,
+ "InsetsAnimation: " + WindowInsets.Type.toString(types), types);
+ for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
+ RunningAnimation runningAnimation = mRunningAnimations.get(i);
+ if (runningAnimation.runner == controller) {
+ runningAnimation.startDispatched = true;
+ }
+ }
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.pendingAnim", 0);
+ mHost.dispatchWindowInsetsAnimationStart(animation, bounds);
+ mStartingAnimation = true;
+ controller.mReadyDispatched = true;
+ listener.onReady(controller, types);
+ mStartingAnimation = false;
+ });
+ }
+
+ @VisibleForTesting
+ public void dispatchAnimationEnd(WindowInsetsAnimation animation) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW,
+ "InsetsAnimation: " + WindowInsets.Type.toString(animation.getTypeMask()),
+ animation.getTypeMask());
+ mHost.dispatchWindowInsetsAnimationEnd(animation);
+ }
+
+ @VisibleForTesting
+ @Override
+ public void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner) {
+ if (mStartingAnimation || runner.getAnimationType() == ANIMATION_TYPE_USER) {
+ mAnimCallback.run();
+ mAnimCallbackScheduled = false;
+ return;
+ }
+ if (!mAnimCallbackScheduled) {
+ mHost.postInsetsAnimationCallback(mAnimCallback);
+ mAnimCallbackScheduled = true;
+ }
+ }
+
+ @Override
+ public void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask) {
+ mHost.setSystemBarsAppearance(appearance, mask);
+ }
+
+ @Override
+ public @Appearance int getSystemBarsAppearance() {
+ if (!mHost.isSystemBarsAppearanceControlled()) {
+ // We only return the requested appearance, not the implied one.
+ return 0;
+ }
+ return mHost.getSystemBarsAppearance();
+ }
+
+ @Override
+ public void setCaptionInsetsHeight(int height) {
+ if (mCaptionInsetsHeight != height) {
+ mCaptionInsetsHeight = height;
+ if (mCaptionInsetsHeight != 0) {
+ mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top,
+ mFrame.right, mFrame.top + mCaptionInsetsHeight));
+ } else {
+ mState.removeSource(ITYPE_CAPTION_BAR);
+ }
+ mHost.notifyInsetsChanged();
+ }
+ }
+
+ @Override
+ public void setSystemBarsBehavior(@Behavior int behavior) {
+ mHost.setSystemBarsBehavior(behavior);
+ }
+
+ @Override
+ public @Behavior int getSystemBarsBehavior() {
+ if (!mHost.isSystemBarsBehaviorControlled()) {
+ // We only return the requested behavior, not the implied one.
+ return 0;
+ }
+ return mHost.getSystemBarsBehavior();
+ }
+
+ @Override
+ public void setAnimationsDisabled(boolean disable) {
+ mAnimationsDisabled = disable;
+ }
+
+ private @InsetsType int calculateControllableTypes() {
+ @InsetsType int result = 0;
+ for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
+ InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+ InsetsSource source = mState.peekSource(consumer.mType);
+ if (consumer.getControl() != null && source != null && source.isUserControllable()) {
+ result |= toPublicType(consumer.mType);
+ }
+ }
+ return result & ~mState.calculateUncontrollableInsetsFromFrame(mFrame);
+ }
+
+ /**
+ * @return The types that are now animating due to a listener invoking control/show/hide
+ */
+ private @InsetsType int invokeControllableInsetsChangedListeners() {
+ mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
+ mLastStartedAnimTypes = 0;
+ @InsetsType int types = calculateControllableTypes();
+ int size = mControllableInsetsChangedListeners.size();
+ for (int i = 0; i < size; i++) {
+ mControllableInsetsChangedListeners.get(i).onControllableInsetsChanged(this, types);
+ }
+ return mLastStartedAnimTypes;
+ }
+
+ @Override
+ public void addOnControllableInsetsChangedListener(
+ OnControllableInsetsChangedListener listener) {
+ Objects.requireNonNull(listener);
+ mControllableInsetsChangedListeners.add(listener);
+ listener.onControllableInsetsChanged(this, calculateControllableTypes());
+ }
+
+ @Override
+ public void removeOnControllableInsetsChangedListener(
+ OnControllableInsetsChangedListener listener) {
+ Objects.requireNonNull(listener);
+ mControllableInsetsChangedListeners.remove(listener);
+ }
+
+ @Override
+ public void releaseSurfaceControlFromRt(SurfaceControl sc) {
+ mHost.releaseSurfaceControlFromRt(sc);
+ }
+
+ @Override
+ public void reportPerceptible(int types, boolean perceptible) {
+ final ArraySet<Integer> internalTypes = toInternalType(types);
+ final int size = mSourceConsumers.size();
+ for (int i = 0; i < size; i++) {
+ final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+ if (internalTypes.contains(consumer.getType())) {
+ consumer.onPerceptible(perceptible);
+ }
+ }
+ }
+
+ Host getHost() {
+ return mHost;
+ }
+}
diff --git a/android/view/InsetsFlags.java b/android/view/InsetsFlags.java
new file mode 100644
index 0000000..3355252
--- /dev/null
+++ b/android/view/InsetsFlags.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS;
+import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
+import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+
+import android.view.WindowInsetsController.Appearance;
+import android.view.WindowInsetsController.Behavior;
+
+/**
+ * Contains the information about {@link Appearance} and {@link Behavior} of system windows which
+ * can produce insets. This is for carrying the request from a client to the system server.
+ * @hide
+ */
+public class InsetsFlags {
+
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_OPAQUE_STATUS_BARS,
+ equals = APPEARANCE_OPAQUE_STATUS_BARS,
+ name = "OPAQUE_STATUS_BARS"),
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_OPAQUE_NAVIGATION_BARS,
+ equals = APPEARANCE_OPAQUE_NAVIGATION_BARS,
+ name = "OPAQUE_NAVIGATION_BARS"),
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_LOW_PROFILE_BARS,
+ equals = APPEARANCE_LOW_PROFILE_BARS,
+ name = "LOW_PROFILE_BARS"),
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_LIGHT_STATUS_BARS,
+ equals = APPEARANCE_LIGHT_STATUS_BARS,
+ name = "LIGHT_STATUS_BARS"),
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_LIGHT_NAVIGATION_BARS,
+ equals = APPEARANCE_LIGHT_NAVIGATION_BARS,
+ name = "LIGHT_NAVIGATION_BARS"),
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
+ equals = APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
+ name = "SEMI_TRANSPARENT_STATUS_BARS"),
+ @ViewDebug.FlagToString(
+ mask = APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS,
+ equals = APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS,
+ name = "SEMI_TRANSPARENT_NAVIGATION_BARS")
+ })
+ public @Appearance int appearance;
+
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(
+ mask = BEHAVIOR_DEFAULT,
+ equals = BEHAVIOR_DEFAULT,
+ name = "DEFAULT"),
+ @ViewDebug.FlagToString(
+ mask = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+ equals = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+ name = "SHOW_TRANSIENT_BARS_BY_SWIPE")
+ })
+ public @Behavior int behavior = BEHAVIOR_DEFAULT;
+}
diff --git a/android/view/InsetsSource.java b/android/view/InsetsSource.java
new file mode 100644
index 0000000..e6cf683
--- /dev/null
+++ b/android/view/InsetsSource.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsSourceProto.FRAME;
+import static android.view.InsetsSourceProto.TYPE;
+import static android.view.InsetsSourceProto.VISIBLE;
+import static android.view.InsetsSourceProto.VISIBLE_FRAME;
+import static android.view.InsetsState.ITYPE_CAPTION_BAR;
+import static android.view.InsetsState.ITYPE_IME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsState.InternalInsetsType;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * Represents the state of a single window generating insets for clients.
+ * @hide
+ */
+public class InsetsSource implements Parcelable {
+
+ private final @InternalInsetsType int mType;
+
+ /** Frame of the source in screen coordinate space */
+ private final Rect mFrame;
+ private @Nullable Rect mVisibleFrame;
+ private boolean mVisible;
+
+ private final Rect mTmpFrame = new Rect();
+
+ public InsetsSource(@InternalInsetsType int type) {
+ mType = type;
+ mFrame = new Rect();
+ mVisible = InsetsState.getDefaultVisibility(type);
+ }
+
+ public InsetsSource(InsetsSource other) {
+ mType = other.mType;
+ mFrame = new Rect(other.mFrame);
+ mVisible = other.mVisible;
+ mVisibleFrame = other.mVisibleFrame != null
+ ? new Rect(other.mVisibleFrame)
+ : null;
+ }
+
+ public void set(InsetsSource other) {
+ mFrame.set(other.mFrame);
+ mVisible = other.mVisible;
+ mVisibleFrame = other.mVisibleFrame != null
+ ? new Rect(other.mVisibleFrame)
+ : null;
+ }
+
+ public void setFrame(int left, int top, int right, int bottom) {
+ mFrame.set(left, top, right, bottom);
+ }
+
+ public void setFrame(Rect frame) {
+ mFrame.set(frame);
+ }
+
+ public void setVisibleFrame(@Nullable Rect visibleFrame) {
+ mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : visibleFrame;
+ }
+
+ public void setVisible(boolean visible) {
+ mVisible = visible;
+ }
+
+ public @InternalInsetsType int getType() {
+ return mType;
+ }
+
+ public Rect getFrame() {
+ return mFrame;
+ }
+
+ public @Nullable Rect getVisibleFrame() {
+ return mVisibleFrame;
+ }
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ boolean isUserControllable() {
+ // If mVisibleFrame is null, it will be the same area as mFrame.
+ return mVisibleFrame == null || !mVisibleFrame.isEmpty();
+ }
+
+ /**
+ * Calculates the insets this source will cause to a client window.
+ *
+ * @param relativeFrame The frame to calculate the insets relative to.
+ * @param ignoreVisibility If true, always reports back insets even if source isn't visible.
+ * @return The resulting insets. The contract is that only one side will be occupied by a
+ * source.
+ */
+ public Insets calculateInsets(Rect relativeFrame, boolean ignoreVisibility) {
+ return calculateInsets(relativeFrame, mFrame, ignoreVisibility);
+ }
+
+ /**
+ * Like {@link #calculateInsets(Rect, boolean)}, but will return visible insets.
+ */
+ public Insets calculateVisibleInsets(Rect relativeFrame) {
+ return calculateInsets(relativeFrame, mVisibleFrame != null ? mVisibleFrame : mFrame,
+ false /* ignoreVisibility */);
+ }
+
+ private Insets calculateInsets(Rect relativeFrame, Rect frame, boolean ignoreVisibility) {
+ if (!ignoreVisibility && !mVisible) {
+ return Insets.NONE;
+ }
+ // During drag-move and drag-resizing, the caption insets position may not get updated
+ // before the app frame get updated. To layout the app content correctly during drag events,
+ // we always return the insets with the corresponding height covering the top.
+ if (getType() == ITYPE_CAPTION_BAR) {
+ return Insets.of(0, frame.height(), 0, 0);
+ }
+ // Checks for whether there is shared edge with insets for 0-width/height window.
+ final boolean hasIntersection = relativeFrame.isEmpty()
+ ? getIntersection(frame, relativeFrame, mTmpFrame)
+ : mTmpFrame.setIntersect(frame, relativeFrame);
+ if (!hasIntersection) {
+ return Insets.NONE;
+ }
+
+ // TODO: Currently, non-floating IME always intersects at bottom due to issues with cutout.
+ // However, we should let the policy decide from the server.
+ if (getType() == ITYPE_IME) {
+ return Insets.of(0, 0, 0, mTmpFrame.height());
+ }
+
+ // Intersecting at top/bottom
+ if (mTmpFrame.width() == relativeFrame.width()) {
+ if (mTmpFrame.top == relativeFrame.top) {
+ return Insets.of(0, mTmpFrame.height(), 0, 0);
+ } else if (mTmpFrame.bottom == relativeFrame.bottom) {
+ return Insets.of(0, 0, 0, mTmpFrame.height());
+ }
+ // TODO: remove when insets are shell-customizable.
+ // This is a hack that says "if this is a top-inset (eg statusbar), always apply it
+ // to the top". It is used when adjusting primary split for IME.
+ if (mTmpFrame.top == 0) {
+ return Insets.of(0, mTmpFrame.height(), 0, 0);
+ }
+ }
+ // Intersecting at left/right
+ else if (mTmpFrame.height() == relativeFrame.height()) {
+ if (mTmpFrame.left == relativeFrame.left) {
+ return Insets.of(mTmpFrame.width(), 0, 0, 0);
+ } else if (mTmpFrame.right == relativeFrame.right) {
+ return Insets.of(0, 0, mTmpFrame.width(), 0);
+ }
+ }
+ return Insets.NONE;
+ }
+
+ /**
+ * Outputs the intersection of two rectangles. The shared edges will also be counted in the
+ * intersection.
+ *
+ * @param a The first rectangle being intersected with.
+ * @param b The second rectangle being intersected with.
+ * @param out The rectangle which represents the intersection.
+ * @return {@code true} if there is any intersection.
+ */
+ private static boolean getIntersection(@NonNull Rect a, @NonNull Rect b, @NonNull Rect out) {
+ if (a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom) {
+ out.left = Math.max(a.left, b.left);
+ out.top = Math.max(a.top, b.top);
+ out.right = Math.min(a.right, b.right);
+ out.bottom = Math.min(a.bottom, b.bottom);
+ return true;
+ }
+ out.setEmpty();
+ return false;
+ }
+
+ /**
+ * Export the state of {@link InsetsSource} into a protocol buffer output stream.
+ *
+ * @param proto Stream to write the state to
+ * @param fieldId FieldId of InsetsSource as defined in the parent message
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(TYPE, InsetsState.typeToString(mType));
+ mFrame.dumpDebug(proto, FRAME);
+ if (mVisibleFrame != null) {
+ mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME);
+ }
+ proto.write(VISIBLE, mVisible);
+ proto.end(token);
+ }
+
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix);
+ pw.print("InsetsSource type="); pw.print(InsetsState.typeToString(mType));
+ pw.print(" frame="); pw.print(mFrame.toShortString());
+ if (mVisibleFrame != null) {
+ pw.print(" visibleFrame="); pw.print(mVisibleFrame.toShortString());
+ }
+ pw.print(" visible="); pw.print(mVisible);
+ pw.println();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return equals(o, false);
+ }
+
+ /**
+ * @param excludeInvisibleImeFrames If {@link InsetsState#ITYPE_IME} frames should be ignored
+ * when IME is not visible.
+ */
+ public boolean equals(@Nullable Object o, boolean excludeInvisibleImeFrames) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ InsetsSource that = (InsetsSource) o;
+
+ if (mType != that.mType) return false;
+ if (mVisible != that.mVisible) return false;
+ if (excludeInvisibleImeFrames && !mVisible && mType == ITYPE_IME) return true;
+ if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
+ return mFrame.equals(that.mFrame);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mType;
+ result = 31 * result + mFrame.hashCode();
+ result = 31 * result + (mVisibleFrame != null ? mVisibleFrame.hashCode() : 0);
+ result = 31 * result + (mVisible ? 1 : 0);
+ return result;
+ }
+
+ public InsetsSource(Parcel in) {
+ mType = in.readInt();
+ mFrame = Rect.CREATOR.createFromParcel(in);
+ if (in.readInt() != 0) {
+ mVisibleFrame = Rect.CREATOR.createFromParcel(in);
+ } else {
+ mVisibleFrame = null;
+ }
+ mVisible = in.readBoolean();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ mFrame.writeToParcel(dest, 0);
+ if (mVisibleFrame != null) {
+ dest.writeInt(1);
+ mVisibleFrame.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeBoolean(mVisible);
+ }
+
+ @Override
+ public String toString() {
+ return "InsetsSource: {"
+ + "mType=" + InsetsState.typeToString(mType)
+ + ", mFrame=" + mFrame.toShortString()
+ + ", mVisible=" + mVisible
+ + "}";
+ }
+
+ public static final @android.annotation.NonNull Creator<InsetsSource> CREATOR = new Creator<InsetsSource>() {
+
+ public InsetsSource createFromParcel(Parcel in) {
+ return new InsetsSource(in);
+ }
+
+ public InsetsSource[] newArray(int size) {
+ return new InsetsSource[size];
+ }
+ };
+}
diff --git a/android/view/InsetsSourceConsumer.java b/android/view/InsetsSourceConsumer.java
new file mode 100644
index 0000000..ee33541
--- /dev/null
+++ b/android/view/InsetsSourceConsumer.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import static android.view.InsetsController.ANIMATION_TYPE_NONE;
+import static android.view.InsetsController.AnimationType;
+import static android.view.InsetsController.DEBUG;
+import static android.view.InsetsSourceConsumerProto.HAS_WINDOW_FOCUS;
+import static android.view.InsetsSourceConsumerProto.INTERNAL_INSETS_TYPE;
+import static android.view.InsetsSourceConsumerProto.IS_REQUESTED_VISIBLE;
+import static android.view.InsetsSourceConsumerProto.PENDING_FRAME;
+import static android.view.InsetsSourceConsumerProto.PENDING_VISIBLE_FRAME;
+import static android.view.InsetsSourceConsumerProto.SOURCE_CONTROL;
+import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsState.getDefaultVisibility;
+import static android.view.InsetsState.toPublicType;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.util.Log;
+import android.util.imetracing.ImeTracing;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.SurfaceControl.Transaction;
+import android.view.WindowInsets.Type.InsetsType;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Controls the visibility and animations of a single window insets source.
+ * @hide
+ */
+public class InsetsSourceConsumer {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {ShowResult.SHOW_IMMEDIATELY, ShowResult.IME_SHOW_DELAYED, ShowResult.IME_SHOW_FAILED})
+ @interface ShowResult {
+ /**
+ * Window type is ready to be shown, will be shown immidiately.
+ */
+ int SHOW_IMMEDIATELY = 0;
+ /**
+ * Result will be delayed. Window needs to be prepared or request is not from controller.
+ * Request will be delegated to controller and may or may not be shown.
+ */
+ int IME_SHOW_DELAYED = 1;
+ /**
+ * Window will not be shown because one of the conditions couldn't be met.
+ * (e.g. in IME's case, when no editor is focused.)
+ */
+ int IME_SHOW_FAILED = 2;
+ }
+
+ protected final InsetsController mController;
+ protected boolean mRequestedVisible;
+ protected final InsetsState mState;
+ protected final @InternalInsetsType int mType;
+
+ private static final String TAG = "InsetsSourceConsumer";
+ private final Supplier<Transaction> mTransactionSupplier;
+ private @Nullable InsetsSourceControl mSourceControl;
+ private boolean mHasWindowFocus;
+
+ /**
+ * Whether the view has focus returned by {@link #onWindowFocusGained(boolean)}.
+ */
+ private boolean mHasViewFocusWhenWindowFocusGain;
+ private Rect mPendingFrame;
+ private Rect mPendingVisibleFrame;
+
+ /**
+ * Indicates if we have the pending animation. When we have the control, we need to play the
+ * animation if the requested visibility is different from the current state. But if we haven't
+ * had a leash yet, we will set this flag, and play the animation once we get the leash.
+ */
+ private boolean mIsAnimationPending;
+
+ public InsetsSourceConsumer(@InternalInsetsType int type, InsetsState state,
+ Supplier<Transaction> transactionSupplier, InsetsController controller) {
+ mType = type;
+ mState = state;
+ mTransactionSupplier = transactionSupplier;
+ mController = controller;
+ mRequestedVisible = getDefaultVisibility(type);
+ }
+
+ /**
+ * Updates the control delivered from the server.
+
+ * @param showTypes An integer array with a single entry that determines which types a show
+ * animation should be run after setting the control.
+ * @param hideTypes An integer array with a single entry that determines which types a hide
+ * animation should be run after setting the control.
+ */
+ public void setControl(@Nullable InsetsSourceControl control,
+ @InsetsType int[] showTypes, @InsetsType int[] hideTypes) {
+ if (mType == ITYPE_IME) {
+ ImeTracing.getInstance().triggerClientDump("InsetsSourceConsumer#setControl",
+ mController.getHost().getInputMethodManager(), null /* icProto */);
+ }
+ if (Objects.equals(mSourceControl, control)) {
+ if (mSourceControl != null && mSourceControl != control) {
+ mSourceControl.release(SurfaceControl::release);
+ mSourceControl = control;
+ }
+ return;
+ }
+ SurfaceControl oldLeash = mSourceControl != null ? mSourceControl.getLeash() : null;
+
+ final InsetsSourceControl lastControl = mSourceControl;
+ mSourceControl = control;
+ if (control != null) {
+ if (DEBUG) Log.d(TAG, String.format("setControl -> %s on %s",
+ InsetsState.typeToString(control.getType()),
+ mController.getHost().getRootViewTitle()));
+ }
+ if (mSourceControl == null) {
+ // We are loosing control
+ mController.notifyControlRevoked(this);
+
+ // Check if we need to restore server visibility.
+ final InsetsSource source = mState.getSource(mType);
+ final boolean serverVisibility =
+ mController.getLastDispatchedState().getSourceOrDefaultVisibility(mType);
+ if (source.isVisible() != serverVisibility) {
+ source.setVisible(serverVisibility);
+ mController.notifyVisibilityChanged();
+ }
+
+ // For updateCompatSysUiVisibility
+ applyLocalVisibilityOverride();
+ } else {
+ // We are gaining control, and need to run an animation since previous state
+ // didn't match
+ final boolean requestedVisible = isRequestedVisibleAwaitingControl();
+ final boolean needAnimation = requestedVisible != mState.getSource(mType).isVisible();
+ if (control.getLeash() != null && (needAnimation || mIsAnimationPending)) {
+ if (DEBUG) Log.d(TAG, String.format("Gaining control in %s, requestedVisible: %b",
+ mController.getHost().getRootViewTitle(), requestedVisible));
+ if (requestedVisible) {
+ showTypes[0] |= toPublicType(getType());
+ } else {
+ hideTypes[0] |= toPublicType(getType());
+ }
+ mIsAnimationPending = false;
+ } else {
+ if (needAnimation) {
+ // We need animation but we haven't had a leash yet. Set this flag that when we
+ // get the leash we can play the deferred animation.
+ mIsAnimationPending = true;
+ }
+ // We are gaining control, but don't need to run an animation.
+ // However make sure that the leash visibility is still up to date.
+ if (applyLocalVisibilityOverride()) {
+ mController.notifyVisibilityChanged();
+ }
+
+ // If we have a new leash, make sure visibility is up-to-date, even though we
+ // didn't want to run an animation above.
+ SurfaceControl newLeash = mSourceControl.getLeash();
+ if (oldLeash == null || newLeash == null || !oldLeash.isSameSurface(newLeash)) {
+ applyHiddenToControl();
+ }
+
+ // Remove the surface that owned by last control when it lost.
+ if (!requestedVisible && !mIsAnimationPending && lastControl == null) {
+ removeSurface();
+ }
+ }
+ }
+ if (lastControl != null) {
+ lastControl.release(SurfaceControl::release);
+ }
+ }
+
+ @VisibleForTesting
+ public InsetsSourceControl getControl() {
+ return mSourceControl;
+ }
+
+ /**
+ * Determines if the consumer will be shown after control is available.
+ * Note: for system bars this method is same as {@link #isRequestedVisible()}.
+ *
+ * @return {@code true} if consumer has a pending show.
+ */
+ protected boolean isRequestedVisibleAwaitingControl() {
+ return isRequestedVisible();
+ }
+
+ int getType() {
+ return mType;
+ }
+
+ @VisibleForTesting
+ public void show(boolean fromIme) {
+ if (DEBUG) Log.d(TAG, String.format("Call show() for type: %s fromIme: %b ",
+ InsetsState.typeToString(mType), fromIme));
+ setRequestedVisible(true);
+ }
+
+ @VisibleForTesting
+ public void hide() {
+ if (DEBUG) Log.d(TAG, String.format("Call hide for %s on %s",
+ InsetsState.typeToString(mType), mController.getHost().getRootViewTitle()));
+ setRequestedVisible(false);
+ }
+
+ void hide(boolean animationFinished, @AnimationType int animationType) {
+ hide();
+ }
+
+ /**
+ * Called when current window gains focus
+ */
+ public void onWindowFocusGained(boolean hasViewFocus) {
+ mHasWindowFocus = true;
+ mHasViewFocusWhenWindowFocusGain = hasViewFocus;
+ }
+
+ /**
+ * Called when current window loses focus.
+ */
+ public void onWindowFocusLost() {
+ mHasWindowFocus = false;
+ }
+
+ boolean hasViewFocusWhenWindowFocusGain() {
+ return mHasViewFocusWhenWindowFocusGain;
+ }
+
+ boolean applyLocalVisibilityOverride() {
+ final InsetsSource source = mState.peekSource(mType);
+ final boolean isVisible = source != null ? source.isVisible() : getDefaultVisibility(mType);
+ final boolean hasControl = mSourceControl != null;
+
+ if (mType == ITYPE_IME) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InsetsSourceConsumer#applyLocalVisibilityOverride",
+ mController.getHost().getInputMethodManager(), null /* icProto */);
+ }
+
+ // We still need to let the legacy app know the visibility change even if we don't have the
+ // control. If we don't have the source, we don't change the requested visibility for making
+ // the callback behavior compatible.
+ mController.updateCompatSysUiVisibility(
+ mType, (hasControl || source == null) ? mRequestedVisible : isVisible, hasControl);
+
+ // If we don't have control, we are not able to change the visibility.
+ if (!hasControl) {
+ if (DEBUG) Log.d(TAG, "applyLocalVisibilityOverride: No control in "
+ + mController.getHost().getRootViewTitle()
+ + " requestedVisible " + mRequestedVisible);
+ return false;
+ }
+ if (isVisible == mRequestedVisible) {
+ return false;
+ }
+ if (DEBUG) Log.d(TAG, String.format("applyLocalVisibilityOverride: %s requestedVisible: %b",
+ mController.getHost().getRootViewTitle(), mRequestedVisible));
+ mState.getSource(mType).setVisible(mRequestedVisible);
+ return true;
+ }
+
+ @VisibleForTesting
+ public boolean isRequestedVisible() {
+ return mRequestedVisible;
+ }
+
+ /**
+ * Request to show current window type.
+ *
+ * @param fromController {@code true} if request is coming from controller.
+ * (e.g. in IME case, controller is
+ * {@link android.inputmethodservice.InputMethodService}).
+ * @return @see {@link ShowResult}.
+ */
+ @VisibleForTesting
+ public @ShowResult int requestShow(boolean fromController) {
+ return ShowResult.SHOW_IMMEDIATELY;
+ }
+
+ /**
+ * Reports that this source's perceptibility has changed
+ *
+ * @param perceptible true if the source is perceptible, false otherwise.
+ * @see InsetsAnimationControlCallbacks#reportPerceptible
+ */
+ public void onPerceptible(boolean perceptible) {
+ }
+
+ /**
+ * Notify listeners that window is now hidden.
+ */
+ void notifyHidden() {
+ // no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
+ }
+
+ /**
+ * Remove surface on which this consumer type is drawn.
+ */
+ public void removeSurface() {
+ // no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public void updateSource(InsetsSource newSource, @AnimationType int animationType) {
+ InsetsSource source = mState.peekSource(mType);
+ if (source == null || animationType == ANIMATION_TYPE_NONE
+ || source.getFrame().equals(newSource.getFrame())) {
+ mPendingFrame = null;
+ mPendingVisibleFrame = null;
+ mState.addSource(newSource);
+ return;
+ }
+
+ // Frame is changing while animating. Keep note of the new frame but keep existing frame
+ // until animation is finished.
+ newSource = new InsetsSource(newSource);
+ mPendingFrame = new Rect(newSource.getFrame());
+ mPendingVisibleFrame = newSource.getVisibleFrame() != null
+ ? new Rect(newSource.getVisibleFrame())
+ : null;
+ newSource.setFrame(source.getFrame());
+ newSource.setVisibleFrame(source.getVisibleFrame());
+ mState.addSource(newSource);
+ if (DEBUG) Log.d(TAG, "updateSource: " + newSource);
+ }
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public boolean notifyAnimationFinished() {
+ if (mPendingFrame != null) {
+ InsetsSource source = mState.getSource(mType);
+ source.setFrame(mPendingFrame);
+ source.setVisibleFrame(mPendingVisibleFrame);
+ mPendingFrame = null;
+ mPendingVisibleFrame = null;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets requested visibility from the client, regardless of whether we are able to control it at
+ * the moment.
+ */
+ protected void setRequestedVisible(boolean requestedVisible) {
+ if (mRequestedVisible != requestedVisible) {
+ mRequestedVisible = requestedVisible;
+
+ // We need an animation later if the leash of a real control (which has an insets hint)
+ // is not ready. The !mIsAnimationPending check is in case that the requested visibility
+ // is changed twice before playing the animation -- we don't need an animation in this
+ // case.
+ mIsAnimationPending = !mIsAnimationPending
+ && mSourceControl != null
+ && mSourceControl.getLeash() == null
+ && !Insets.NONE.equals(mSourceControl.getInsetsHint());
+
+ mController.onRequestedVisibilityChanged(this);
+ if (DEBUG) Log.d(TAG, "setRequestedVisible: " + requestedVisible);
+ }
+ if (applyLocalVisibilityOverride()) {
+ mController.notifyVisibilityChanged();
+ }
+ }
+
+ private void applyHiddenToControl() {
+ if (mSourceControl == null || mSourceControl.getLeash() == null) {
+ return;
+ }
+
+ final Transaction t = mTransactionSupplier.get();
+ if (DEBUG) Log.d(TAG, "applyHiddenToControl: " + mRequestedVisible);
+ if (mRequestedVisible) {
+ t.show(mSourceControl.getLeash());
+ } else {
+ t.hide(mSourceControl.getLeash());
+ }
+ t.apply();
+ onPerceptible(mRequestedVisible);
+ }
+
+ void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(INTERNAL_INSETS_TYPE, InsetsState.typeToString(mType));
+ proto.write(HAS_WINDOW_FOCUS, mHasWindowFocus);
+ proto.write(IS_REQUESTED_VISIBLE, mRequestedVisible);
+ if (mSourceControl != null) {
+ mSourceControl.dumpDebug(proto, SOURCE_CONTROL);
+ }
+ if (mPendingFrame != null) {
+ mPendingFrame.dumpDebug(proto, PENDING_FRAME);
+ }
+ if (mPendingVisibleFrame != null) {
+ mPendingVisibleFrame.dumpDebug(proto, PENDING_VISIBLE_FRAME);
+ }
+ proto.end(token);
+ }
+}
diff --git a/android/view/InsetsSourceControl.java b/android/view/InsetsSourceControl.java
new file mode 100644
index 0000000..1506ee4
--- /dev/null
+++ b/android/view/InsetsSourceControl.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.graphics.PointProto.X;
+import static android.graphics.PointProto.Y;
+import static android.view.InsetsSourceControlProto.LEASH;
+import static android.view.InsetsSourceControlProto.POSITION;
+import static android.view.InsetsSourceControlProto.TYPE;
+
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Point;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
+import android.view.InsetsState.InternalInsetsType;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Represents a parcelable object to allow controlling a single {@link InsetsSource}.
+ * @hide
+ */
+public class InsetsSourceControl implements Parcelable {
+
+ private final @InternalInsetsType int mType;
+ private final @Nullable SurfaceControl mLeash;
+ private final Point mSurfacePosition;
+
+ // This is used while playing an insets animation regardless of the relative frame. This would
+ // be the insets received by the bounds of its source window.
+ private Insets mInsetsHint;
+
+ private boolean mSkipAnimationOnce;
+ private int mParcelableFlags;
+
+ public InsetsSourceControl(@InternalInsetsType int type, @Nullable SurfaceControl leash,
+ Point surfacePosition, Insets insetsHint) {
+ mType = type;
+ mLeash = leash;
+ mSurfacePosition = surfacePosition;
+ mInsetsHint = insetsHint;
+ }
+
+ public InsetsSourceControl(InsetsSourceControl other) {
+ mType = other.mType;
+ if (other.mLeash != null) {
+ mLeash = new SurfaceControl(other.mLeash, "InsetsSourceControl");
+ } else {
+ mLeash = null;
+ }
+ mSurfacePosition = new Point(other.mSurfacePosition);
+ mInsetsHint = other.mInsetsHint;
+ mSkipAnimationOnce = other.getAndClearSkipAnimationOnce();
+ }
+
+ public InsetsSourceControl(Parcel in) {
+ mType = in.readInt();
+ mLeash = in.readTypedObject(SurfaceControl.CREATOR);
+ mSurfacePosition = in.readTypedObject(Point.CREATOR);
+ mInsetsHint = in.readTypedObject(Insets.CREATOR);
+ mSkipAnimationOnce = in.readBoolean();
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Gets the leash for controlling insets source. If the system is controlling the insets source,
+ * for example, transient bars, the client will receive fake controls without leash in it.
+ *
+ * @return the leash.
+ */
+ public @Nullable SurfaceControl getLeash() {
+ return mLeash;
+ }
+
+ public boolean setSurfacePosition(int left, int top) {
+ if (mSurfacePosition.equals(left, top)) {
+ return false;
+ }
+ mSurfacePosition.set(left, top);
+ return true;
+ }
+
+ public Point getSurfacePosition() {
+ return mSurfacePosition;
+ }
+
+ public void setInsetsHint(Insets insets) {
+ mInsetsHint = insets;
+ }
+
+ public void setInsetsHint(int left, int top, int right, int bottom) {
+ mInsetsHint = Insets.of(left, top, right, bottom);
+ }
+
+ public Insets getInsetsHint() {
+ return mInsetsHint;
+ }
+
+ public void setSkipAnimationOnce(boolean skipAnimation) {
+ mSkipAnimationOnce = skipAnimation;
+ }
+
+ /**
+ * Get the state whether the current control needs to skip animation or not.
+ *
+ * Note that this is a one-time check that the state is only valid and can be called when
+ * {@link InsetsController#applyAnimation} to check if the current control can skip animation
+ * at this time, and then will clear the state value.
+ */
+ public boolean getAndClearSkipAnimationOnce() {
+ final boolean result = mSkipAnimationOnce;
+ mSkipAnimationOnce = false;
+ return result;
+ }
+
+ public void setParcelableFlags(int parcelableFlags) {
+ mParcelableFlags = parcelableFlags;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeTypedObject(mLeash, mParcelableFlags);
+ dest.writeTypedObject(mSurfacePosition, mParcelableFlags);
+ dest.writeTypedObject(mInsetsHint, mParcelableFlags);
+ dest.writeBoolean(mSkipAnimationOnce);
+ }
+
+ public void release(Consumer<SurfaceControl> surfaceReleaseConsumer) {
+ if (mLeash != null) {
+ surfaceReleaseConsumer.accept(mLeash);
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final InsetsSourceControl that = (InsetsSourceControl) o;
+ final SurfaceControl thatLeash = that.mLeash;
+ return mType == that.mType
+ && ((mLeash == thatLeash)
+ || (mLeash != null && thatLeash != null && mLeash.isSameSurface(thatLeash)))
+ && mSurfacePosition.equals(that.mSurfacePosition)
+ && mInsetsHint.equals(that.mInsetsHint)
+ && mSkipAnimationOnce == that.mSkipAnimationOnce;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mType;
+ result = 31 * result + (mLeash != null ? mLeash.hashCode() : 0);
+ result = 31 * result + mSurfacePosition.hashCode();
+ result = 31 * result + mInsetsHint.hashCode();
+ result = 31 * result + (mSkipAnimationOnce ? 1 : 0);
+ return result;
+ }
+
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix);
+ pw.print("InsetsSourceControl type="); pw.print(InsetsState.typeToString(mType));
+ pw.print(" mLeash="); pw.print(mLeash);
+ pw.print(" mSurfacePosition="); pw.print(mSurfacePosition);
+ pw.print(" mInsetsHint="); pw.print(mInsetsHint);
+ pw.print(" mSkipAnimationOnce="); pw.print(mSkipAnimationOnce);
+ pw.println();
+ }
+
+ public static final @android.annotation.NonNull Creator<InsetsSourceControl> CREATOR
+ = new Creator<InsetsSourceControl>() {
+ public InsetsSourceControl createFromParcel(Parcel in) {
+ return new InsetsSourceControl(in);
+ }
+
+ public InsetsSourceControl[] newArray(int size) {
+ return new InsetsSourceControl[size];
+ }
+ };
+
+ /**
+ * Export the state of {@link InsetsSourceControl} into a protocol buffer output stream.
+ *
+ * @param proto Stream to write the state to
+ * @param fieldId FieldId of InsetsSource as defined in the parent message
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(TYPE, InsetsState.typeToString(mType));
+
+ final long surfaceToken = proto.start(POSITION);
+ proto.write(X, mSurfacePosition.x);
+ proto.write(Y, mSurfacePosition.y);
+ proto.end(surfaceToken);
+
+ if (mLeash != null) {
+ mLeash.dumpDebug(proto, LEASH);
+ }
+ proto.end(token);
+ }
+}
diff --git a/android/view/InsetsState.java b/android/view/InsetsState.java
new file mode 100644
index 0000000..37101b7
--- /dev/null
+++ b/android/view/InsetsState.java
@@ -0,0 +1,893 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsStateProto.DISPLAY_CUTOUT;
+import static android.view.InsetsStateProto.DISPLAY_FRAME;
+import static android.view.InsetsStateProto.SOURCES;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.indexOf;
+import static android.view.WindowInsets.Type.isVisibleInsetsType;
+import static android.view.WindowInsets.Type.statusBars;
+import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.WindowConfiguration;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * Holder for state of system windows that cause window insets for all other windows in the system.
+ * @hide
+ */
+public class InsetsState implements Parcelable {
+
+ /**
+ * Internal representation of inset source types. This is different from the public API in
+ * {@link WindowInsets.Type} as one type from the public API might indicate multiple windows
+ * at the same time.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "ITYPE", value = {
+ ITYPE_STATUS_BAR,
+ ITYPE_NAVIGATION_BAR,
+ ITYPE_CAPTION_BAR,
+ ITYPE_TOP_GESTURES,
+ ITYPE_BOTTOM_GESTURES,
+ ITYPE_LEFT_GESTURES,
+ ITYPE_RIGHT_GESTURES,
+ ITYPE_TOP_MANDATORY_GESTURES,
+ ITYPE_BOTTOM_MANDATORY_GESTURES,
+ ITYPE_LEFT_MANDATORY_GESTURES,
+ ITYPE_RIGHT_MANDATORY_GESTURES,
+ ITYPE_TOP_TAPPABLE_ELEMENT,
+ ITYPE_BOTTOM_TAPPABLE_ELEMENT,
+ ITYPE_LEFT_DISPLAY_CUTOUT,
+ ITYPE_TOP_DISPLAY_CUTOUT,
+ ITYPE_RIGHT_DISPLAY_CUTOUT,
+ ITYPE_BOTTOM_DISPLAY_CUTOUT,
+ ITYPE_IME,
+ ITYPE_CLIMATE_BAR,
+ ITYPE_EXTRA_NAVIGATION_BAR
+ })
+ public @interface InternalInsetsType {}
+
+ /**
+ * Special value to be used to by methods returning an {@link InternalInsetsType} to indicate
+ * that the objects/parameters aren't associated with an {@link InternalInsetsType}
+ */
+ public static final int ITYPE_INVALID = -1;
+
+ static final int FIRST_TYPE = 0;
+
+ public static final int ITYPE_STATUS_BAR = FIRST_TYPE;
+ public static final int ITYPE_NAVIGATION_BAR = 1;
+ public static final int ITYPE_CAPTION_BAR = 2;
+
+ public static final int ITYPE_TOP_GESTURES = 3;
+ public static final int ITYPE_BOTTOM_GESTURES = 4;
+ public static final int ITYPE_LEFT_GESTURES = 5;
+ public static final int ITYPE_RIGHT_GESTURES = 6;
+
+ public static final int ITYPE_TOP_MANDATORY_GESTURES = 7;
+ public static final int ITYPE_BOTTOM_MANDATORY_GESTURES = 8;
+ public static final int ITYPE_LEFT_MANDATORY_GESTURES = 9;
+ public static final int ITYPE_RIGHT_MANDATORY_GESTURES = 10;
+
+ public static final int ITYPE_LEFT_DISPLAY_CUTOUT = 11;
+ public static final int ITYPE_TOP_DISPLAY_CUTOUT = 12;
+ public static final int ITYPE_RIGHT_DISPLAY_CUTOUT = 13;
+ public static final int ITYPE_BOTTOM_DISPLAY_CUTOUT = 14;
+
+ public static final int ITYPE_LEFT_TAPPABLE_ELEMENT = 15;
+ public static final int ITYPE_TOP_TAPPABLE_ELEMENT = 16;
+ public static final int ITYPE_RIGHT_TAPPABLE_ELEMENT = 17;
+ public static final int ITYPE_BOTTOM_TAPPABLE_ELEMENT = 18;
+
+ /** Input method window. */
+ public static final int ITYPE_IME = 19;
+
+ /** Additional system decorations inset type. */
+ public static final int ITYPE_CLIMATE_BAR = 20;
+ public static final int ITYPE_EXTRA_NAVIGATION_BAR = 21;
+
+ static final int LAST_TYPE = ITYPE_EXTRA_NAVIGATION_BAR;
+ public static final int SIZE = LAST_TYPE + 1;
+
+ // Derived types
+
+ /** A shelf is the same as the navigation bar. */
+ public static final int ITYPE_SHELF = ITYPE_NAVIGATION_BAR;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "IINSETS_SIDE", value = {
+ ISIDE_LEFT,
+ ISIDE_TOP,
+ ISIDE_RIGHT,
+ ISIDE_BOTTOM,
+ ISIDE_FLOATING,
+ ISIDE_UNKNOWN
+ })
+ public @interface InternalInsetsSide {}
+ static final int ISIDE_LEFT = 0;
+ static final int ISIDE_TOP = 1;
+ static final int ISIDE_RIGHT = 2;
+ static final int ISIDE_BOTTOM = 3;
+ static final int ISIDE_FLOATING = 4;
+ static final int ISIDE_UNKNOWN = 5;
+
+ private final InsetsSource[] mSources = new InsetsSource[SIZE];
+
+ /**
+ * The frame of the display these sources are relative to.
+ */
+ private final Rect mDisplayFrame = new Rect();
+
+ /** The area cut from the display. */
+ private final DisplayCutout.ParcelableWrapper mDisplayCutout =
+ new DisplayCutout.ParcelableWrapper();
+
+ /** The rounded corners on the display */
+ private RoundedCorners mRoundedCorners = RoundedCorners.NO_ROUNDED_CORNERS;
+
+ /** The bounds of the Privacy Indicator */
+ private PrivacyIndicatorBounds mPrivacyIndicatorBounds =
+ new PrivacyIndicatorBounds();
+
+ public InsetsState() {
+ }
+
+ public InsetsState(InsetsState copy) {
+ set(copy);
+ }
+
+ public InsetsState(InsetsState copy, boolean copySources) {
+ set(copy, copySources);
+ }
+
+ /**
+ * Calculates {@link WindowInsets} based on the current source configuration.
+ *
+ * @param frame The frame to calculate the insets relative to.
+ * @param ignoringVisibilityState {@link InsetsState} used to calculate
+ * {@link WindowInsets#getInsetsIgnoringVisibility(int)} information, or pass
+ * {@code null} to use this state to calculate that information.
+ * @return The calculated insets.
+ */
+ public WindowInsets calculateInsets(Rect frame, @Nullable InsetsState ignoringVisibilityState,
+ boolean isScreenRound, boolean alwaysConsumeSystemBars,
+ int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags,
+ int windowType, @WindowConfiguration.WindowingMode int windowingMode,
+ @Nullable @InternalInsetsSide SparseIntArray typeSideMap) {
+ Insets[] typeInsetsMap = new Insets[Type.SIZE];
+ Insets[] typeMaxInsetsMap = new Insets[Type.SIZE];
+ boolean[] typeVisibilityMap = new boolean[SIZE];
+ final Rect relativeFrame = new Rect(frame);
+ final Rect relativeFrameMax = new Rect(frame);
+ for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+ InsetsSource source = mSources[type];
+ if (source == null) {
+ int index = indexOf(toPublicType(type));
+ if (typeInsetsMap[index] == null) {
+ typeInsetsMap[index] = Insets.NONE;
+ }
+ continue;
+ }
+
+ processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
+ typeSideMap, typeVisibilityMap);
+
+ // IME won't be reported in max insets as the size depends on the EditorInfo of the IME
+ // target.
+ if (source.getType() != ITYPE_IME) {
+ InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null
+ ? ignoringVisibilityState.getSource(type)
+ : source;
+ if (ignoringVisibilitySource == null) {
+ continue;
+ }
+ processSource(ignoringVisibilitySource, relativeFrameMax,
+ true /* ignoreVisibility */, typeMaxInsetsMap, null /* typeSideMap */,
+ null /* typeVisibilityMap */);
+ }
+ }
+ final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST;
+
+ @InsetsType int compatInsetsTypes = systemBars() | displayCutout();
+ if (softInputAdjustMode == SOFT_INPUT_ADJUST_RESIZE) {
+ compatInsetsTypes |= ime();
+ }
+ if ((legacyWindowFlags & FLAG_FULLSCREEN) != 0) {
+ compatInsetsTypes &= ~statusBars();
+ }
+ if (clearCompatInsets(windowType, legacyWindowFlags, windowingMode)) {
+ compatInsetsTypes = 0;
+ }
+
+ return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound,
+ alwaysConsumeSystemBars, calculateRelativeCutout(frame),
+ calculateRelativeRoundedCorners(frame),
+ calculateRelativePrivacyIndicatorBounds(frame),
+ compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0);
+ }
+
+ private DisplayCutout calculateRelativeCutout(Rect frame) {
+ final DisplayCutout raw = mDisplayCutout.get();
+ if (mDisplayFrame.equals(frame)) {
+ return raw;
+ }
+ if (frame == null) {
+ return DisplayCutout.NO_CUTOUT;
+ }
+ final int insetLeft = frame.left - mDisplayFrame.left;
+ final int insetTop = frame.top - mDisplayFrame.top;
+ final int insetRight = mDisplayFrame.right - frame.right;
+ final int insetBottom = mDisplayFrame.bottom - frame.bottom;
+ if (insetLeft >= raw.getSafeInsetLeft()
+ && insetTop >= raw.getSafeInsetTop()
+ && insetRight >= raw.getSafeInsetRight()
+ && insetBottom >= raw.getSafeInsetBottom()) {
+ return DisplayCutout.NO_CUTOUT;
+ }
+ return raw.inset(insetLeft, insetTop, insetRight, insetBottom);
+ }
+
+ private RoundedCorners calculateRelativeRoundedCorners(Rect frame) {
+ if (mDisplayFrame.equals(frame)) {
+ return mRoundedCorners;
+ }
+ if (frame == null) {
+ return RoundedCorners.NO_ROUNDED_CORNERS;
+ }
+ final int insetLeft = frame.left - mDisplayFrame.left;
+ final int insetTop = frame.top - mDisplayFrame.top;
+ final int insetRight = mDisplayFrame.right - frame.right;
+ final int insetBottom = mDisplayFrame.bottom - frame.bottom;
+ return mRoundedCorners.inset(insetLeft, insetTop, insetRight, insetBottom);
+ }
+
+ private PrivacyIndicatorBounds calculateRelativePrivacyIndicatorBounds(Rect frame) {
+ if (mDisplayFrame.equals(frame)) {
+ return mPrivacyIndicatorBounds;
+ }
+ if (frame == null) {
+ return null;
+ }
+ final int insetLeft = frame.left - mDisplayFrame.left;
+ final int insetTop = frame.top - mDisplayFrame.top;
+ final int insetRight = mDisplayFrame.right - frame.right;
+ final int insetBottom = mDisplayFrame.bottom - frame.bottom;
+ return mPrivacyIndicatorBounds.inset(insetLeft, insetTop, insetRight, insetBottom);
+ }
+
+ public Rect calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility) {
+ Insets insets = Insets.NONE;
+ for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+ InsetsSource source = mSources[type];
+ if (source == null) {
+ continue;
+ }
+ int publicType = InsetsState.toPublicType(type);
+ if ((publicType & types) == 0) {
+ continue;
+ }
+ insets = Insets.max(source.calculateInsets(frame, ignoreVisibility), insets);
+ }
+ return insets.toRect();
+ }
+
+ public Rect calculateVisibleInsets(Rect frame, @SoftInputModeFlags int softInputMode) {
+ Insets insets = Insets.NONE;
+ for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+ InsetsSource source = mSources[type];
+ if (source == null) {
+ continue;
+ }
+
+ // Ignore everything that's not a system bar or IME.
+ int publicType = InsetsState.toPublicType(type);
+ if (!isVisibleInsetsType(publicType, softInputMode)) {
+ continue;
+ }
+ insets = Insets.max(source.calculateVisibleInsets(frame), insets);
+ }
+ return insets.toRect();
+ }
+
+ /**
+ * Calculate which insets *cannot* be controlled, because the frame does not cover the
+ * respective side of the inset.
+ *
+ * If the frame of our window doesn't cover the entire inset, the control API makes very
+ * little sense, as we don't deal with negative insets.
+ */
+ @InsetsType
+ public int calculateUncontrollableInsetsFromFrame(Rect frame) {
+ int blocked = 0;
+ for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
+ InsetsSource source = mSources[type];
+ if (source == null) {
+ continue;
+ }
+ if (!canControlSide(frame, getInsetSide(
+ source.calculateInsets(frame, true /* ignoreVisibility */)))) {
+ blocked |= toPublicType(type);
+ }
+ }
+ return blocked;
+ }
+
+ private boolean canControlSide(Rect frame, int side) {
+ switch (side) {
+ case ISIDE_LEFT:
+ case ISIDE_RIGHT:
+ return frame.left == mDisplayFrame.left && frame.right == mDisplayFrame.right;
+ case ISIDE_TOP:
+ case ISIDE_BOTTOM:
+ return frame.top == mDisplayFrame.top && frame.bottom == mDisplayFrame.bottom;
+ case ISIDE_FLOATING:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
+ Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray typeSideMap,
+ @Nullable boolean[] typeVisibilityMap) {
+ Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
+
+ int type = toPublicType(source.getType());
+ processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ insets, type);
+
+ if (type == Type.MANDATORY_SYSTEM_GESTURES) {
+ // Mandatory system gestures are also system gestures.
+ // TODO: find a way to express this more generally. One option would be to define
+ // Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the
+ // ability to set systemGestureInsets() independently from
+ // mandatorySystemGestureInsets() in the Builder.
+ processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
+ insets, Type.SYSTEM_GESTURES);
+ }
+ }
+
+ private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
+ @InternalInsetsSide @Nullable SparseIntArray typeSideMap,
+ @Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
+ int index = indexOf(type);
+ Insets existing = typeInsetsMap[index];
+ if (existing == null) {
+ typeInsetsMap[index] = insets;
+ } else {
+ typeInsetsMap[index] = Insets.max(existing, insets);
+ }
+
+ if (typeVisibilityMap != null) {
+ typeVisibilityMap[index] = source.isVisible();
+ }
+
+ if (typeSideMap != null) {
+ @InternalInsetsSide int insetSide = getInsetSide(insets);
+ if (insetSide != ISIDE_UNKNOWN) {
+ typeSideMap.put(source.getType(), insetSide);
+ }
+ }
+ }
+
+ /**
+ * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b
+ * is set in order that this method returns a meaningful result.
+ */
+ static @InternalInsetsSide int getInsetSide(Insets insets) {
+ if (Insets.NONE.equals(insets)) {
+ return ISIDE_FLOATING;
+ }
+ if (insets.left != 0) {
+ return ISIDE_LEFT;
+ }
+ if (insets.top != 0) {
+ return ISIDE_TOP;
+ }
+ if (insets.right != 0) {
+ return ISIDE_RIGHT;
+ }
+ if (insets.bottom != 0) {
+ return ISIDE_BOTTOM;
+ }
+ return ISIDE_UNKNOWN;
+ }
+
+ public InsetsSource getSource(@InternalInsetsType int type) {
+ InsetsSource source = mSources[type];
+ if (source != null) {
+ return source;
+ }
+ source = new InsetsSource(type);
+ mSources[type] = source;
+ return source;
+ }
+
+ public @Nullable InsetsSource peekSource(@InternalInsetsType int type) {
+ return mSources[type];
+ }
+
+ /**
+ * Returns the source visibility or the default visibility if the source doesn't exist. This is
+ * useful if when treating this object as a request.
+ *
+ * @param type The {@link InternalInsetsType} to query.
+ * @return {@code true} if the source is visible or the type is default visible and the source
+ * doesn't exist.
+ */
+ public boolean getSourceOrDefaultVisibility(@InternalInsetsType int type) {
+ final InsetsSource source = mSources[type];
+ return source != null ? source.isVisible() : getDefaultVisibility(type);
+ }
+
+ public void setDisplayFrame(Rect frame) {
+ mDisplayFrame.set(frame);
+ }
+
+ public Rect getDisplayFrame() {
+ return mDisplayFrame;
+ }
+
+ public void setDisplayCutout(DisplayCutout cutout) {
+ mDisplayCutout.set(cutout);
+ }
+
+ public DisplayCutout getDisplayCutout() {
+ return mDisplayCutout.get();
+ }
+
+ public void setRoundedCorners(RoundedCorners roundedCorners) {
+ mRoundedCorners = roundedCorners;
+ }
+
+ public RoundedCorners getRoundedCorners() {
+ return mRoundedCorners;
+ }
+
+ public void setPrivacyIndicatorBounds(PrivacyIndicatorBounds bounds) {
+ mPrivacyIndicatorBounds = bounds;
+ }
+
+ public PrivacyIndicatorBounds getPrivacyIndicatorBounds() {
+ return mPrivacyIndicatorBounds;
+ }
+
+ /**
+ * Modifies the state of this class to exclude a certain type to make it ready for dispatching
+ * to the client.
+ *
+ * @param type The {@link InternalInsetsType} of the source to remove
+ * @return {@code true} if this InsetsState was modified; {@code false} otherwise.
+ */
+ public boolean removeSource(@InternalInsetsType int type) {
+ if (mSources[type] == null) {
+ return false;
+ }
+ mSources[type] = null;
+ return true;
+ }
+
+ /**
+ * A shortcut for setting the visibility of the source.
+ *
+ * @param type The {@link InternalInsetsType} of the source to set the visibility
+ * @param visible {@code true} for visible
+ */
+ public void setSourceVisible(@InternalInsetsType int type, boolean visible) {
+ InsetsSource source = mSources[type];
+ if (source != null) {
+ source.setVisible(visible);
+ }
+ }
+
+ /**
+ * Scales the frame and the visible frame (if there is one) of each source.
+ *
+ * @param scale the scale to be applied
+ */
+ public void scale(float scale) {
+ mDisplayFrame.scale(scale);
+ mDisplayCutout.scale(scale);
+ mRoundedCorners = mRoundedCorners.scale(scale);
+ mPrivacyIndicatorBounds = mPrivacyIndicatorBounds.scale(scale);
+ for (int i = 0; i < SIZE; i++) {
+ final InsetsSource source = mSources[i];
+ if (source != null) {
+ source.getFrame().scale(scale);
+ final Rect visibleFrame = source.getVisibleFrame();
+ if (visibleFrame != null) {
+ visibleFrame.scale(scale);
+ }
+ }
+ }
+ }
+
+ public void set(InsetsState other) {
+ set(other, false /* copySources */);
+ }
+
+ public void set(InsetsState other, boolean copySources) {
+ mDisplayFrame.set(other.mDisplayFrame);
+ mDisplayCutout.set(other.mDisplayCutout);
+ mRoundedCorners = other.getRoundedCorners();
+ mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
+ if (copySources) {
+ for (int i = 0; i < SIZE; i++) {
+ InsetsSource source = other.mSources[i];
+ mSources[i] = source != null ? new InsetsSource(source) : null;
+ }
+ } else {
+ for (int i = 0; i < SIZE; i++) {
+ mSources[i] = other.mSources[i];
+ }
+ }
+ }
+
+ /**
+ * Sets the values from the other InsetsState. But for sources, only specific types of source
+ * would be set.
+ *
+ * @param other the other InsetsState.
+ * @param types the only types of sources would be set.
+ */
+ public void set(InsetsState other, @InsetsType int types) {
+ mDisplayFrame.set(other.mDisplayFrame);
+ mDisplayCutout.set(other.mDisplayCutout);
+ mRoundedCorners = other.getRoundedCorners();
+ mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
+ final ArraySet<Integer> t = toInternalType(types);
+ for (int i = t.size() - 1; i >= 0; i--) {
+ final int type = t.valueAt(i);
+ mSources[type] = other.mSources[type];
+ }
+ }
+
+ public void addSource(InsetsSource source) {
+ mSources[source.getType()] = source;
+ }
+
+ public static boolean clearCompatInsets(int windowType, int windowFlags, int windowingMode) {
+ return (windowFlags & FLAG_LAYOUT_NO_LIMITS) != 0
+ && windowType != TYPE_WALLPAPER && windowType != TYPE_SYSTEM_ERROR
+ && !WindowConfiguration.inMultiWindowMode(windowingMode);
+ }
+
+ public static @InternalInsetsType ArraySet<Integer> toInternalType(@InsetsType int types) {
+ final ArraySet<Integer> result = new ArraySet<>();
+ if ((types & Type.STATUS_BARS) != 0) {
+ result.add(ITYPE_STATUS_BAR);
+ result.add(ITYPE_CLIMATE_BAR);
+ }
+ if ((types & Type.NAVIGATION_BARS) != 0) {
+ result.add(ITYPE_NAVIGATION_BAR);
+ result.add(ITYPE_EXTRA_NAVIGATION_BAR);
+ }
+ if ((types & Type.CAPTION_BAR) != 0) {
+ result.add(ITYPE_CAPTION_BAR);
+ }
+ if ((types & Type.SYSTEM_GESTURES) != 0) {
+ result.add(ITYPE_LEFT_GESTURES);
+ result.add(ITYPE_TOP_GESTURES);
+ result.add(ITYPE_RIGHT_GESTURES);
+ result.add(ITYPE_BOTTOM_GESTURES);
+ }
+ if ((types & Type.MANDATORY_SYSTEM_GESTURES) != 0) {
+ result.add(ITYPE_LEFT_MANDATORY_GESTURES);
+ result.add(ITYPE_TOP_MANDATORY_GESTURES);
+ result.add(ITYPE_RIGHT_MANDATORY_GESTURES);
+ result.add(ITYPE_BOTTOM_MANDATORY_GESTURES);
+ }
+ if ((types & Type.DISPLAY_CUTOUT) != 0) {
+ result.add(ITYPE_LEFT_DISPLAY_CUTOUT);
+ result.add(ITYPE_TOP_DISPLAY_CUTOUT);
+ result.add(ITYPE_RIGHT_DISPLAY_CUTOUT);
+ result.add(ITYPE_BOTTOM_DISPLAY_CUTOUT);
+ }
+ if ((types & Type.IME) != 0) {
+ result.add(ITYPE_IME);
+ }
+ return result;
+ }
+
+ /**
+ * Converting a internal type to the public type.
+ * @param type internal insets type, {@code InternalInsetsType}.
+ * @return public insets type, {@code Type.InsetsType}.
+ */
+ public static @Type.InsetsType int toPublicType(@InternalInsetsType int type) {
+ switch (type) {
+ case ITYPE_STATUS_BAR:
+ case ITYPE_CLIMATE_BAR:
+ return Type.STATUS_BARS;
+ case ITYPE_NAVIGATION_BAR:
+ case ITYPE_EXTRA_NAVIGATION_BAR:
+ return Type.NAVIGATION_BARS;
+ case ITYPE_CAPTION_BAR:
+ return Type.CAPTION_BAR;
+ case ITYPE_IME:
+ return Type.IME;
+ case ITYPE_TOP_MANDATORY_GESTURES:
+ case ITYPE_BOTTOM_MANDATORY_GESTURES:
+ case ITYPE_LEFT_MANDATORY_GESTURES:
+ case ITYPE_RIGHT_MANDATORY_GESTURES:
+ return Type.MANDATORY_SYSTEM_GESTURES;
+ case ITYPE_TOP_GESTURES:
+ case ITYPE_BOTTOM_GESTURES:
+ case ITYPE_LEFT_GESTURES:
+ case ITYPE_RIGHT_GESTURES:
+ return Type.SYSTEM_GESTURES;
+ case ITYPE_LEFT_TAPPABLE_ELEMENT:
+ case ITYPE_TOP_TAPPABLE_ELEMENT:
+ case ITYPE_RIGHT_TAPPABLE_ELEMENT:
+ case ITYPE_BOTTOM_TAPPABLE_ELEMENT:
+ return Type.TAPPABLE_ELEMENT;
+ case ITYPE_LEFT_DISPLAY_CUTOUT:
+ case ITYPE_TOP_DISPLAY_CUTOUT:
+ case ITYPE_RIGHT_DISPLAY_CUTOUT:
+ case ITYPE_BOTTOM_DISPLAY_CUTOUT:
+ return Type.DISPLAY_CUTOUT;
+ default:
+ throw new IllegalArgumentException("Unknown type: " + type);
+ }
+ }
+
+ public static boolean getDefaultVisibility(@InternalInsetsType int type) {
+ return type != ITYPE_IME;
+ }
+
+ public static boolean containsType(@InternalInsetsType int[] types,
+ @InternalInsetsType int type) {
+ if (types == null) {
+ return false;
+ }
+ for (int t : types) {
+ if (t == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void dump(String prefix, PrintWriter pw) {
+ final String newPrefix = prefix + " ";
+ pw.println(prefix + "InsetsState");
+ pw.println(newPrefix + "mDisplayFrame=" + mDisplayFrame);
+ pw.println(newPrefix + "mDisplayCutout=" + mDisplayCutout.get());
+ pw.println(newPrefix + "mRoundedCorners=" + mRoundedCorners);
+ pw.println(newPrefix + "mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds);
+ for (int i = 0; i < SIZE; i++) {
+ InsetsSource source = mSources[i];
+ if (source == null) continue;
+ source.dump(newPrefix + " ", pw);
+ }
+ }
+
+ void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ InsetsSource source = mSources[ITYPE_IME];
+ if (source != null) {
+ source.dumpDebug(proto, SOURCES);
+ }
+ mDisplayFrame.dumpDebug(proto, DISPLAY_FRAME);
+ mDisplayCutout.get().dumpDebug(proto, DISPLAY_CUTOUT);
+ proto.end(token);
+ }
+
+ public static String typeToString(@InternalInsetsType int type) {
+ switch (type) {
+ case ITYPE_STATUS_BAR:
+ return "ITYPE_STATUS_BAR";
+ case ITYPE_NAVIGATION_BAR:
+ return "ITYPE_NAVIGATION_BAR";
+ case ITYPE_CAPTION_BAR:
+ return "ITYPE_CAPTION_BAR";
+ case ITYPE_TOP_GESTURES:
+ return "ITYPE_TOP_GESTURES";
+ case ITYPE_BOTTOM_GESTURES:
+ return "ITYPE_BOTTOM_GESTURES";
+ case ITYPE_LEFT_GESTURES:
+ return "ITYPE_LEFT_GESTURES";
+ case ITYPE_RIGHT_GESTURES:
+ return "ITYPE_RIGHT_GESTURES";
+ case ITYPE_TOP_MANDATORY_GESTURES:
+ return "ITYPE_TOP_MANDATORY_GESTURES";
+ case ITYPE_BOTTOM_MANDATORY_GESTURES:
+ return "ITYPE_BOTTOM_MANDATORY_GESTURES";
+ case ITYPE_LEFT_MANDATORY_GESTURES:
+ return "ITYPE_LEFT_MANDATORY_GESTURES";
+ case ITYPE_RIGHT_MANDATORY_GESTURES:
+ return "ITYPE_RIGHT_MANDATORY_GESTURES";
+ case ITYPE_LEFT_TAPPABLE_ELEMENT:
+ return "ITYPE_LEFT_TAPPABLE_ELEMENT";
+ case ITYPE_TOP_TAPPABLE_ELEMENT:
+ return "ITYPE_TOP_TAPPABLE_ELEMENT";
+ case ITYPE_RIGHT_TAPPABLE_ELEMENT:
+ return "ITYPE_RIGHT_TAPPABLE_ELEMENT";
+ case ITYPE_BOTTOM_TAPPABLE_ELEMENT:
+ return "ITYPE_BOTTOM_TAPPABLE_ELEMENT";
+ case ITYPE_LEFT_DISPLAY_CUTOUT:
+ return "ITYPE_LEFT_DISPLAY_CUTOUT";
+ case ITYPE_TOP_DISPLAY_CUTOUT:
+ return "ITYPE_TOP_DISPLAY_CUTOUT";
+ case ITYPE_RIGHT_DISPLAY_CUTOUT:
+ return "ITYPE_RIGHT_DISPLAY_CUTOUT";
+ case ITYPE_BOTTOM_DISPLAY_CUTOUT:
+ return "ITYPE_BOTTOM_DISPLAY_CUTOUT";
+ case ITYPE_IME:
+ return "ITYPE_IME";
+ case ITYPE_CLIMATE_BAR:
+ return "ITYPE_CLIMATE_BAR";
+ case ITYPE_EXTRA_NAVIGATION_BAR:
+ return "ITYPE_EXTRA_NAVIGATION_BAR";
+ default:
+ return "ITYPE_UNKNOWN_" + type;
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return equals(o, false, false);
+ }
+
+ /**
+ * An equals method can exclude the caption insets. This is useful because we assemble the
+ * caption insets information on the client side, and when we communicate with server, it's
+ * excluded.
+ * @param excludingCaptionInsets {@code true} if we want to compare two InsetsState objects but
+ * ignore the caption insets source value.
+ * @param excludeInvisibleImeFrames If {@link #ITYPE_IME} frames should be ignored when IME is
+ * not visible.
+ * @return {@code true} if the two InsetsState objects are equal, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ public boolean equals(@Nullable Object o, boolean excludingCaptionInsets,
+ boolean excludeInvisibleImeFrames) {
+ if (this == o) { return true; }
+ if (o == null || getClass() != o.getClass()) { return false; }
+
+ InsetsState state = (InsetsState) o;
+
+ if (!mDisplayFrame.equals(state.mDisplayFrame)
+ || !mDisplayCutout.equals(state.mDisplayCutout)
+ || !mRoundedCorners.equals(state.mRoundedCorners)
+ || !mPrivacyIndicatorBounds.equals(state.mPrivacyIndicatorBounds)) {
+ return false;
+ }
+ for (int i = 0; i < SIZE; i++) {
+ if (excludingCaptionInsets) {
+ if (i == ITYPE_CAPTION_BAR) continue;
+ }
+ InsetsSource source = mSources[i];
+ InsetsSource otherSource = state.mSources[i];
+ if (source == null && otherSource == null) {
+ continue;
+ }
+ if (source == null || otherSource == null) {
+ return false;
+ }
+ if (!otherSource.equals(source, excludeInvisibleImeFrames)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDisplayFrame, mDisplayCutout, Arrays.hashCode(mSources),
+ mRoundedCorners, mPrivacyIndicatorBounds);
+ }
+
+ public InsetsState(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mDisplayFrame.writeToParcel(dest, flags);
+ mDisplayCutout.writeToParcel(dest, flags);
+ dest.writeTypedArray(mSources, 0 /* parcelableFlags */);
+ dest.writeTypedObject(mRoundedCorners, flags);
+ dest.writeTypedObject(mPrivacyIndicatorBounds, flags);
+ }
+
+ public static final @NonNull Creator<InsetsState> CREATOR = new Creator<InsetsState>() {
+
+ public InsetsState createFromParcel(Parcel in) {
+ return new InsetsState(in);
+ }
+
+ public InsetsState[] newArray(int size) {
+ return new InsetsState[size];
+ }
+ };
+
+ public void readFromParcel(Parcel in) {
+ mDisplayFrame.readFromParcel(in);
+ mDisplayCutout.readFromParcel(in);
+ in.readTypedArray(mSources, InsetsSource.CREATOR);
+ mRoundedCorners = in.readTypedObject(RoundedCorners.CREATOR);
+ mPrivacyIndicatorBounds = in.readTypedObject(PrivacyIndicatorBounds.CREATOR);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner joiner = new StringJoiner(", ");
+ for (int i = 0; i < SIZE; i++) {
+ InsetsSource source = mSources[i];
+ if (source != null) {
+ joiner.add(source.toString());
+ }
+ }
+ return "InsetsState: {"
+ + "mDisplayFrame=" + mDisplayFrame
+ + ", mDisplayCutout=" + mDisplayCutout
+ + ", mRoundedCorners=" + mRoundedCorners
+ + ", mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds
+ + ", mSources= { " + joiner
+ + " }";
+ }
+
+ public @NonNull String toSourceVisibilityString() {
+ StringJoiner joiner = new StringJoiner(", ");
+ for (int i = 0; i < SIZE; i++) {
+ InsetsSource source = mSources[i];
+ if (source != null) {
+ joiner.add(typeToString(i) + ": " + (source.isVisible() ? "visible" : "invisible"));
+ }
+ }
+ return joiner.toString();
+ }
+}
+
diff --git a/android/view/KeyCharacterMap.java b/android/view/KeyCharacterMap.java
new file mode 100644
index 0000000..aa1acc1
--- /dev/null
+++ b/android/view/KeyCharacterMap.java
@@ -0,0 +1,818 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.hardware.input.InputManager;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.method.MetaKeyKeyListener;
+import android.util.AndroidRuntimeException;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.text.Normalizer;
+
+/**
+ * Describes the keys provided by a keyboard device and their associated labels.
+ */
+public class KeyCharacterMap implements Parcelable {
+ /**
+ * The id of the device's primary built in keyboard is always 0.
+ *
+ * @deprecated This constant should no longer be used because there is no
+ * guarantee that a device has a built-in keyboard that can be used for
+ * typing text. There might not be a built-in keyboard, the built-in keyboard
+ * might be a {@link #NUMERIC} or {@link #SPECIAL_FUNCTION} keyboard, or there
+ * might be multiple keyboards installed including external keyboards.
+ * When interpreting key presses received from the framework, applications should
+ * use the device id specified in the {@link KeyEvent} received.
+ * When synthesizing key presses for delivery elsewhere or when translating key presses
+ * from unknown keyboards, applications should use the special {@link #VIRTUAL_KEYBOARD}
+ * device id.
+ */
+ @Deprecated
+ public static final int BUILT_IN_KEYBOARD = 0;
+
+ /**
+ * The id of a generic virtual keyboard with a full layout that can be used to
+ * synthesize key events. Typically used with {@link #getEvents}.
+ */
+ public static final int VIRTUAL_KEYBOARD = -1;
+
+ /**
+ * A numeric (12-key) keyboard.
+ * <p>
+ * A numeric keyboard supports text entry using a multi-tap approach.
+ * It may be necessary to tap a key multiple times to generate the desired letter
+ * or symbol.
+ * </p><p>
+ * This type of keyboard is generally designed for thumb typing.
+ * </p>
+ */
+ public static final int NUMERIC = 1;
+
+ /**
+ * A keyboard with all the letters, but with more than one letter per key.
+ * <p>
+ * This type of keyboard is generally designed for thumb typing.
+ * </p>
+ */
+ public static final int PREDICTIVE = 2;
+
+ /**
+ * A keyboard with all the letters, and maybe some numbers.
+ * <p>
+ * An alphabetic keyboard supports text entry directly but may have a condensed
+ * layout with a small form factor. In contrast to a {@link #FULL full keyboard}, some
+ * symbols may only be accessible using special on-screen character pickers.
+ * In addition, to improve typing speed and accuracy, the framework provides
+ * special affordances for alphabetic keyboards such as auto-capitalization
+ * and toggled / locked shift and alt keys.
+ * </p><p>
+ * This type of keyboard is generally designed for thumb typing.
+ * </p>
+ */
+ public static final int ALPHA = 3;
+
+ /**
+ * A full PC-style keyboard.
+ * <p>
+ * A full keyboard behaves like a PC keyboard. All symbols are accessed directly
+ * by pressing keys on the keyboard without on-screen support or affordances such
+ * as auto-capitalization.
+ * </p><p>
+ * This type of keyboard is generally designed for full two hand typing.
+ * </p>
+ */
+ public static final int FULL = 4;
+
+ /**
+ * A keyboard that is only used to control special functions rather than for typing.
+ * <p>
+ * A special function keyboard consists only of non-printing keys such as
+ * HOME and POWER that are not actually used for typing.
+ * </p>
+ */
+ public static final int SPECIAL_FUNCTION = 5;
+
+ /**
+ * This private-use character is used to trigger Unicode character
+ * input by hex digits.
+ */
+ public static final char HEX_INPUT = '\uEF00';
+
+ /**
+ * This private-use character is used to bring up a character picker for
+ * miscellaneous symbols.
+ */
+ public static final char PICKER_DIALOG_INPUT = '\uEF01';
+
+ /**
+ * Modifier keys may be chorded with character keys.
+ *
+ * @see #getModifierBehavior()
+ */
+ public static final int MODIFIER_BEHAVIOR_CHORDED = 0;
+
+ /**
+ * Modifier keys may be chorded with character keys or they may toggle
+ * into latched or locked states when pressed independently.
+ *
+ * @see #getModifierBehavior()
+ */
+ public static final int MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED = 1;
+
+ /*
+ * This bit will be set in the return value of {@link #get(int, int)} if the
+ * key is a "dead key."
+ */
+ public static final int COMBINING_ACCENT = 0x80000000;
+
+ /**
+ * Mask the return value from {@link #get(int, int)} with this value to get
+ * a printable representation of the accent character of a "dead key."
+ */
+ public static final int COMBINING_ACCENT_MASK = 0x7FFFFFFF;
+
+ /* Characters used to display placeholders for dead keys. */
+ private static final int ACCENT_ACUTE = '\u00B4';
+ private static final int ACCENT_BREVE = '\u02D8';
+ private static final int ACCENT_CARON = '\u02C7';
+ private static final int ACCENT_CEDILLA = '\u00B8';
+ private static final int ACCENT_CIRCUMFLEX = '\u02C6';
+ private static final int ACCENT_COMMA_ABOVE = '\u1FBD';
+ private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC';
+ private static final int ACCENT_DOT_ABOVE = '\u02D9';
+ private static final int ACCENT_DOT_BELOW = '.'; // approximate
+ private static final int ACCENT_DOUBLE_ACUTE = '\u02DD';
+ private static final int ACCENT_GRAVE = '\u02CB';
+ private static final int ACCENT_HOOK_ABOVE = '\u02C0';
+ private static final int ACCENT_HORN = '\''; // approximate
+ private static final int ACCENT_MACRON = '\u00AF';
+ private static final int ACCENT_MACRON_BELOW = '\u02CD';
+ private static final int ACCENT_OGONEK = '\u02DB';
+ private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD';
+ private static final int ACCENT_RING_ABOVE = '\u02DA';
+ private static final int ACCENT_STROKE = '-'; // approximate
+ private static final int ACCENT_TILDE = '\u02DC';
+ private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB';
+ private static final int ACCENT_UMLAUT = '\u00A8';
+ private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8';
+ private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC';
+
+ /* Legacy dead key display characters used in previous versions of the API.
+ * We still support these characters by mapping them to their non-legacy version. */
+ private static final int ACCENT_GRAVE_LEGACY = '`';
+ private static final int ACCENT_CIRCUMFLEX_LEGACY = '^';
+ private static final int ACCENT_TILDE_LEGACY = '~';
+
+ private static final int CHAR_SPACE = ' ';
+
+ /**
+ * Maps Unicode combining diacritical to display-form dead key.
+ */
+ private static final SparseIntArray sCombiningToAccent = new SparseIntArray();
+ private static final SparseIntArray sAccentToCombining = new SparseIntArray();
+ static {
+ addCombining('\u0300', ACCENT_GRAVE);
+ addCombining('\u0301', ACCENT_ACUTE);
+ addCombining('\u0302', ACCENT_CIRCUMFLEX);
+ addCombining('\u0303', ACCENT_TILDE);
+ addCombining('\u0304', ACCENT_MACRON);
+ addCombining('\u0306', ACCENT_BREVE);
+ addCombining('\u0307', ACCENT_DOT_ABOVE);
+ addCombining('\u0308', ACCENT_UMLAUT);
+ addCombining('\u0309', ACCENT_HOOK_ABOVE);
+ addCombining('\u030A', ACCENT_RING_ABOVE);
+ addCombining('\u030B', ACCENT_DOUBLE_ACUTE);
+ addCombining('\u030C', ACCENT_CARON);
+ addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE);
+ //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE);
+ //addCombining('\u030F', ACCENT_DOUBLE_GRAVE);
+ //addCombining('\u0310', ACCENT_CANDRABINDU);
+ //addCombining('\u0311', ACCENT_INVERTED_BREVE);
+ addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE);
+ addCombining('\u0313', ACCENT_COMMA_ABOVE);
+ addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE);
+ addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT);
+ addCombining('\u031B', ACCENT_HORN);
+ addCombining('\u0323', ACCENT_DOT_BELOW);
+ //addCombining('\u0326', ACCENT_COMMA_BELOW);
+ addCombining('\u0327', ACCENT_CEDILLA);
+ addCombining('\u0328', ACCENT_OGONEK);
+ addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW);
+ addCombining('\u0331', ACCENT_MACRON_BELOW);
+ addCombining('\u0335', ACCENT_STROKE);
+ //addCombining('\u0342', ACCENT_PERISPOMENI);
+ //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS);
+ //addCombining('\u0345', ACCENT_YPOGEGRAMMENI);
+
+ // One-way mappings to equivalent preferred accents.
+ sCombiningToAccent.append('\u0340', ACCENT_GRAVE);
+ sCombiningToAccent.append('\u0341', ACCENT_ACUTE);
+ sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE);
+
+ // One-way legacy mappings to preserve compatibility with older applications.
+ sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300');
+ sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302');
+ sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303');
+ }
+
+ private static void addCombining(int combining, int accent) {
+ sCombiningToAccent.append(combining, accent);
+ sAccentToCombining.append(accent, combining);
+ }
+
+ /**
+ * Maps combinations of (display-form) combining key and second character
+ * to combined output character.
+ * These mappings are derived from the Unicode NFC tables as needed.
+ */
+ private static final SparseIntArray sDeadKeyCache = new SparseIntArray();
+ private static final StringBuilder sDeadKeyBuilder = new StringBuilder();
+ static {
+ // Non-standard decompositions.
+ // Stroke modifier for Finnish multilingual keyboard and others.
+ addDeadKey(ACCENT_STROKE, 'D', '\u0110');
+ addDeadKey(ACCENT_STROKE, 'G', '\u01e4');
+ addDeadKey(ACCENT_STROKE, 'H', '\u0126');
+ addDeadKey(ACCENT_STROKE, 'I', '\u0197');
+ addDeadKey(ACCENT_STROKE, 'L', '\u0141');
+ addDeadKey(ACCENT_STROKE, 'O', '\u00d8');
+ addDeadKey(ACCENT_STROKE, 'T', '\u0166');
+ addDeadKey(ACCENT_STROKE, 'd', '\u0111');
+ addDeadKey(ACCENT_STROKE, 'g', '\u01e5');
+ addDeadKey(ACCENT_STROKE, 'h', '\u0127');
+ addDeadKey(ACCENT_STROKE, 'i', '\u0268');
+ addDeadKey(ACCENT_STROKE, 'l', '\u0142');
+ addDeadKey(ACCENT_STROKE, 'o', '\u00f8');
+ addDeadKey(ACCENT_STROKE, 't', '\u0167');
+ }
+
+ private static void addDeadKey(int accent, int c, int result) {
+ final int combining = sAccentToCombining.get(accent);
+ if (combining == 0) {
+ throw new IllegalStateException("Invalid dead key declaration.");
+ }
+ final int combination = (combining << 16) | c;
+ sDeadKeyCache.put(combination, result);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<KeyCharacterMap> CREATOR =
+ new Parcelable.Creator<KeyCharacterMap>() {
+ public KeyCharacterMap createFromParcel(Parcel in) {
+ return new KeyCharacterMap(in);
+ }
+ public KeyCharacterMap[] newArray(int size) {
+ return new KeyCharacterMap[size];
+ }
+ };
+
+ private long mPtr;
+
+ private static native long nativeReadFromParcel(Parcel in);
+ private static native void nativeWriteToParcel(long ptr, Parcel out);
+ private static native void nativeDispose(long ptr);
+
+ private static native char nativeGetCharacter(long ptr, int keyCode, int metaState);
+ private static native boolean nativeGetFallbackAction(long ptr, int keyCode, int metaState,
+ FallbackAction outFallbackAction);
+ private static native char nativeGetNumber(long ptr, int keyCode);
+ private static native char nativeGetMatch(long ptr, int keyCode, char[] chars, int metaState);
+ private static native char nativeGetDisplayLabel(long ptr, int keyCode);
+ private static native int nativeGetKeyboardType(long ptr);
+ private static native KeyEvent[] nativeGetEvents(long ptr, char[] chars);
+ private static native KeyCharacterMap nativeObtainEmptyKeyCharacterMap(int deviceId);
+ private static native boolean nativeEquals(long ptr1, long ptr2);
+
+ private KeyCharacterMap(Parcel in) {
+ if (in == null) {
+ throw new IllegalArgumentException("parcel must not be null");
+ }
+ mPtr = nativeReadFromParcel(in);
+ if (mPtr == 0) {
+ throw new RuntimeException("Could not read KeyCharacterMap from parcel.");
+ }
+ }
+
+ // Called from native
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private KeyCharacterMap(long ptr) {
+ mPtr = ptr;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mPtr != 0) {
+ nativeDispose(mPtr);
+ mPtr = 0;
+ }
+ }
+
+ /**
+ * Obtain empty key character map
+ * @param deviceId The input device ID
+ * @return The KeyCharacterMap object
+ * @hide
+ */
+ @VisibleForTesting
+ @Nullable
+ public static KeyCharacterMap obtainEmptyMap(int deviceId) {
+ return nativeObtainEmptyKeyCharacterMap(deviceId);
+ }
+
+ /**
+ * Loads the key character maps for the keyboard with the specified device id.
+ *
+ * @param deviceId The device id of the keyboard.
+ * @return The associated key character map.
+ * @throws {@link UnavailableException} if the key character map
+ * could not be loaded because it was malformed or the default key character map
+ * is missing from the system.
+ */
+ public static KeyCharacterMap load(int deviceId) {
+ final InputManager im = InputManager.getInstance();
+ InputDevice inputDevice = im.getInputDevice(deviceId);
+ if (inputDevice == null) {
+ inputDevice = im.getInputDevice(VIRTUAL_KEYBOARD);
+ if (inputDevice == null) {
+ throw new UnavailableException(
+ "Could not load key character map for device " + deviceId);
+ }
+ }
+ return inputDevice.getKeyCharacterMap();
+ }
+
+ /**
+ * Gets the Unicode character generated by the specified key and meta
+ * key state combination.
+ * <p>
+ * Returns the Unicode character that the specified key would produce
+ * when the specified meta bits (see {@link MetaKeyKeyListener})
+ * were active.
+ * </p><p>
+ * Returns 0 if the key is not one that is used to type Unicode
+ * characters.
+ * </p><p>
+ * If the return value has bit {@link #COMBINING_ACCENT} set, the
+ * key is a "dead key" that should be combined with another to
+ * actually produce a character -- see {@link #getDeadChar} --
+ * after masking with {@link #COMBINING_ACCENT_MASK}.
+ * </p>
+ *
+ * @param keyCode The key code.
+ * @param metaState The meta key modifier state.
+ * @return The associated character or combining accent, or 0 if none.
+ */
+ public int get(int keyCode, int metaState) {
+ metaState = KeyEvent.normalizeMetaState(metaState);
+ char ch = nativeGetCharacter(mPtr, keyCode, metaState);
+
+ int map = sCombiningToAccent.get(ch);
+ if (map != 0) {
+ return map | COMBINING_ACCENT;
+ } else {
+ return ch;
+ }
+ }
+
+ /**
+ * Gets the fallback action to perform if the application does not
+ * handle the specified key.
+ * <p>
+ * When an application does not handle a particular key, the system may
+ * translate the key to an alternate fallback key (specified in the
+ * fallback action) and dispatch it to the application.
+ * The event containing the fallback key is flagged
+ * with {@link KeyEvent#FLAG_FALLBACK}.
+ * </p>
+ *
+ * @param keyCode The key code.
+ * @param metaState The meta key modifier state.
+ * @return The fallback action, or null if none. Remember to recycle the fallback action.
+ *
+ * @hide
+ */
+ public FallbackAction getFallbackAction(int keyCode, int metaState) {
+ FallbackAction action = FallbackAction.obtain();
+ metaState = KeyEvent.normalizeMetaState(metaState);
+ if (nativeGetFallbackAction(mPtr, keyCode, metaState, action)) {
+ action.metaState = KeyEvent.normalizeMetaState(action.metaState);
+ return action;
+ }
+ action.recycle();
+ return null;
+ }
+
+ /**
+ * Gets the number or symbol associated with the key.
+ * <p>
+ * The character value is returned, not the numeric value.
+ * If the key is not a number, but is a symbol, the symbol is retuned.
+ * </p><p>
+ * This method is intended to to support dial pads and other numeric or
+ * symbolic entry on keyboards where certain keys serve dual function
+ * as alphabetic and symbolic keys. This method returns the number
+ * or symbol associated with the key independent of whether the user
+ * has pressed the required modifier.
+ * </p><p>
+ * For example, on one particular keyboard the keys on the top QWERTY row generate
+ * numbers when ALT is pressed such that ALT-Q maps to '1'. So for that keyboard
+ * when {@link #getNumber} is called with {@link KeyEvent#KEYCODE_Q} it returns '1'
+ * so that the user can type numbers without pressing ALT when it makes sense.
+ * </p>
+ *
+ * @param keyCode The key code.
+ * @return The associated numeric or symbolic character, or 0 if none.
+ */
+ public char getNumber(int keyCode) {
+ return nativeGetNumber(mPtr, keyCode);
+ }
+
+ /**
+ * Gets the first character in the character array that can be generated
+ * by the specified key code.
+ * <p>
+ * This is a convenience function that returns the same value as
+ * {@link #getMatch(int,char[],int) getMatch(keyCode, chars, 0)}.
+ * </p>
+ *
+ * @param keyCode The keycode.
+ * @param chars The array of matching characters to consider.
+ * @return The matching associated character, or 0 if none.
+ * @throws {@link IllegalArgumentException} if the passed array of characters is null.
+ */
+ public char getMatch(int keyCode, char[] chars) {
+ return getMatch(keyCode, chars, 0);
+ }
+
+ /**
+ * Gets the first character in the character array that can be generated
+ * by the specified key code. If there are multiple choices, prefers
+ * the one that would be generated with the specified meta key modifier state.
+ *
+ * @param keyCode The key code.
+ * @param chars The array of matching characters to consider.
+ * @param metaState The preferred meta key modifier state.
+ * @return The matching associated character, or 0 if none.
+ * @throws {@link IllegalArgumentException} if the passed array of characters is null.
+ */
+ public char getMatch(int keyCode, char[] chars, int metaState) {
+ if (chars == null) {
+ throw new IllegalArgumentException("chars must not be null.");
+ }
+
+ metaState = KeyEvent.normalizeMetaState(metaState);
+ return nativeGetMatch(mPtr, keyCode, chars, metaState);
+ }
+
+ /**
+ * Gets the primary character for this key.
+ * In other words, the label that is physically printed on it.
+ *
+ * @param keyCode The key code.
+ * @return The display label character, or 0 if none (eg. for non-printing keys).
+ */
+ public char getDisplayLabel(int keyCode) {
+ return nativeGetDisplayLabel(mPtr, keyCode);
+ }
+
+ /**
+ * Get the character that is produced by combining the dead key producing accent
+ * with the key producing character c.
+ * For example, getDeadChar('`', 'e') returns è.
+ * getDeadChar('^', ' ') returns '^' and getDeadChar('^', '^') returns '^'.
+ *
+ * @param accent The accent character. eg. '`'
+ * @param c The basic character.
+ * @return The combined character, or 0 if the characters cannot be combined.
+ */
+ public static int getDeadChar(int accent, int c) {
+ if (c == accent || CHAR_SPACE == c) {
+ // The same dead character typed twice or a dead character followed by a
+ // space should both produce the non-combining version of the combining char.
+ // In this case we don't even need to compute the combining character.
+ return accent;
+ }
+
+ int combining = sAccentToCombining.get(accent);
+ if (combining == 0) {
+ return 0;
+ }
+
+ final int combination = (combining << 16) | c;
+ int combined;
+ synchronized (sDeadKeyCache) {
+ combined = sDeadKeyCache.get(combination, -1);
+ if (combined == -1) {
+ sDeadKeyBuilder.setLength(0);
+ sDeadKeyBuilder.append((char)c);
+ sDeadKeyBuilder.append((char)combining);
+ String result = Normalizer.normalize(sDeadKeyBuilder, Normalizer.Form.NFC);
+ combined = result.codePointCount(0, result.length()) == 1
+ ? result.codePointAt(0) : 0;
+ sDeadKeyCache.put(combination, combined);
+ }
+ }
+ return combined;
+ }
+
+ /**
+ * Describes the character mappings associated with a key.
+ *
+ * @deprecated instead use {@link KeyCharacterMap#getDisplayLabel(int)},
+ * {@link KeyCharacterMap#getNumber(int)} and {@link KeyCharacterMap#get(int, int)}.
+ */
+ @Deprecated
+ public static class KeyData {
+ public static final int META_LENGTH = 4;
+
+ /**
+ * The display label (see {@link #getDisplayLabel}).
+ */
+ public char displayLabel;
+ /**
+ * The "number" value (see {@link #getNumber}).
+ */
+ public char number;
+ /**
+ * The character that will be generated in various meta states
+ * (the same ones used for {@link #get} and defined as
+ * {@link KeyEvent#META_SHIFT_ON} and {@link KeyEvent#META_ALT_ON}).
+ * <table>
+ * <tr><th>Index</th><th align="left">Value</th></tr>
+ * <tr><td>0</td><td>no modifiers</td></tr>
+ * <tr><td>1</td><td>caps</td></tr>
+ * <tr><td>2</td><td>alt</td></tr>
+ * <tr><td>3</td><td>caps + alt</td></tr>
+ * </table>
+ */
+ public char[] meta = new char[META_LENGTH];
+ }
+
+ /**
+ * Get the character conversion data for a given key code.
+ *
+ * @param keyCode The keyCode to query.
+ * @param results A {@link KeyData} instance that will be filled with the results.
+ * @return True if the key was mapped. If the key was not mapped, results is not modified.
+ *
+ * @deprecated instead use {@link KeyCharacterMap#getDisplayLabel(int)},
+ * {@link KeyCharacterMap#getNumber(int)} or {@link KeyCharacterMap#get(int, int)}.
+ */
+ @Deprecated
+ public boolean getKeyData(int keyCode, KeyData results) {
+ if (results.meta.length < KeyData.META_LENGTH) {
+ throw new IndexOutOfBoundsException(
+ "results.meta.length must be >= " + KeyData.META_LENGTH);
+ }
+
+ char displayLabel = nativeGetDisplayLabel(mPtr, keyCode);
+ if (displayLabel == 0) {
+ return false;
+ }
+
+ results.displayLabel = displayLabel;
+ results.number = nativeGetNumber(mPtr, keyCode);
+ results.meta[0] = nativeGetCharacter(mPtr, keyCode, 0);
+ results.meta[1] = nativeGetCharacter(mPtr, keyCode, KeyEvent.META_SHIFT_ON);
+ results.meta[2] = nativeGetCharacter(mPtr, keyCode, KeyEvent.META_ALT_ON);
+ results.meta[3] = nativeGetCharacter(mPtr, keyCode,
+ KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON);
+ return true;
+ }
+
+ /**
+ * Get an array of KeyEvent objects that if put into the input stream
+ * could plausibly generate the provided sequence of characters. It is
+ * not guaranteed that the sequence is the only way to generate these
+ * events or that it is optimal.
+ * <p>
+ * This function is primarily offered for instrumentation and testing purposes.
+ * It may fail to map characters to key codes. In particular, the key character
+ * map for the {@link #BUILT_IN_KEYBOARD built-in keyboard} device id may be empty.
+ * Consider using the key character map associated with the
+ * {@link #VIRTUAL_KEYBOARD virtual keyboard} device id instead.
+ * </p><p>
+ * For robust text entry, do not use this function. Instead construct a
+ * {@link KeyEvent} with action code {@link KeyEvent#ACTION_MULTIPLE} that contains
+ * the desired string using {@link KeyEvent#KeyEvent(long, String, int, int)}.
+ * </p>
+ *
+ * @param chars The sequence of characters to generate.
+ * @return An array of {@link KeyEvent} objects, or null if the given char array
+ * can not be generated using the current key character map.
+ * @throws {@link IllegalArgumentException} if the passed array of characters is null.
+ */
+ public KeyEvent[] getEvents(char[] chars) {
+ if (chars == null) {
+ throw new IllegalArgumentException("chars must not be null.");
+ }
+ return nativeGetEvents(mPtr, chars);
+ }
+
+ /**
+ * Returns true if the specified key produces a glyph.
+ *
+ * @param keyCode The key code.
+ * @return True if the key is a printing key.
+ */
+ public boolean isPrintingKey(int keyCode) {
+ int type = Character.getType(nativeGetDisplayLabel(mPtr, keyCode));
+
+ switch (type)
+ {
+ case Character.SPACE_SEPARATOR:
+ case Character.LINE_SEPARATOR:
+ case Character.PARAGRAPH_SEPARATOR:
+ case Character.CONTROL:
+ case Character.FORMAT:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Gets the keyboard type.
+ * Returns {@link #NUMERIC}, {@link #PREDICTIVE}, {@link #ALPHA}, {@link #FULL}
+ * or {@link #SPECIAL_FUNCTION}.
+ * <p>
+ * Different keyboard types have different semantics. Refer to the documentation
+ * associated with the keyboard type constants for details.
+ * </p>
+ *
+ * @return The keyboard type.
+ */
+ public int getKeyboardType() {
+ return nativeGetKeyboardType(mPtr);
+ }
+
+ /**
+ * Gets a constant that describes the behavior of this keyboard's modifier keys
+ * such as {@link KeyEvent#KEYCODE_SHIFT_LEFT}.
+ * <p>
+ * Currently there are two behaviors that may be combined:
+ * </p>
+ * <ul>
+ * <li>Chorded behavior: When the modifier key is pressed together with one or more
+ * character keys, the keyboard inserts the modified keys and
+ * then resets the modifier state when the modifier key is released.</li>
+ * <li>Toggled behavior: When the modifier key is pressed and released on its own
+ * it first toggles into a latched state. When latched, the modifier will apply
+ * to next character key that is pressed and will then reset itself to the initial state.
+ * If the modifier is already latched and the modifier key is pressed and release on
+ * its own again, then it toggles into a locked state. When locked, the modifier will
+ * apply to all subsequent character keys that are pressed until unlocked by pressing
+ * the modifier key on its own one more time to reset it to the initial state.
+ * Toggled behavior is useful for small profile keyboards designed for thumb typing.
+ * </ul>
+ * <p>
+ * This function currently returns {@link #MODIFIER_BEHAVIOR_CHORDED} when the
+ * {@link #getKeyboardType() keyboard type} is {@link #FULL} or {@link #SPECIAL_FUNCTION} and
+ * {@link #MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED} otherwise.
+ * In the future, the function may also take into account global keyboard
+ * accessibility settings, other user preferences, or new device capabilities.
+ * </p>
+ *
+ * @return The modifier behavior for this keyboard.
+ *
+ * @see #MODIFIER_BEHAVIOR_CHORDED
+ * @see #MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED
+ */
+ public int getModifierBehavior() {
+ switch (getKeyboardType()) {
+ case FULL:
+ case SPECIAL_FUNCTION:
+ return MODIFIER_BEHAVIOR_CHORDED;
+ default:
+ return MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED;
+ }
+ }
+
+ /**
+ * Queries the framework about whether any physical keys exist on the
+ * any keyboard attached to the device that are capable of producing the given key code.
+ *
+ * @param keyCode The key code to query.
+ * @return True if at least one attached keyboard supports the specified key code.
+ */
+ public static boolean deviceHasKey(int keyCode) {
+ return InputManager.getInstance().deviceHasKeys(new int[] { keyCode })[0];
+ }
+
+ /**
+ * Queries the framework about whether any physical keys exist on the
+ * any keyboard attached to the device that are capable of producing the given
+ * array of key codes.
+ *
+ * @param keyCodes The array of key codes to query.
+ * @return A new array of the same size as the key codes array whose elements
+ * are set to true if at least one attached keyboard supports the corresponding key code
+ * at the same index in the key codes array.
+ */
+ public static boolean[] deviceHasKeys(int[] keyCodes) {
+ return InputManager.getInstance().deviceHasKeys(keyCodes);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (out == null) {
+ throw new IllegalArgumentException("parcel must not be null");
+ }
+ nativeWriteToParcel(mPtr, out);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof KeyCharacterMap)) {
+ return false;
+ }
+ KeyCharacterMap peer = (KeyCharacterMap) obj;
+ if (mPtr == 0 || peer.mPtr == 0) {
+ return mPtr == peer.mPtr;
+ }
+ return nativeEquals(mPtr, peer.mPtr);
+ }
+
+ /**
+ * Thrown by {@link KeyCharacterMap#load} when a key character map could not be loaded.
+ */
+ public static class UnavailableException extends AndroidRuntimeException {
+ public UnavailableException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Specifies a substitute key code and meta state as a fallback action
+ * for an unhandled key.
+ * @hide
+ */
+ public static final class FallbackAction {
+ private static final int MAX_RECYCLED = 10;
+ private static final Object sRecycleLock = new Object();
+ private static FallbackAction sRecycleBin;
+ private static int sRecycledCount;
+
+ private FallbackAction next;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int keyCode;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int metaState;
+
+ private FallbackAction() {
+ }
+
+ public static FallbackAction obtain() {
+ final FallbackAction target;
+ synchronized (sRecycleLock) {
+ if (sRecycleBin == null) {
+ target = new FallbackAction();
+ } else {
+ target = sRecycleBin;
+ sRecycleBin = target.next;
+ sRecycledCount--;
+ target.next = null;
+ }
+ }
+ return target;
+ }
+
+ public void recycle() {
+ synchronized (sRecycleLock) {
+ if (sRecycledCount < MAX_RECYCLED) {
+ next = sRecycleBin;
+ sRecycleBin = this;
+ sRecycledCount += 1;
+ } else {
+ next = null;
+ }
+ }
+ }
+ }
+}
diff --git a/android/view/KeyEvent.java b/android/view/KeyEvent.java
new file mode 100644
index 0000000..cda9b23
--- /dev/null
+++ b/android/view/KeyEvent.java
@@ -0,0 +1,3178 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.IInputConstants.INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.method.MetaKeyKeyListener;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.KeyCharacterMap.KeyData;
+
+/**
+ * Object used to report key and button events.
+ * <p>
+ * Each key press is described by a sequence of key events. A key press
+ * starts with a key event with {@link #ACTION_DOWN}. If the key is held
+ * sufficiently long that it repeats, then the initial down is followed
+ * additional key events with {@link #ACTION_DOWN} and a non-zero value for
+ * {@link #getRepeatCount()}. The last key event is a {@link #ACTION_UP}
+ * for the key up. If the key press is canceled, the key up event will have the
+ * {@link #FLAG_CANCELED} flag set.
+ * </p><p>
+ * Key events are generally accompanied by a key code ({@link #getKeyCode()}),
+ * scan code ({@link #getScanCode()}) and meta state ({@link #getMetaState()}).
+ * Key code constants are defined in this class. Scan code constants are raw
+ * device-specific codes obtained from the OS and so are not generally meaningful
+ * to applications unless interpreted using the {@link KeyCharacterMap}.
+ * Meta states describe the pressed state of key modifiers
+ * such as {@link #META_SHIFT_ON} or {@link #META_ALT_ON}.
+ * </p><p>
+ * Key codes typically correspond one-to-one with individual keys on an input device.
+ * Many keys and key combinations serve quite different functions on different
+ * input devices so care must be taken when interpreting them. Always use the
+ * {@link KeyCharacterMap} associated with the input device when mapping keys
+ * to characters. Be aware that there may be multiple key input devices active
+ * at the same time and each will have its own key character map.
+ * </p><p>
+ * As soft input methods can use multiple and inventive ways of inputting text,
+ * there is no guarantee that any key press on a soft keyboard will generate a key
+ * event: this is left to the IME's discretion, and in fact sending such events is
+ * discouraged. You should never rely on receiving KeyEvents for any key on a soft
+ * input method. In particular, the default software keyboard will never send any
+ * key event to any application targetting Jelly Bean or later, and will only send
+ * events for some presses of the delete and return keys to applications targetting
+ * Ice Cream Sandwich or earlier. Be aware that other software input methods may
+ * never send key events regardless of the version. Consider using editor actions
+ * like {@link android.view.inputmethod.EditorInfo#IME_ACTION_DONE} if you need
+ * specific interaction with the software keyboard, as it gives more visibility to
+ * the user as to how your application will react to key presses.
+ * </p><p>
+ * When interacting with an IME, the framework may deliver key events
+ * with the special action {@link #ACTION_MULTIPLE} that either specifies
+ * that single repeated key code or a sequence of characters to insert.
+ * </p><p>
+ * In general, the framework cannot guarantee that the key events it delivers
+ * to a view always constitute complete key sequences since some events may be dropped
+ * or modified by containing views before they are delivered. The view implementation
+ * should be prepared to handle {@link #FLAG_CANCELED} and should tolerate anomalous
+ * situations such as receiving a new {@link #ACTION_DOWN} without first having
+ * received an {@link #ACTION_UP} for the prior key press.
+ * </p><p>
+ * Refer to {@link InputDevice} for more information about how different kinds of
+ * input devices and sources represent keys and buttons.
+ * </p>
+ */
+public class KeyEvent extends InputEvent implements Parcelable {
+ /** Key code constant: Unknown key code. */
+ public static final int KEYCODE_UNKNOWN = 0;
+ /** Key code constant: Soft Left key.
+ * Usually situated below the display on phones and used as a multi-function
+ * feature key for selecting a software defined function shown on the bottom left
+ * of the display. */
+ public static final int KEYCODE_SOFT_LEFT = 1;
+ /** Key code constant: Soft Right key.
+ * Usually situated below the display on phones and used as a multi-function
+ * feature key for selecting a software defined function shown on the bottom right
+ * of the display. */
+ public static final int KEYCODE_SOFT_RIGHT = 2;
+ /** Key code constant: Home key.
+ * This key is handled by the framework and is never delivered to applications. */
+ public static final int KEYCODE_HOME = 3;
+ /** Key code constant: Back key. */
+ public static final int KEYCODE_BACK = 4;
+ /** Key code constant: Call key. */
+ public static final int KEYCODE_CALL = 5;
+ /** Key code constant: End Call key. */
+ public static final int KEYCODE_ENDCALL = 6;
+ /** Key code constant: '0' key. */
+ public static final int KEYCODE_0 = 7;
+ /** Key code constant: '1' key. */
+ public static final int KEYCODE_1 = 8;
+ /** Key code constant: '2' key. */
+ public static final int KEYCODE_2 = 9;
+ /** Key code constant: '3' key. */
+ public static final int KEYCODE_3 = 10;
+ /** Key code constant: '4' key. */
+ public static final int KEYCODE_4 = 11;
+ /** Key code constant: '5' key. */
+ public static final int KEYCODE_5 = 12;
+ /** Key code constant: '6' key. */
+ public static final int KEYCODE_6 = 13;
+ /** Key code constant: '7' key. */
+ public static final int KEYCODE_7 = 14;
+ /** Key code constant: '8' key. */
+ public static final int KEYCODE_8 = 15;
+ /** Key code constant: '9' key. */
+ public static final int KEYCODE_9 = 16;
+ /** Key code constant: '*' key. */
+ public static final int KEYCODE_STAR = 17;
+ /** Key code constant: '#' key. */
+ public static final int KEYCODE_POUND = 18;
+ /** Key code constant: Directional Pad Up key.
+ * May also be synthesized from trackball motions. */
+ public static final int KEYCODE_DPAD_UP = 19;
+ /** Key code constant: Directional Pad Down key.
+ * May also be synthesized from trackball motions. */
+ public static final int KEYCODE_DPAD_DOWN = 20;
+ /** Key code constant: Directional Pad Left key.
+ * May also be synthesized from trackball motions. */
+ public static final int KEYCODE_DPAD_LEFT = 21;
+ /** Key code constant: Directional Pad Right key.
+ * May also be synthesized from trackball motions. */
+ public static final int KEYCODE_DPAD_RIGHT = 22;
+ /** Key code constant: Directional Pad Center key.
+ * May also be synthesized from trackball motions. */
+ public static final int KEYCODE_DPAD_CENTER = 23;
+ /** Key code constant: Volume Up key.
+ * Adjusts the speaker volume up. */
+ public static final int KEYCODE_VOLUME_UP = 24;
+ /** Key code constant: Volume Down key.
+ * Adjusts the speaker volume down. */
+ public static final int KEYCODE_VOLUME_DOWN = 25;
+ /** Key code constant: Power key. */
+ public static final int KEYCODE_POWER = 26;
+ /** Key code constant: Camera key.
+ * Used to launch a camera application or take pictures. */
+ public static final int KEYCODE_CAMERA = 27;
+ /** Key code constant: Clear key. */
+ public static final int KEYCODE_CLEAR = 28;
+ /** Key code constant: 'A' key. */
+ public static final int KEYCODE_A = 29;
+ /** Key code constant: 'B' key. */
+ public static final int KEYCODE_B = 30;
+ /** Key code constant: 'C' key. */
+ public static final int KEYCODE_C = 31;
+ /** Key code constant: 'D' key. */
+ public static final int KEYCODE_D = 32;
+ /** Key code constant: 'E' key. */
+ public static final int KEYCODE_E = 33;
+ /** Key code constant: 'F' key. */
+ public static final int KEYCODE_F = 34;
+ /** Key code constant: 'G' key. */
+ public static final int KEYCODE_G = 35;
+ /** Key code constant: 'H' key. */
+ public static final int KEYCODE_H = 36;
+ /** Key code constant: 'I' key. */
+ public static final int KEYCODE_I = 37;
+ /** Key code constant: 'J' key. */
+ public static final int KEYCODE_J = 38;
+ /** Key code constant: 'K' key. */
+ public static final int KEYCODE_K = 39;
+ /** Key code constant: 'L' key. */
+ public static final int KEYCODE_L = 40;
+ /** Key code constant: 'M' key. */
+ public static final int KEYCODE_M = 41;
+ /** Key code constant: 'N' key. */
+ public static final int KEYCODE_N = 42;
+ /** Key code constant: 'O' key. */
+ public static final int KEYCODE_O = 43;
+ /** Key code constant: 'P' key. */
+ public static final int KEYCODE_P = 44;
+ /** Key code constant: 'Q' key. */
+ public static final int KEYCODE_Q = 45;
+ /** Key code constant: 'R' key. */
+ public static final int KEYCODE_R = 46;
+ /** Key code constant: 'S' key. */
+ public static final int KEYCODE_S = 47;
+ /** Key code constant: 'T' key. */
+ public static final int KEYCODE_T = 48;
+ /** Key code constant: 'U' key. */
+ public static final int KEYCODE_U = 49;
+ /** Key code constant: 'V' key. */
+ public static final int KEYCODE_V = 50;
+ /** Key code constant: 'W' key. */
+ public static final int KEYCODE_W = 51;
+ /** Key code constant: 'X' key. */
+ public static final int KEYCODE_X = 52;
+ /** Key code constant: 'Y' key. */
+ public static final int KEYCODE_Y = 53;
+ /** Key code constant: 'Z' key. */
+ public static final int KEYCODE_Z = 54;
+ /** Key code constant: ',' key. */
+ public static final int KEYCODE_COMMA = 55;
+ /** Key code constant: '.' key. */
+ public static final int KEYCODE_PERIOD = 56;
+ /** Key code constant: Left Alt modifier key. */
+ public static final int KEYCODE_ALT_LEFT = 57;
+ /** Key code constant: Right Alt modifier key. */
+ public static final int KEYCODE_ALT_RIGHT = 58;
+ /** Key code constant: Left Shift modifier key. */
+ public static final int KEYCODE_SHIFT_LEFT = 59;
+ /** Key code constant: Right Shift modifier key. */
+ public static final int KEYCODE_SHIFT_RIGHT = 60;
+ /** Key code constant: Tab key. */
+ public static final int KEYCODE_TAB = 61;
+ /** Key code constant: Space key. */
+ public static final int KEYCODE_SPACE = 62;
+ /** Key code constant: Symbol modifier key.
+ * Used to enter alternate symbols. */
+ public static final int KEYCODE_SYM = 63;
+ /** Key code constant: Explorer special function key.
+ * Used to launch a browser application. */
+ public static final int KEYCODE_EXPLORER = 64;
+ /** Key code constant: Envelope special function key.
+ * Used to launch a mail application. */
+ public static final int KEYCODE_ENVELOPE = 65;
+ /** Key code constant: Enter key. */
+ public static final int KEYCODE_ENTER = 66;
+ /** Key code constant: Backspace key.
+ * Deletes characters before the insertion point, unlike {@link #KEYCODE_FORWARD_DEL}. */
+ public static final int KEYCODE_DEL = 67;
+ /** Key code constant: '`' (backtick) key. */
+ public static final int KEYCODE_GRAVE = 68;
+ /** Key code constant: '-'. */
+ public static final int KEYCODE_MINUS = 69;
+ /** Key code constant: '=' key. */
+ public static final int KEYCODE_EQUALS = 70;
+ /** Key code constant: '[' key. */
+ public static final int KEYCODE_LEFT_BRACKET = 71;
+ /** Key code constant: ']' key. */
+ public static final int KEYCODE_RIGHT_BRACKET = 72;
+ /** Key code constant: '\' key. */
+ public static final int KEYCODE_BACKSLASH = 73;
+ /** Key code constant: ';' key. */
+ public static final int KEYCODE_SEMICOLON = 74;
+ /** Key code constant: ''' (apostrophe) key. */
+ public static final int KEYCODE_APOSTROPHE = 75;
+ /** Key code constant: '/' key. */
+ public static final int KEYCODE_SLASH = 76;
+ /** Key code constant: '@' key. */
+ public static final int KEYCODE_AT = 77;
+ /** Key code constant: Number modifier key.
+ * Used to enter numeric symbols.
+ * This key is not Num Lock; it is more like {@link #KEYCODE_ALT_LEFT} and is
+ * interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener}. */
+ public static final int KEYCODE_NUM = 78;
+ /** Key code constant: Headset Hook key.
+ * Used to hang up calls and stop media. */
+ public static final int KEYCODE_HEADSETHOOK = 79;
+ /** Key code constant: Camera Focus key.
+ * Used to focus the camera. */
+ public static final int KEYCODE_FOCUS = 80; // *Camera* focus
+ /** Key code constant: '+' key. */
+ public static final int KEYCODE_PLUS = 81;
+ /** Key code constant: Menu key. */
+ public static final int KEYCODE_MENU = 82;
+ /** Key code constant: Notification key. */
+ public static final int KEYCODE_NOTIFICATION = 83;
+ /** Key code constant: Search key. */
+ public static final int KEYCODE_SEARCH = 84;
+ /** Key code constant: Play/Pause media key. */
+ public static final int KEYCODE_MEDIA_PLAY_PAUSE= 85;
+ /** Key code constant: Stop media key. */
+ public static final int KEYCODE_MEDIA_STOP = 86;
+ /** Key code constant: Play Next media key. */
+ public static final int KEYCODE_MEDIA_NEXT = 87;
+ /** Key code constant: Play Previous media key. */
+ public static final int KEYCODE_MEDIA_PREVIOUS = 88;
+ /** Key code constant: Rewind media key. */
+ public static final int KEYCODE_MEDIA_REWIND = 89;
+ /** Key code constant: Fast Forward media key. */
+ public static final int KEYCODE_MEDIA_FAST_FORWARD = 90;
+ /** Key code constant: Mute key.
+ * Mutes the microphone, unlike {@link #KEYCODE_VOLUME_MUTE}. */
+ public static final int KEYCODE_MUTE = 91;
+ /** Key code constant: Page Up key. */
+ public static final int KEYCODE_PAGE_UP = 92;
+ /** Key code constant: Page Down key. */
+ public static final int KEYCODE_PAGE_DOWN = 93;
+ /** Key code constant: Picture Symbols modifier key.
+ * Used to switch symbol sets (Emoji, Kao-moji). */
+ public static final int KEYCODE_PICTSYMBOLS = 94; // switch symbol-sets (Emoji,Kao-moji)
+ /** Key code constant: Switch Charset modifier key.
+ * Used to switch character sets (Kanji, Katakana). */
+ public static final int KEYCODE_SWITCH_CHARSET = 95; // switch char-sets (Kanji,Katakana)
+ /** Key code constant: A Button key.
+ * On a game controller, the A button should be either the button labeled A
+ * or the first button on the bottom row of controller buttons. */
+ public static final int KEYCODE_BUTTON_A = 96;
+ /** Key code constant: B Button key.
+ * On a game controller, the B button should be either the button labeled B
+ * or the second button on the bottom row of controller buttons. */
+ public static final int KEYCODE_BUTTON_B = 97;
+ /** Key code constant: C Button key.
+ * On a game controller, the C button should be either the button labeled C
+ * or the third button on the bottom row of controller buttons. */
+ public static final int KEYCODE_BUTTON_C = 98;
+ /** Key code constant: X Button key.
+ * On a game controller, the X button should be either the button labeled X
+ * or the first button on the upper row of controller buttons. */
+ public static final int KEYCODE_BUTTON_X = 99;
+ /** Key code constant: Y Button key.
+ * On a game controller, the Y button should be either the button labeled Y
+ * or the second button on the upper row of controller buttons. */
+ public static final int KEYCODE_BUTTON_Y = 100;
+ /** Key code constant: Z Button key.
+ * On a game controller, the Z button should be either the button labeled Z
+ * or the third button on the upper row of controller buttons. */
+ public static final int KEYCODE_BUTTON_Z = 101;
+ /** Key code constant: L1 Button key.
+ * On a game controller, the L1 button should be either the button labeled L1 (or L)
+ * or the top left trigger button. */
+ public static final int KEYCODE_BUTTON_L1 = 102;
+ /** Key code constant: R1 Button key.
+ * On a game controller, the R1 button should be either the button labeled R1 (or R)
+ * or the top right trigger button. */
+ public static final int KEYCODE_BUTTON_R1 = 103;
+ /** Key code constant: L2 Button key.
+ * On a game controller, the L2 button should be either the button labeled L2
+ * or the bottom left trigger button. */
+ public static final int KEYCODE_BUTTON_L2 = 104;
+ /** Key code constant: R2 Button key.
+ * On a game controller, the R2 button should be either the button labeled R2
+ * or the bottom right trigger button. */
+ public static final int KEYCODE_BUTTON_R2 = 105;
+ /** Key code constant: Left Thumb Button key.
+ * On a game controller, the left thumb button indicates that the left (or only)
+ * joystick is pressed. */
+ public static final int KEYCODE_BUTTON_THUMBL = 106;
+ /** Key code constant: Right Thumb Button key.
+ * On a game controller, the right thumb button indicates that the right
+ * joystick is pressed. */
+ public static final int KEYCODE_BUTTON_THUMBR = 107;
+ /** Key code constant: Start Button key.
+ * On a game controller, the button labeled Start. */
+ public static final int KEYCODE_BUTTON_START = 108;
+ /** Key code constant: Select Button key.
+ * On a game controller, the button labeled Select. */
+ public static final int KEYCODE_BUTTON_SELECT = 109;
+ /** Key code constant: Mode Button key.
+ * On a game controller, the button labeled Mode. */
+ public static final int KEYCODE_BUTTON_MODE = 110;
+ /** Key code constant: Escape key. */
+ public static final int KEYCODE_ESCAPE = 111;
+ /** Key code constant: Forward Delete key.
+ * Deletes characters ahead of the insertion point, unlike {@link #KEYCODE_DEL}. */
+ public static final int KEYCODE_FORWARD_DEL = 112;
+ /** Key code constant: Left Control modifier key. */
+ public static final int KEYCODE_CTRL_LEFT = 113;
+ /** Key code constant: Right Control modifier key. */
+ public static final int KEYCODE_CTRL_RIGHT = 114;
+ /** Key code constant: Caps Lock key. */
+ public static final int KEYCODE_CAPS_LOCK = 115;
+ /** Key code constant: Scroll Lock key. */
+ public static final int KEYCODE_SCROLL_LOCK = 116;
+ /** Key code constant: Left Meta modifier key. */
+ public static final int KEYCODE_META_LEFT = 117;
+ /** Key code constant: Right Meta modifier key. */
+ public static final int KEYCODE_META_RIGHT = 118;
+ /** Key code constant: Function modifier key. */
+ public static final int KEYCODE_FUNCTION = 119;
+ /** Key code constant: System Request / Print Screen key. */
+ public static final int KEYCODE_SYSRQ = 120;
+ /** Key code constant: Break / Pause key. */
+ public static final int KEYCODE_BREAK = 121;
+ /** Key code constant: Home Movement key.
+ * Used for scrolling or moving the cursor around to the start of a line
+ * or to the top of a list. */
+ public static final int KEYCODE_MOVE_HOME = 122;
+ /** Key code constant: End Movement key.
+ * Used for scrolling or moving the cursor around to the end of a line
+ * or to the bottom of a list. */
+ public static final int KEYCODE_MOVE_END = 123;
+ /** Key code constant: Insert key.
+ * Toggles insert / overwrite edit mode. */
+ public static final int KEYCODE_INSERT = 124;
+ /** Key code constant: Forward key.
+ * Navigates forward in the history stack. Complement of {@link #KEYCODE_BACK}. */
+ public static final int KEYCODE_FORWARD = 125;
+ /** Key code constant: Play media key. */
+ public static final int KEYCODE_MEDIA_PLAY = 126;
+ /** Key code constant: Pause media key. */
+ public static final int KEYCODE_MEDIA_PAUSE = 127;
+ /** Key code constant: Close media key.
+ * May be used to close a CD tray, for example. */
+ public static final int KEYCODE_MEDIA_CLOSE = 128;
+ /** Key code constant: Eject media key.
+ * May be used to eject a CD tray, for example. */
+ public static final int KEYCODE_MEDIA_EJECT = 129;
+ /** Key code constant: Record media key. */
+ public static final int KEYCODE_MEDIA_RECORD = 130;
+ /** Key code constant: F1 key. */
+ public static final int KEYCODE_F1 = 131;
+ /** Key code constant: F2 key. */
+ public static final int KEYCODE_F2 = 132;
+ /** Key code constant: F3 key. */
+ public static final int KEYCODE_F3 = 133;
+ /** Key code constant: F4 key. */
+ public static final int KEYCODE_F4 = 134;
+ /** Key code constant: F5 key. */
+ public static final int KEYCODE_F5 = 135;
+ /** Key code constant: F6 key. */
+ public static final int KEYCODE_F6 = 136;
+ /** Key code constant: F7 key. */
+ public static final int KEYCODE_F7 = 137;
+ /** Key code constant: F8 key. */
+ public static final int KEYCODE_F8 = 138;
+ /** Key code constant: F9 key. */
+ public static final int KEYCODE_F9 = 139;
+ /** Key code constant: F10 key. */
+ public static final int KEYCODE_F10 = 140;
+ /** Key code constant: F11 key. */
+ public static final int KEYCODE_F11 = 141;
+ /** Key code constant: F12 key. */
+ public static final int KEYCODE_F12 = 142;
+ /** Key code constant: Num Lock key.
+ * This is the Num Lock key; it is different from {@link #KEYCODE_NUM}.
+ * This key alters the behavior of other keys on the numeric keypad. */
+ public static final int KEYCODE_NUM_LOCK = 143;
+ /** Key code constant: Numeric keypad '0' key. */
+ public static final int KEYCODE_NUMPAD_0 = 144;
+ /** Key code constant: Numeric keypad '1' key. */
+ public static final int KEYCODE_NUMPAD_1 = 145;
+ /** Key code constant: Numeric keypad '2' key. */
+ public static final int KEYCODE_NUMPAD_2 = 146;
+ /** Key code constant: Numeric keypad '3' key. */
+ public static final int KEYCODE_NUMPAD_3 = 147;
+ /** Key code constant: Numeric keypad '4' key. */
+ public static final int KEYCODE_NUMPAD_4 = 148;
+ /** Key code constant: Numeric keypad '5' key. */
+ public static final int KEYCODE_NUMPAD_5 = 149;
+ /** Key code constant: Numeric keypad '6' key. */
+ public static final int KEYCODE_NUMPAD_6 = 150;
+ /** Key code constant: Numeric keypad '7' key. */
+ public static final int KEYCODE_NUMPAD_7 = 151;
+ /** Key code constant: Numeric keypad '8' key. */
+ public static final int KEYCODE_NUMPAD_8 = 152;
+ /** Key code constant: Numeric keypad '9' key. */
+ public static final int KEYCODE_NUMPAD_9 = 153;
+ /** Key code constant: Numeric keypad '/' key (for division). */
+ public static final int KEYCODE_NUMPAD_DIVIDE = 154;
+ /** Key code constant: Numeric keypad '*' key (for multiplication). */
+ public static final int KEYCODE_NUMPAD_MULTIPLY = 155;
+ /** Key code constant: Numeric keypad '-' key (for subtraction). */
+ public static final int KEYCODE_NUMPAD_SUBTRACT = 156;
+ /** Key code constant: Numeric keypad '+' key (for addition). */
+ public static final int KEYCODE_NUMPAD_ADD = 157;
+ /** Key code constant: Numeric keypad '.' key (for decimals or digit grouping). */
+ public static final int KEYCODE_NUMPAD_DOT = 158;
+ /** Key code constant: Numeric keypad ',' key (for decimals or digit grouping). */
+ public static final int KEYCODE_NUMPAD_COMMA = 159;
+ /** Key code constant: Numeric keypad Enter key. */
+ public static final int KEYCODE_NUMPAD_ENTER = 160;
+ /** Key code constant: Numeric keypad '=' key. */
+ public static final int KEYCODE_NUMPAD_EQUALS = 161;
+ /** Key code constant: Numeric keypad '(' key. */
+ public static final int KEYCODE_NUMPAD_LEFT_PAREN = 162;
+ /** Key code constant: Numeric keypad ')' key. */
+ public static final int KEYCODE_NUMPAD_RIGHT_PAREN = 163;
+ /** Key code constant: Volume Mute key.
+ * Mutes the speaker, unlike {@link #KEYCODE_MUTE}.
+ * This key should normally be implemented as a toggle such that the first press
+ * mutes the speaker and the second press restores the original volume. */
+ public static final int KEYCODE_VOLUME_MUTE = 164;
+ /** Key code constant: Info key.
+ * Common on TV remotes to show additional information related to what is
+ * currently being viewed. */
+ public static final int KEYCODE_INFO = 165;
+ /** Key code constant: Channel up key.
+ * On TV remotes, increments the television channel. */
+ public static final int KEYCODE_CHANNEL_UP = 166;
+ /** Key code constant: Channel down key.
+ * On TV remotes, decrements the television channel. */
+ public static final int KEYCODE_CHANNEL_DOWN = 167;
+ /** Key code constant: Zoom in key. */
+ public static final int KEYCODE_ZOOM_IN = 168;
+ /** Key code constant: Zoom out key. */
+ public static final int KEYCODE_ZOOM_OUT = 169;
+ /** Key code constant: TV key.
+ * On TV remotes, switches to viewing live TV. */
+ public static final int KEYCODE_TV = 170;
+ /** Key code constant: Window key.
+ * On TV remotes, toggles picture-in-picture mode or other windowing functions.
+ * On Android Wear devices, triggers a display offset. */
+ public static final int KEYCODE_WINDOW = 171;
+ /** Key code constant: Guide key.
+ * On TV remotes, shows a programming guide. */
+ public static final int KEYCODE_GUIDE = 172;
+ /** Key code constant: DVR key.
+ * On some TV remotes, switches to a DVR mode for recorded shows. */
+ public static final int KEYCODE_DVR = 173;
+ /** Key code constant: Bookmark key.
+ * On some TV remotes, bookmarks content or web pages. */
+ public static final int KEYCODE_BOOKMARK = 174;
+ /** Key code constant: Toggle captions key.
+ * Switches the mode for closed-captioning text, for example during television shows. */
+ public static final int KEYCODE_CAPTIONS = 175;
+ /** Key code constant: Settings key.
+ * Starts the system settings activity. */
+ public static final int KEYCODE_SETTINGS = 176;
+ /**
+ * Key code constant: TV power key.
+ * On HDMI TV panel devices and Android TV devices that don't support HDMI, toggles the power
+ * state of the device.
+ * On HDMI source devices, toggles the power state of the HDMI-connected TV via HDMI-CEC and
+ * makes the source device follow this power state.
+ */
+ public static final int KEYCODE_TV_POWER = 177;
+ /** Key code constant: TV input key.
+ * On TV remotes, switches the input on a television screen. */
+ public static final int KEYCODE_TV_INPUT = 178;
+ /** Key code constant: Set-top-box power key.
+ * On TV remotes, toggles the power on an external Set-top-box. */
+ public static final int KEYCODE_STB_POWER = 179;
+ /** Key code constant: Set-top-box input key.
+ * On TV remotes, switches the input mode on an external Set-top-box. */
+ public static final int KEYCODE_STB_INPUT = 180;
+ /** Key code constant: A/V Receiver power key.
+ * On TV remotes, toggles the power on an external A/V Receiver. */
+ public static final int KEYCODE_AVR_POWER = 181;
+ /** Key code constant: A/V Receiver input key.
+ * On TV remotes, switches the input mode on an external A/V Receiver. */
+ public static final int KEYCODE_AVR_INPUT = 182;
+ /** Key code constant: Red "programmable" key.
+ * On TV remotes, acts as a contextual/programmable key. */
+ public static final int KEYCODE_PROG_RED = 183;
+ /** Key code constant: Green "programmable" key.
+ * On TV remotes, actsas a contextual/programmable key. */
+ public static final int KEYCODE_PROG_GREEN = 184;
+ /** Key code constant: Yellow "programmable" key.
+ * On TV remotes, acts as a contextual/programmable key. */
+ public static final int KEYCODE_PROG_YELLOW = 185;
+ /** Key code constant: Blue "programmable" key.
+ * On TV remotes, acts as a contextual/programmable key. */
+ public static final int KEYCODE_PROG_BLUE = 186;
+ /** Key code constant: App switch key.
+ * Should bring up the application switcher dialog. */
+ public static final int KEYCODE_APP_SWITCH = 187;
+ /** Key code constant: Generic Game Pad Button #1.*/
+ public static final int KEYCODE_BUTTON_1 = 188;
+ /** Key code constant: Generic Game Pad Button #2.*/
+ public static final int KEYCODE_BUTTON_2 = 189;
+ /** Key code constant: Generic Game Pad Button #3.*/
+ public static final int KEYCODE_BUTTON_3 = 190;
+ /** Key code constant: Generic Game Pad Button #4.*/
+ public static final int KEYCODE_BUTTON_4 = 191;
+ /** Key code constant: Generic Game Pad Button #5.*/
+ public static final int KEYCODE_BUTTON_5 = 192;
+ /** Key code constant: Generic Game Pad Button #6.*/
+ public static final int KEYCODE_BUTTON_6 = 193;
+ /** Key code constant: Generic Game Pad Button #7.*/
+ public static final int KEYCODE_BUTTON_7 = 194;
+ /** Key code constant: Generic Game Pad Button #8.*/
+ public static final int KEYCODE_BUTTON_8 = 195;
+ /** Key code constant: Generic Game Pad Button #9.*/
+ public static final int KEYCODE_BUTTON_9 = 196;
+ /** Key code constant: Generic Game Pad Button #10.*/
+ public static final int KEYCODE_BUTTON_10 = 197;
+ /** Key code constant: Generic Game Pad Button #11.*/
+ public static final int KEYCODE_BUTTON_11 = 198;
+ /** Key code constant: Generic Game Pad Button #12.*/
+ public static final int KEYCODE_BUTTON_12 = 199;
+ /** Key code constant: Generic Game Pad Button #13.*/
+ public static final int KEYCODE_BUTTON_13 = 200;
+ /** Key code constant: Generic Game Pad Button #14.*/
+ public static final int KEYCODE_BUTTON_14 = 201;
+ /** Key code constant: Generic Game Pad Button #15.*/
+ public static final int KEYCODE_BUTTON_15 = 202;
+ /** Key code constant: Generic Game Pad Button #16.*/
+ public static final int KEYCODE_BUTTON_16 = 203;
+ /** Key code constant: Language Switch key.
+ * Toggles the current input language such as switching between English and Japanese on
+ * a QWERTY keyboard. On some devices, the same function may be performed by
+ * pressing Shift+Spacebar. */
+ public static final int KEYCODE_LANGUAGE_SWITCH = 204;
+ /** Key code constant: Manner Mode key.
+ * Toggles silent or vibrate mode on and off to make the device behave more politely
+ * in certain settings such as on a crowded train. On some devices, the key may only
+ * operate when long-pressed. */
+ public static final int KEYCODE_MANNER_MODE = 205;
+ /** Key code constant: 3D Mode key.
+ * Toggles the display between 2D and 3D mode. */
+ public static final int KEYCODE_3D_MODE = 206;
+ /** Key code constant: Contacts special function key.
+ * Used to launch an address book application. */
+ public static final int KEYCODE_CONTACTS = 207;
+ /** Key code constant: Calendar special function key.
+ * Used to launch a calendar application. */
+ public static final int KEYCODE_CALENDAR = 208;
+ /** Key code constant: Music special function key.
+ * Used to launch a music player application. */
+ public static final int KEYCODE_MUSIC = 209;
+ /** Key code constant: Calculator special function key.
+ * Used to launch a calculator application. */
+ public static final int KEYCODE_CALCULATOR = 210;
+ /** Key code constant: Japanese full-width / half-width key. */
+ public static final int KEYCODE_ZENKAKU_HANKAKU = 211;
+ /** Key code constant: Japanese alphanumeric key. */
+ public static final int KEYCODE_EISU = 212;
+ /** Key code constant: Japanese non-conversion key. */
+ public static final int KEYCODE_MUHENKAN = 213;
+ /** Key code constant: Japanese conversion key. */
+ public static final int KEYCODE_HENKAN = 214;
+ /** Key code constant: Japanese katakana / hiragana key. */
+ public static final int KEYCODE_KATAKANA_HIRAGANA = 215;
+ /** Key code constant: Japanese Yen key. */
+ public static final int KEYCODE_YEN = 216;
+ /** Key code constant: Japanese Ro key. */
+ public static final int KEYCODE_RO = 217;
+ /** Key code constant: Japanese kana key. */
+ public static final int KEYCODE_KANA = 218;
+ /** Key code constant: Assist key.
+ * Launches the global assist activity. Not delivered to applications. */
+ public static final int KEYCODE_ASSIST = 219;
+ /** Key code constant: Brightness Down key.
+ * Adjusts the screen brightness down. */
+ public static final int KEYCODE_BRIGHTNESS_DOWN = 220;
+ /** Key code constant: Brightness Up key.
+ * Adjusts the screen brightness up. */
+ public static final int KEYCODE_BRIGHTNESS_UP = 221;
+ /** Key code constant: Audio Track key.
+ * Switches the audio tracks. */
+ public static final int KEYCODE_MEDIA_AUDIO_TRACK = 222;
+ /** Key code constant: Sleep key.
+ * Puts the device to sleep. Behaves somewhat like {@link #KEYCODE_POWER} but it
+ * has no effect if the device is already asleep. */
+ public static final int KEYCODE_SLEEP = 223;
+ /** Key code constant: Wakeup key.
+ * Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it
+ * has no effect if the device is already awake. */
+ public static final int KEYCODE_WAKEUP = 224;
+ /** Key code constant: Pairing key.
+ * Initiates peripheral pairing mode. Useful for pairing remote control
+ * devices or game controllers, especially if no other input mode is
+ * available. */
+ public static final int KEYCODE_PAIRING = 225;
+ /** Key code constant: Media Top Menu key.
+ * Goes to the top of media menu. */
+ public static final int KEYCODE_MEDIA_TOP_MENU = 226;
+ /** Key code constant: '11' key. */
+ public static final int KEYCODE_11 = 227;
+ /** Key code constant: '12' key. */
+ public static final int KEYCODE_12 = 228;
+ /** Key code constant: Last Channel key.
+ * Goes to the last viewed channel. */
+ public static final int KEYCODE_LAST_CHANNEL = 229;
+ /** Key code constant: TV data service key.
+ * Displays data services like weather, sports. */
+ public static final int KEYCODE_TV_DATA_SERVICE = 230;
+ /** Key code constant: Voice Assist key.
+ * Launches the global voice assist activity. Not delivered to applications. */
+ public static final int KEYCODE_VOICE_ASSIST = 231;
+ /** Key code constant: Radio key.
+ * Toggles TV service / Radio service. */
+ public static final int KEYCODE_TV_RADIO_SERVICE = 232;
+ /** Key code constant: Teletext key.
+ * Displays Teletext service. */
+ public static final int KEYCODE_TV_TELETEXT = 233;
+ /** Key code constant: Number entry key.
+ * Initiates to enter multi-digit channel nubmber when each digit key is assigned
+ * for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC
+ * User Control Code. */
+ public static final int KEYCODE_TV_NUMBER_ENTRY = 234;
+ /** Key code constant: Analog Terrestrial key.
+ * Switches to analog terrestrial broadcast service. */
+ public static final int KEYCODE_TV_TERRESTRIAL_ANALOG = 235;
+ /** Key code constant: Digital Terrestrial key.
+ * Switches to digital terrestrial broadcast service. */
+ public static final int KEYCODE_TV_TERRESTRIAL_DIGITAL = 236;
+ /** Key code constant: Satellite key.
+ * Switches to digital satellite broadcast service. */
+ public static final int KEYCODE_TV_SATELLITE = 237;
+ /** Key code constant: BS key.
+ * Switches to BS digital satellite broadcasting service available in Japan. */
+ public static final int KEYCODE_TV_SATELLITE_BS = 238;
+ /** Key code constant: CS key.
+ * Switches to CS digital satellite broadcasting service available in Japan. */
+ public static final int KEYCODE_TV_SATELLITE_CS = 239;
+ /** Key code constant: BS/CS key.
+ * Toggles between BS and CS digital satellite services. */
+ public static final int KEYCODE_TV_SATELLITE_SERVICE = 240;
+ /** Key code constant: Toggle Network key.
+ * Toggles selecting broacast services. */
+ public static final int KEYCODE_TV_NETWORK = 241;
+ /** Key code constant: Antenna/Cable key.
+ * Toggles broadcast input source between antenna and cable. */
+ public static final int KEYCODE_TV_ANTENNA_CABLE = 242;
+ /** Key code constant: HDMI #1 key.
+ * Switches to HDMI input #1. */
+ public static final int KEYCODE_TV_INPUT_HDMI_1 = 243;
+ /** Key code constant: HDMI #2 key.
+ * Switches to HDMI input #2. */
+ public static final int KEYCODE_TV_INPUT_HDMI_2 = 244;
+ /** Key code constant: HDMI #3 key.
+ * Switches to HDMI input #3. */
+ public static final int KEYCODE_TV_INPUT_HDMI_3 = 245;
+ /** Key code constant: HDMI #4 key.
+ * Switches to HDMI input #4. */
+ public static final int KEYCODE_TV_INPUT_HDMI_4 = 246;
+ /** Key code constant: Composite #1 key.
+ * Switches to composite video input #1. */
+ public static final int KEYCODE_TV_INPUT_COMPOSITE_1 = 247;
+ /** Key code constant: Composite #2 key.
+ * Switches to composite video input #2. */
+ public static final int KEYCODE_TV_INPUT_COMPOSITE_2 = 248;
+ /** Key code constant: Component #1 key.
+ * Switches to component video input #1. */
+ public static final int KEYCODE_TV_INPUT_COMPONENT_1 = 249;
+ /** Key code constant: Component #2 key.
+ * Switches to component video input #2. */
+ public static final int KEYCODE_TV_INPUT_COMPONENT_2 = 250;
+ /** Key code constant: VGA #1 key.
+ * Switches to VGA (analog RGB) input #1. */
+ public static final int KEYCODE_TV_INPUT_VGA_1 = 251;
+ /** Key code constant: Audio description key.
+ * Toggles audio description off / on. */
+ public static final int KEYCODE_TV_AUDIO_DESCRIPTION = 252;
+ /** Key code constant: Audio description mixing volume up key.
+ * Louden audio description volume as compared with normal audio volume. */
+ public static final int KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253;
+ /** Key code constant: Audio description mixing volume down key.
+ * Lessen audio description volume as compared with normal audio volume. */
+ public static final int KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254;
+ /** Key code constant: Zoom mode key.
+ * Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) */
+ public static final int KEYCODE_TV_ZOOM_MODE = 255;
+ /** Key code constant: Contents menu key.
+ * Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control
+ * Code */
+ public static final int KEYCODE_TV_CONTENTS_MENU = 256;
+ /** Key code constant: Media context menu key.
+ * Goes to the context menu of media contents. Corresponds to Media Context-sensitive
+ * Menu (0x11) of CEC User Control Code. */
+ public static final int KEYCODE_TV_MEDIA_CONTEXT_MENU = 257;
+ /** Key code constant: Timer programming key.
+ * Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of
+ * CEC User Control Code. */
+ public static final int KEYCODE_TV_TIMER_PROGRAMMING = 258;
+ /** Key code constant: Help key. */
+ public static final int KEYCODE_HELP = 259;
+ /** Key code constant: Navigate to previous key.
+ * Goes backward by one item in an ordered collection of items. */
+ public static final int KEYCODE_NAVIGATE_PREVIOUS = 260;
+ /** Key code constant: Navigate to next key.
+ * Advances to the next item in an ordered collection of items. */
+ public static final int KEYCODE_NAVIGATE_NEXT = 261;
+ /** Key code constant: Navigate in key.
+ * Activates the item that currently has focus or expands to the next level of a navigation
+ * hierarchy. */
+ public static final int KEYCODE_NAVIGATE_IN = 262;
+ /** Key code constant: Navigate out key.
+ * Backs out one level of a navigation hierarchy or collapses the item that currently has
+ * focus. */
+ public static final int KEYCODE_NAVIGATE_OUT = 263;
+ /** Key code constant: Primary stem key for Wear
+ * Main power/reset button on watch. */
+ public static final int KEYCODE_STEM_PRIMARY = 264;
+ /** Key code constant: Generic stem key 1 for Wear */
+ public static final int KEYCODE_STEM_1 = 265;
+ /** Key code constant: Generic stem key 2 for Wear */
+ public static final int KEYCODE_STEM_2 = 266;
+ /** Key code constant: Generic stem key 3 for Wear */
+ public static final int KEYCODE_STEM_3 = 267;
+ /** Key code constant: Directional Pad Up-Left */
+ public static final int KEYCODE_DPAD_UP_LEFT = 268;
+ /** Key code constant: Directional Pad Down-Left */
+ public static final int KEYCODE_DPAD_DOWN_LEFT = 269;
+ /** Key code constant: Directional Pad Up-Right */
+ public static final int KEYCODE_DPAD_UP_RIGHT = 270;
+ /** Key code constant: Directional Pad Down-Right */
+ public static final int KEYCODE_DPAD_DOWN_RIGHT = 271;
+ /** Key code constant: Skip forward media key. */
+ public static final int KEYCODE_MEDIA_SKIP_FORWARD = 272;
+ /** Key code constant: Skip backward media key. */
+ public static final int KEYCODE_MEDIA_SKIP_BACKWARD = 273;
+ /** Key code constant: Step forward media key.
+ * Steps media forward, one frame at a time. */
+ public static final int KEYCODE_MEDIA_STEP_FORWARD = 274;
+ /** Key code constant: Step backward media key.
+ * Steps media backward, one frame at a time. */
+ public static final int KEYCODE_MEDIA_STEP_BACKWARD = 275;
+ /** Key code constant: put device to sleep unless a wakelock is held. */
+ public static final int KEYCODE_SOFT_SLEEP = 276;
+ /** Key code constant: Cut key. */
+ public static final int KEYCODE_CUT = 277;
+ /** Key code constant: Copy key. */
+ public static final int KEYCODE_COPY = 278;
+ /** Key code constant: Paste key. */
+ public static final int KEYCODE_PASTE = 279;
+ /** Key code constant: Consumed by the system for navigation up */
+ public static final int KEYCODE_SYSTEM_NAVIGATION_UP = 280;
+ /** Key code constant: Consumed by the system for navigation down */
+ public static final int KEYCODE_SYSTEM_NAVIGATION_DOWN = 281;
+ /** Key code constant: Consumed by the system for navigation left*/
+ public static final int KEYCODE_SYSTEM_NAVIGATION_LEFT = 282;
+ /** Key code constant: Consumed by the system for navigation right */
+ public static final int KEYCODE_SYSTEM_NAVIGATION_RIGHT = 283;
+ /** Key code constant: Show all apps */
+ public static final int KEYCODE_ALL_APPS = 284;
+ /** Key code constant: Refresh key. */
+ public static final int KEYCODE_REFRESH = 285;
+ /** Key code constant: Thumbs up key. Apps can use this to let user upvote content. */
+ public static final int KEYCODE_THUMBS_UP = 286;
+ /** Key code constant: Thumbs down key. Apps can use this to let user downvote content. */
+ public static final int KEYCODE_THUMBS_DOWN = 287;
+ /**
+ * Key code constant: Used to switch current {@link android.accounts.Account} that is
+ * consuming content. May be consumed by system to set account globally.
+ */
+ public static final int KEYCODE_PROFILE_SWITCH = 288;
+
+ /**
+ * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
+ * @hide
+ */
+ @TestApi
+ public static final int LAST_KEYCODE = KEYCODE_PROFILE_SWITCH;
+
+ // NOTE: If you add a new keycode here you must also add it to:
+ // isSystem()
+ // isWakeKey()
+ // frameworks/native/include/android/keycodes.h
+ // frameworks/native/include/input/InputEventLabels.h
+ // frameworks/base/core/res/res/values/attrs.xml
+ // emulator?
+ // LAST_KEYCODE
+ //
+ // Also Android currently does not reserve code ranges for vendor-
+ // specific key codes. If you have new key codes to have, you
+ // MUST contribute a patch to the open source project to define
+ // those new codes. This is intended to maintain a consistent
+ // set of key code definitions across all Android devices.
+
+ // Symbolic names of all metakeys in bit order from least significant to most significant.
+ // Accordingly there are exactly 32 values in this table.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final String[] META_SYMBOLIC_NAMES = new String[] {
+ "META_SHIFT_ON",
+ "META_ALT_ON",
+ "META_SYM_ON",
+ "META_FUNCTION_ON",
+ "META_ALT_LEFT_ON",
+ "META_ALT_RIGHT_ON",
+ "META_SHIFT_LEFT_ON",
+ "META_SHIFT_RIGHT_ON",
+ "META_CAP_LOCKED",
+ "META_ALT_LOCKED",
+ "META_SYM_LOCKED",
+ "0x00000800",
+ "META_CTRL_ON",
+ "META_CTRL_LEFT_ON",
+ "META_CTRL_RIGHT_ON",
+ "0x00008000",
+ "META_META_ON",
+ "META_META_LEFT_ON",
+ "META_META_RIGHT_ON",
+ "0x00080000",
+ "META_CAPS_LOCK_ON",
+ "META_NUM_LOCK_ON",
+ "META_SCROLL_LOCK_ON",
+ "0x00800000",
+ "0x01000000",
+ "0x02000000",
+ "0x04000000",
+ "0x08000000",
+ "0x10000000",
+ "0x20000000",
+ "0x40000000",
+ "0x80000000",
+ };
+
+ private static final String LABEL_PREFIX = "KEYCODE_";
+
+ /**
+ * @deprecated There are now more than MAX_KEYCODE keycodes.
+ * Use {@link #getMaxKeyCode()} instead.
+ */
+ @Deprecated
+ public static final int MAX_KEYCODE = 84;
+
+ /**
+ * {@link #getAction} value: the key has been pressed down.
+ */
+ public static final int ACTION_DOWN = 0;
+ /**
+ * {@link #getAction} value: the key has been released.
+ */
+ public static final int ACTION_UP = 1;
+ /**
+ * @deprecated No longer used by the input system.
+ * {@link #getAction} value: multiple duplicate key events have
+ * occurred in a row, or a complex string is being delivered. If the
+ * key code is not {@link #KEYCODE_UNKNOWN} then the
+ * {@link #getRepeatCount()} method returns the number of times
+ * the given key code should be executed.
+ * Otherwise, if the key code is {@link #KEYCODE_UNKNOWN}, then
+ * this is a sequence of characters as returned by {@link #getCharacters}.
+ */
+ @Deprecated
+ public static final int ACTION_MULTIPLE = 2;
+
+ /**
+ * SHIFT key locked in CAPS mode.
+ * Reserved for use by {@link MetaKeyKeyListener} for a published constant in its API.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int META_CAP_LOCKED = 0x100;
+
+ /**
+ * ALT key locked.
+ * Reserved for use by {@link MetaKeyKeyListener} for a published constant in its API.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int META_ALT_LOCKED = 0x200;
+
+ /**
+ * SYM key locked.
+ * Reserved for use by {@link MetaKeyKeyListener} for a published constant in its API.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int META_SYM_LOCKED = 0x400;
+
+ /**
+ * Text is in selection mode.
+ * Reserved for use by {@link MetaKeyKeyListener} for a private unpublished constant
+ * in its API that is currently being retained for legacy reasons.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int META_SELECTING = 0x800;
+
+ /**
+ * <p>This mask is used to check whether one of the ALT meta keys is pressed.</p>
+ *
+ * @see #isAltPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_ALT_LEFT
+ * @see #KEYCODE_ALT_RIGHT
+ */
+ public static final int META_ALT_ON = 0x02;
+
+ /**
+ * <p>This mask is used to check whether the left ALT meta key is pressed.</p>
+ *
+ * @see #isAltPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_ALT_LEFT
+ */
+ public static final int META_ALT_LEFT_ON = 0x10;
+
+ /**
+ * <p>This mask is used to check whether the right the ALT meta key is pressed.</p>
+ *
+ * @see #isAltPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_ALT_RIGHT
+ */
+ public static final int META_ALT_RIGHT_ON = 0x20;
+
+ /**
+ * <p>This mask is used to check whether one of the SHIFT meta keys is pressed.</p>
+ *
+ * @see #isShiftPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_SHIFT_LEFT
+ * @see #KEYCODE_SHIFT_RIGHT
+ */
+ public static final int META_SHIFT_ON = 0x1;
+
+ /**
+ * <p>This mask is used to check whether the left SHIFT meta key is pressed.</p>
+ *
+ * @see #isShiftPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_SHIFT_LEFT
+ */
+ public static final int META_SHIFT_LEFT_ON = 0x40;
+
+ /**
+ * <p>This mask is used to check whether the right SHIFT meta key is pressed.</p>
+ *
+ * @see #isShiftPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_SHIFT_RIGHT
+ */
+ public static final int META_SHIFT_RIGHT_ON = 0x80;
+
+ /**
+ * <p>This mask is used to check whether the SYM meta key is pressed.</p>
+ *
+ * @see #isSymPressed()
+ * @see #getMetaState()
+ */
+ public static final int META_SYM_ON = 0x4;
+
+ /**
+ * <p>This mask is used to check whether the FUNCTION meta key is pressed.</p>
+ *
+ * @see #isFunctionPressed()
+ * @see #getMetaState()
+ */
+ public static final int META_FUNCTION_ON = 0x8;
+
+ /**
+ * <p>This mask is used to check whether one of the CTRL meta keys is pressed.</p>
+ *
+ * @see #isCtrlPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_CTRL_LEFT
+ * @see #KEYCODE_CTRL_RIGHT
+ */
+ public static final int META_CTRL_ON = 0x1000;
+
+ /**
+ * <p>This mask is used to check whether the left CTRL meta key is pressed.</p>
+ *
+ * @see #isCtrlPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_CTRL_LEFT
+ */
+ public static final int META_CTRL_LEFT_ON = 0x2000;
+
+ /**
+ * <p>This mask is used to check whether the right CTRL meta key is pressed.</p>
+ *
+ * @see #isCtrlPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_CTRL_RIGHT
+ */
+ public static final int META_CTRL_RIGHT_ON = 0x4000;
+
+ /**
+ * <p>This mask is used to check whether one of the META meta keys is pressed.</p>
+ *
+ * @see #isMetaPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_META_LEFT
+ * @see #KEYCODE_META_RIGHT
+ */
+ public static final int META_META_ON = 0x10000;
+
+ /**
+ * <p>This mask is used to check whether the left META meta key is pressed.</p>
+ *
+ * @see #isMetaPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_META_LEFT
+ */
+ public static final int META_META_LEFT_ON = 0x20000;
+
+ /**
+ * <p>This mask is used to check whether the right META meta key is pressed.</p>
+ *
+ * @see #isMetaPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_META_RIGHT
+ */
+ public static final int META_META_RIGHT_ON = 0x40000;
+
+ /**
+ * <p>This mask is used to check whether the CAPS LOCK meta key is on.</p>
+ *
+ * @see #isCapsLockOn()
+ * @see #getMetaState()
+ * @see #KEYCODE_CAPS_LOCK
+ */
+ public static final int META_CAPS_LOCK_ON = 0x100000;
+
+ /**
+ * <p>This mask is used to check whether the NUM LOCK meta key is on.</p>
+ *
+ * @see #isNumLockOn()
+ * @see #getMetaState()
+ * @see #KEYCODE_NUM_LOCK
+ */
+ public static final int META_NUM_LOCK_ON = 0x200000;
+
+ /**
+ * <p>This mask is used to check whether the SCROLL LOCK meta key is on.</p>
+ *
+ * @see #isScrollLockOn()
+ * @see #getMetaState()
+ * @see #KEYCODE_SCROLL_LOCK
+ */
+ public static final int META_SCROLL_LOCK_ON = 0x400000;
+
+ /**
+ * This mask is a combination of {@link #META_SHIFT_ON}, {@link #META_SHIFT_LEFT_ON}
+ * and {@link #META_SHIFT_RIGHT_ON}.
+ */
+ public static final int META_SHIFT_MASK = META_SHIFT_ON
+ | META_SHIFT_LEFT_ON | META_SHIFT_RIGHT_ON;
+
+ /**
+ * This mask is a combination of {@link #META_ALT_ON}, {@link #META_ALT_LEFT_ON}
+ * and {@link #META_ALT_RIGHT_ON}.
+ */
+ public static final int META_ALT_MASK = META_ALT_ON
+ | META_ALT_LEFT_ON | META_ALT_RIGHT_ON;
+
+ /**
+ * This mask is a combination of {@link #META_CTRL_ON}, {@link #META_CTRL_LEFT_ON}
+ * and {@link #META_CTRL_RIGHT_ON}.
+ */
+ public static final int META_CTRL_MASK = META_CTRL_ON
+ | META_CTRL_LEFT_ON | META_CTRL_RIGHT_ON;
+
+ /**
+ * This mask is a combination of {@link #META_META_ON}, {@link #META_META_LEFT_ON}
+ * and {@link #META_META_RIGHT_ON}.
+ */
+ public static final int META_META_MASK = META_META_ON
+ | META_META_LEFT_ON | META_META_RIGHT_ON;
+
+ /**
+ * This mask is set if the device woke because of this key event.
+ *
+ * @deprecated This flag will never be set by the system since the system
+ * consumes all wake keys itself.
+ */
+ @Deprecated
+ public static final int FLAG_WOKE_HERE = 0x1;
+
+ /**
+ * This mask is set if the key event was generated by a software keyboard.
+ */
+ public static final int FLAG_SOFT_KEYBOARD = 0x2;
+
+ /**
+ * This mask is set if we don't want the key event to cause us to leave
+ * touch mode.
+ */
+ public static final int FLAG_KEEP_TOUCH_MODE = 0x4;
+
+ /**
+ * This mask is set if an event was known to come from a trusted part
+ * of the system. That is, the event is known to come from the user,
+ * and could not have been spoofed by a third party component.
+ */
+ public static final int FLAG_FROM_SYSTEM = 0x8;
+
+ /**
+ * This mask is used for compatibility, to identify enter keys that are
+ * coming from an IME whose enter key has been auto-labelled "next" or
+ * "done". This allows TextView to dispatch these as normal enter keys
+ * for old applications, but still do the appropriate action when
+ * receiving them.
+ */
+ public static final int FLAG_EDITOR_ACTION = 0x10;
+
+ /**
+ * When associated with up key events, this indicates that the key press
+ * has been canceled. Typically this is used with virtual touch screen
+ * keys, where the user can slide from the virtual key area on to the
+ * display: in that case, the application will receive a canceled up
+ * event and should not perform the action normally associated with the
+ * key. Note that for this to work, the application can not perform an
+ * action for a key until it receives an up or the long press timeout has
+ * expired.
+ */
+ public static final int FLAG_CANCELED = 0x20;
+
+ /**
+ * This key event was generated by a virtual (on-screen) hard key area.
+ * Typically this is an area of the touchscreen, outside of the regular
+ * display, dedicated to "hardware" buttons.
+ */
+ public static final int FLAG_VIRTUAL_HARD_KEY = 0x40;
+
+ /**
+ * This flag is set for the first key repeat that occurs after the
+ * long press timeout.
+ */
+ public static final int FLAG_LONG_PRESS = 0x80;
+
+ /**
+ * Set when a key event has {@link #FLAG_CANCELED} set because a long
+ * press action was executed while it was down.
+ */
+ public static final int FLAG_CANCELED_LONG_PRESS = 0x100;
+
+ /**
+ * Set for {@link #ACTION_UP} when this event's key code is still being
+ * tracked from its initial down. That is, somebody requested that tracking
+ * started on the key down and a long press has not caused
+ * the tracking to be canceled.
+ */
+ public static final int FLAG_TRACKING = 0x200;
+
+ /**
+ * Set when a key event has been synthesized to implement default behavior
+ * for an event that the application did not handle.
+ * Fallback key events are generated by unhandled trackball motions
+ * (to emulate a directional keypad) and by certain unhandled key presses
+ * that are declared in the key map (such as special function numeric keypad
+ * keys when numlock is off).
+ */
+ public static final int FLAG_FALLBACK = 0x400;
+
+ /**
+ * This flag indicates that this event was modified by or generated from an accessibility
+ * service. Value = 0x800
+ * @hide
+ */
+ @TestApi
+ public static final int FLAG_IS_ACCESSIBILITY_EVENT = INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
+
+ /**
+ * Signifies that the key is being predispatched.
+ * @hide
+ */
+ public static final int FLAG_PREDISPATCH = 0x20000000;
+
+ /**
+ * Private control to determine when an app is tracking a key sequence.
+ * @hide
+ */
+ public static final int FLAG_START_TRACKING = 0x40000000;
+
+ /**
+ * Private flag that indicates when the system has detected that this key event
+ * may be inconsistent with respect to the sequence of previously delivered key events,
+ * such as when a key up event is sent but the key was not down.
+ *
+ * @hide
+ * @see #isTainted
+ * @see #setTainted
+ */
+ public static final int FLAG_TAINTED = 0x80000000;
+
+ /**
+ * Returns the maximum keycode.
+ */
+ public static int getMaxKeyCode() {
+ return LAST_KEYCODE;
+ }
+
+ /**
+ * Get the character that is produced by putting accent on the character
+ * c.
+ * For example, getDeadChar('`', 'e') returns è.
+ */
+ public static int getDeadChar(int accent, int c) {
+ return KeyCharacterMap.getDeadChar(accent, c);
+ }
+
+ static final boolean DEBUG = false;
+ static final String TAG = "KeyEvent";
+
+ private static final int MAX_RECYCLED = 10;
+ private static final Object gRecyclerLock = new Object();
+ private static int gRecyclerUsed;
+ private static KeyEvent gRecyclerTop;
+
+ private KeyEvent mNext;
+
+ private int mId;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mDeviceId;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private int mSource;
+ private int mDisplayId;
+ private @Nullable byte[] mHmac;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mMetaState;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mAction;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mKeyCode;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mScanCode;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mRepeatCount;
+ @UnsupportedAppUsage
+ private int mFlags;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private long mDownTime;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private long mEventTime;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private String mCharacters;
+
+ public interface Callback {
+ /**
+ * Called when a key down event has occurred. If you return true,
+ * you can first call {@link KeyEvent#startTracking()
+ * KeyEvent.startTracking()} to have the framework track the event
+ * through its {@link #onKeyUp(int, KeyEvent)} and also call your
+ * {@link #onKeyLongPress(int, KeyEvent)} if it occurs.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyDown(int keyCode, KeyEvent event);
+
+ /**
+ * Called when a long press has occurred. If you return true,
+ * the final key up will have {@link KeyEvent#FLAG_CANCELED} and
+ * {@link KeyEvent#FLAG_CANCELED_LONG_PRESS} set. Note that in
+ * order to receive this callback, someone in the event change
+ * <em>must</em> return true from {@link #onKeyDown} <em>and</em>
+ * call {@link KeyEvent#startTracking()} on the event.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyLongPress(int keyCode, KeyEvent event);
+
+ /**
+ * Called when a key up event has occurred.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyUp(int keyCode, KeyEvent event);
+
+ /**
+ * Called when a user's interaction with an analog control, such as
+ * flinging a trackball, generates simulated down/up events for the same
+ * key multiple times in quick succession.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param count Number of pairs as returned by event.getRepeatCount().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
+ }
+
+ private static native String nativeKeyCodeToString(int keyCode);
+ private static native int nativeKeyCodeFromString(String keyCode);
+ private static native int nativeNextId();
+
+ private KeyEvent() {}
+
+ /**
+ * Create a new key event.
+ *
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ */
+ public KeyEvent(int action, int code) {
+ mId = nativeNextId();
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = 0;
+ mDeviceId = KeyCharacterMap.VIRTUAL_KEYBOARD;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat) {
+ mId = nativeNextId();
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mDeviceId = KeyCharacterMap.VIRTUAL_KEYBOARD;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState) {
+ mId = nativeNextId();
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ mDeviceId = KeyCharacterMap.VIRTUAL_KEYBOARD;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ * @param deviceId The device ID that generated the key event.
+ * @param scancode Raw device scan code of the event.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int deviceId, int scancode) {
+ mId = nativeNextId();
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ mDeviceId = deviceId;
+ mScanCode = scancode;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ * @param deviceId The device ID that generated the key event.
+ * @param scancode Raw device scan code of the event.
+ * @param flags The flags for this key event
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int deviceId, int scancode, int flags) {
+ mId = nativeNextId();
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ mDeviceId = deviceId;
+ mScanCode = scancode;
+ mFlags = flags;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ * @param deviceId The device ID that generated the key event.
+ * @param scancode Raw device scan code of the event.
+ * @param flags The flags for this key event
+ * @param source The input source such as {@link InputDevice#SOURCE_KEYBOARD}.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int deviceId, int scancode, int flags, int source) {
+ mId = nativeNextId();
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ mDeviceId = deviceId;
+ mScanCode = scancode;
+ mFlags = flags;
+ mSource = source;
+ mDisplayId = INVALID_DISPLAY;
+ }
+
+ /**
+ * Create a new key event for a string of characters. The key code,
+ * action, repeat count and source will automatically be set to
+ * {@link #KEYCODE_UNKNOWN}, {@link #ACTION_MULTIPLE}, 0, and
+ * {@link InputDevice#SOURCE_KEYBOARD} for you.
+ *
+ * @param time The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event occured.
+ * @param characters The string of characters.
+ * @param deviceId The device ID that generated the key event.
+ * @param flags The flags for this key event
+ */
+ public KeyEvent(long time, String characters, int deviceId, int flags) {
+ mId = nativeNextId();
+ mDownTime = time;
+ mEventTime = time;
+ mCharacters = characters;
+ mAction = ACTION_MULTIPLE;
+ mKeyCode = KEYCODE_UNKNOWN;
+ mRepeatCount = 0;
+ mDeviceId = deviceId;
+ mFlags = flags;
+ mSource = InputDevice.SOURCE_KEYBOARD;
+ mDisplayId = INVALID_DISPLAY;
+ }
+
+ /**
+ * Make an exact copy of an existing key event.
+ */
+ public KeyEvent(KeyEvent origEvent) {
+ mId = origEvent.mId;
+ mDownTime = origEvent.mDownTime;
+ mEventTime = origEvent.mEventTime;
+ mAction = origEvent.mAction;
+ mKeyCode = origEvent.mKeyCode;
+ mRepeatCount = origEvent.mRepeatCount;
+ mMetaState = origEvent.mMetaState;
+ mDeviceId = origEvent.mDeviceId;
+ mSource = origEvent.mSource;
+ mDisplayId = origEvent.mDisplayId;
+ mHmac = origEvent.mHmac == null ? null : origEvent.mHmac.clone();
+ mScanCode = origEvent.mScanCode;
+ mFlags = origEvent.mFlags;
+ mCharacters = origEvent.mCharacters;
+ }
+
+ /**
+ * Copy an existing key event, modifying its time and repeat count.
+ *
+ * @deprecated Use {@link #changeTimeRepeat(KeyEvent, long, int)}
+ * instead.
+ *
+ * @param origEvent The existing event to be copied.
+ * @param eventTime The new event time
+ * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
+ * @param newRepeat The new repeat count of the event.
+ */
+ @Deprecated
+ public KeyEvent(KeyEvent origEvent, long eventTime, int newRepeat) {
+ mId = nativeNextId(); // Not an exact copy so assign a new ID.
+ mDownTime = origEvent.mDownTime;
+ mEventTime = eventTime;
+ mAction = origEvent.mAction;
+ mKeyCode = origEvent.mKeyCode;
+ mRepeatCount = newRepeat;
+ mMetaState = origEvent.mMetaState;
+ mDeviceId = origEvent.mDeviceId;
+ mSource = origEvent.mSource;
+ mDisplayId = origEvent.mDisplayId;
+ mHmac = null; // Don't copy HMAC, it will be invalid because eventTime is changing
+ mScanCode = origEvent.mScanCode;
+ mFlags = origEvent.mFlags;
+ mCharacters = origEvent.mCharacters;
+ }
+
+ private static KeyEvent obtain() {
+ final KeyEvent ev;
+ synchronized (gRecyclerLock) {
+ ev = gRecyclerTop;
+ if (ev == null) {
+ return new KeyEvent();
+ }
+ gRecyclerTop = ev.mNext;
+ gRecyclerUsed -= 1;
+ }
+ ev.mNext = null;
+ ev.prepareForReuse();
+ return ev;
+ }
+
+ /**
+ * Obtains a (potentially recycled) key event. Used by native code to create a Java object.
+ *
+ * @hide
+ */
+ public static KeyEvent obtain(int id, long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int deviceId, int scancode, int flags, int source, int displayId, @Nullable byte[] hmac,
+ String characters) {
+ KeyEvent ev = obtain();
+ ev.mId = id;
+ ev.mDownTime = downTime;
+ ev.mEventTime = eventTime;
+ ev.mAction = action;
+ ev.mKeyCode = code;
+ ev.mRepeatCount = repeat;
+ ev.mMetaState = metaState;
+ ev.mDeviceId = deviceId;
+ ev.mScanCode = scancode;
+ ev.mFlags = flags;
+ ev.mSource = source;
+ ev.mDisplayId = displayId;
+ ev.mHmac = hmac;
+ ev.mCharacters = characters;
+ return ev;
+ }
+
+ /**
+ * Obtains a (potentially recycled) key event.
+ *
+ * @hide
+ */
+ public static KeyEvent obtain(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int deviceId, int scanCode, int flags, int source, int displayId, String characters) {
+ return obtain(nativeNextId(), downTime, eventTime, action, code, repeat, metaState,
+ deviceId, scanCode, flags, source, displayId, null /* hmac */, characters);
+ }
+
+ /**
+ * Obtains a (potentially recycled) key event.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static KeyEvent obtain(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int deviceId, int scancode, int flags, int source, String characters) {
+ return obtain(downTime, eventTime, action, code, repeat, metaState, deviceId, scancode,
+ flags, source, INVALID_DISPLAY, characters);
+ }
+
+ /**
+
+ /**
+ * Obtains a (potentially recycled) copy of another key event.
+ *
+ * @hide
+ */
+ public static KeyEvent obtain(KeyEvent other) {
+ KeyEvent ev = obtain();
+ ev.mId = other.mId;
+ ev.mDownTime = other.mDownTime;
+ ev.mEventTime = other.mEventTime;
+ ev.mAction = other.mAction;
+ ev.mKeyCode = other.mKeyCode;
+ ev.mRepeatCount = other.mRepeatCount;
+ ev.mMetaState = other.mMetaState;
+ ev.mDeviceId = other.mDeviceId;
+ ev.mScanCode = other.mScanCode;
+ ev.mFlags = other.mFlags;
+ ev.mSource = other.mSource;
+ ev.mDisplayId = other.mDisplayId;
+ ev.mHmac = other.mHmac == null ? null : other.mHmac.clone();
+ ev.mCharacters = other.mCharacters;
+ return ev;
+ }
+
+ /** @hide */
+ @Override
+ public KeyEvent copy() {
+ return obtain(this);
+ }
+
+ /**
+ * Recycles a key event.
+ * Key events should only be recycled if they are owned by the system since user
+ * code expects them to be essentially immutable, "tracking" notwithstanding.
+ *
+ * @hide
+ */
+ @Override
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public final void recycle() {
+ super.recycle();
+ mCharacters = null;
+
+ synchronized (gRecyclerLock) {
+ if (gRecyclerUsed < MAX_RECYCLED) {
+ gRecyclerUsed++;
+ mNext = gRecyclerTop;
+ gRecyclerTop = this;
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public final void recycleIfNeededAfterDispatch() {
+ // Do nothing.
+ }
+
+ /** @hide */
+ @Override
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Create a new key event that is the same as the given one, but whose
+ * event time and repeat count are replaced with the given value.
+ *
+ * @param event The existing event to be copied. This is not modified.
+ * @param eventTime The new event time
+ * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
+ * @param newRepeat The new repeat count of the event.
+ */
+ public static KeyEvent changeTimeRepeat(KeyEvent event, long eventTime,
+ int newRepeat) {
+ return new KeyEvent(event, eventTime, newRepeat);
+ }
+
+ /**
+ * Create a new key event that is the same as the given one, but whose
+ * event time and repeat count are replaced with the given value.
+ *
+ * @param event The existing event to be copied. This is not modified.
+ * @param eventTime The new event time
+ * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
+ * @param newRepeat The new repeat count of the event.
+ * @param newFlags New flags for the event, replacing the entire value
+ * in the original event.
+ */
+ public static KeyEvent changeTimeRepeat(KeyEvent event, long eventTime,
+ int newRepeat, int newFlags) {
+ KeyEvent ret = new KeyEvent(event);
+ ret.mId = nativeNextId(); // Not an exact copy so assign a new ID.
+ ret.mEventTime = eventTime;
+ ret.mRepeatCount = newRepeat;
+ ret.mFlags = newFlags;
+ return ret;
+ }
+
+ /**
+ * Copy an existing key event, modifying its action.
+ *
+ * @param origEvent The existing event to be copied.
+ * @param action The new action code of the event.
+ */
+ private KeyEvent(KeyEvent origEvent, int action) {
+ mId = nativeNextId(); // Not an exact copy so assign a new ID.
+ mDownTime = origEvent.mDownTime;
+ mEventTime = origEvent.mEventTime;
+ mAction = action;
+ mKeyCode = origEvent.mKeyCode;
+ mRepeatCount = origEvent.mRepeatCount;
+ mMetaState = origEvent.mMetaState;
+ mDeviceId = origEvent.mDeviceId;
+ mSource = origEvent.mSource;
+ mDisplayId = origEvent.mDisplayId;
+ mHmac = null; // Don't copy the hmac, it will be invalid since action is changing
+ mScanCode = origEvent.mScanCode;
+ mFlags = origEvent.mFlags;
+ // Don't copy mCharacters, since one way or the other we'll lose it
+ // when changing the action.
+ }
+
+ /**
+ * Create a new key event that is the same as the given one, but whose
+ * action is replaced with the given value.
+ *
+ * @param event The existing event to be copied. This is not modified.
+ * @param action The new action code of the event.
+ */
+ public static KeyEvent changeAction(KeyEvent event, int action) {
+ return new KeyEvent(event, action);
+ }
+
+ /**
+ * Create a new key event that is the same as the given one, but whose
+ * flags are replaced with the given value.
+ *
+ * @param event The existing event to be copied. This is not modified.
+ * @param flags The new flags constant.
+ */
+ public static KeyEvent changeFlags(KeyEvent event, int flags) {
+ event = new KeyEvent(event);
+ event.mId = nativeNextId(); // Not an exact copy so assign a new ID.
+ event.mFlags = flags;
+ return event;
+ }
+
+ /** @hide */
+ @Override
+ public final boolean isTainted() {
+ return (mFlags & FLAG_TAINTED) != 0;
+ }
+
+ /** @hide */
+ @Override
+ public final void setTainted(boolean tainted) {
+ mFlags = tainted ? mFlags | FLAG_TAINTED : mFlags & ~FLAG_TAINTED;
+ }
+
+ /**
+ * Don't use in new code, instead explicitly check
+ * {@link #getAction()}.
+ *
+ * @return If the action is ACTION_DOWN, returns true; else false.
+ *
+ * @deprecated
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @Deprecated public final boolean isDown() {
+ return mAction == ACTION_DOWN;
+ }
+
+ /** Is this a system key? System keys can not be used for menu shortcuts.
+ */
+ public final boolean isSystem() {
+ return isSystemKey(mKeyCode);
+ }
+
+ /** @hide */
+ public final boolean isWakeKey() {
+ return isWakeKey(mKeyCode);
+ }
+
+ /**
+ * Returns true if the specified keycode is a gamepad button.
+ * @return True if the keycode is a gamepad button, such as {@link #KEYCODE_BUTTON_A}.
+ */
+ public static final boolean isGamepadButton(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BUTTON_A:
+ case KeyEvent.KEYCODE_BUTTON_B:
+ case KeyEvent.KEYCODE_BUTTON_C:
+ case KeyEvent.KEYCODE_BUTTON_X:
+ case KeyEvent.KEYCODE_BUTTON_Y:
+ case KeyEvent.KEYCODE_BUTTON_Z:
+ case KeyEvent.KEYCODE_BUTTON_L1:
+ case KeyEvent.KEYCODE_BUTTON_R1:
+ case KeyEvent.KEYCODE_BUTTON_L2:
+ case KeyEvent.KEYCODE_BUTTON_R2:
+ case KeyEvent.KEYCODE_BUTTON_THUMBL:
+ case KeyEvent.KEYCODE_BUTTON_THUMBR:
+ case KeyEvent.KEYCODE_BUTTON_START:
+ case KeyEvent.KEYCODE_BUTTON_SELECT:
+ case KeyEvent.KEYCODE_BUTTON_MODE:
+ case KeyEvent.KEYCODE_BUTTON_1:
+ case KeyEvent.KEYCODE_BUTTON_2:
+ case KeyEvent.KEYCODE_BUTTON_3:
+ case KeyEvent.KEYCODE_BUTTON_4:
+ case KeyEvent.KEYCODE_BUTTON_5:
+ case KeyEvent.KEYCODE_BUTTON_6:
+ case KeyEvent.KEYCODE_BUTTON_7:
+ case KeyEvent.KEYCODE_BUTTON_8:
+ case KeyEvent.KEYCODE_BUTTON_9:
+ case KeyEvent.KEYCODE_BUTTON_10:
+ case KeyEvent.KEYCODE_BUTTON_11:
+ case KeyEvent.KEYCODE_BUTTON_12:
+ case KeyEvent.KEYCODE_BUTTON_13:
+ case KeyEvent.KEYCODE_BUTTON_14:
+ case KeyEvent.KEYCODE_BUTTON_15:
+ case KeyEvent.KEYCODE_BUTTON_16:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** Whether key will, by default, trigger a click on the focused view.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final boolean isConfirmKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_SPACE:
+ case KeyEvent.KEYCODE_NUMPAD_ENTER:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether this key will be sent to the
+ * {@link android.media.session.MediaSession.Callback} if not handled.
+ */
+ public static final boolean isMediaSessionKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ return true;
+ }
+ return false;
+ }
+
+ /** Is this a system key? System keys can not be used for menu shortcuts.
+ * @hide
+ */
+ public static final boolean isSystemKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_SOFT_RIGHT:
+ case KeyEvent.KEYCODE_HOME:
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_CALL:
+ case KeyEvent.KEYCODE_ENDCALL:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_POWER:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_CAMERA:
+ case KeyEvent.KEYCODE_FOCUS:
+ case KeyEvent.KEYCODE_SEARCH:
+ case KeyEvent.KEYCODE_BRIGHTNESS_DOWN:
+ case KeyEvent.KEYCODE_BRIGHTNESS_UP:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP:
+ case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN:
+ case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
+ case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT:
+ return true;
+ }
+
+ return false;
+ }
+
+ /** @hide */
+ public static final boolean isWakeKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CAMERA:
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_PAIRING:
+ case KeyEvent.KEYCODE_STEM_1:
+ case KeyEvent.KEYCODE_STEM_2:
+ case KeyEvent.KEYCODE_STEM_3:
+ case KeyEvent.KEYCODE_WAKEUP:
+ return true;
+ }
+ return false;
+ }
+
+ /** @hide */
+ public static final boolean isMetaKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_META_LEFT || keyCode == KeyEvent.KEYCODE_META_RIGHT;
+ }
+
+ /** @hide */
+ public static final boolean isAltKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final int getDeviceId() {
+ return mDeviceId;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final int getSource() {
+ return mSource;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final void setSource(int source) {
+ mSource = source;
+ }
+
+ /** @hide */
+ @Override
+ public final int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /** @hide */
+ @TestApi
+ @Override
+ public final void setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ /**
+ * <p>Returns the state of the meta keys.</p>
+ *
+ * @return an integer in which each bit set to 1 represents a pressed
+ * meta key
+ *
+ * @see #isAltPressed()
+ * @see #isShiftPressed()
+ * @see #isSymPressed()
+ * @see #isCtrlPressed()
+ * @see #isMetaPressed()
+ * @see #isFunctionPressed()
+ * @see #isCapsLockOn()
+ * @see #isNumLockOn()
+ * @see #isScrollLockOn()
+ * @see #META_ALT_ON
+ * @see #META_ALT_LEFT_ON
+ * @see #META_ALT_RIGHT_ON
+ * @see #META_SHIFT_ON
+ * @see #META_SHIFT_LEFT_ON
+ * @see #META_SHIFT_RIGHT_ON
+ * @see #META_SYM_ON
+ * @see #META_FUNCTION_ON
+ * @see #META_CTRL_ON
+ * @see #META_CTRL_LEFT_ON
+ * @see #META_CTRL_RIGHT_ON
+ * @see #META_META_ON
+ * @see #META_META_LEFT_ON
+ * @see #META_META_RIGHT_ON
+ * @see #META_CAPS_LOCK_ON
+ * @see #META_NUM_LOCK_ON
+ * @see #META_SCROLL_LOCK_ON
+ * @see #getModifiers
+ */
+ public final int getMetaState() {
+ return mMetaState;
+ }
+
+ /**
+ * Returns the state of the modifier keys.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, this function specifically masks out
+ * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}.
+ * </p><p>
+ * The value returned consists of the meta state (from {@link #getMetaState})
+ * normalized using {@link #normalizeMetaState(int)} and then masked with
+ * {@link #getModifierMetaStateMask} so that only valid modifier bits are retained.
+ * </p>
+ *
+ * @return An integer in which each bit set to 1 represents a pressed modifier key.
+ * @see #getMetaState
+ */
+ public final int getModifiers() {
+ return normalizeMetaState(mMetaState) & META_MODIFIER_MASK;
+ }
+
+ /**
+ * Modifies the flags of the event.
+ *
+ * @param newFlags New flags for the event, replacing the entire value.
+ * @hide
+ */
+ public final void setFlags(int newFlags) {
+ mFlags = newFlags;
+ }
+
+ /**
+ * Returns the flags for this key event.
+ *
+ * @see #FLAG_WOKE_HERE
+ */
+ public final int getFlags() {
+ return mFlags;
+ }
+
+ // Mask of all modifier key meta states. Specifically excludes locked keys like caps lock.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final int META_MODIFIER_MASK =
+ META_SHIFT_ON | META_SHIFT_LEFT_ON | META_SHIFT_RIGHT_ON
+ | META_ALT_ON | META_ALT_LEFT_ON | META_ALT_RIGHT_ON
+ | META_CTRL_ON | META_CTRL_LEFT_ON | META_CTRL_RIGHT_ON
+ | META_META_ON | META_META_LEFT_ON | META_META_RIGHT_ON
+ | META_SYM_ON | META_FUNCTION_ON;
+
+ // Mask of all lock key meta states.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final int META_LOCK_MASK =
+ META_CAPS_LOCK_ON | META_NUM_LOCK_ON | META_SCROLL_LOCK_ON;
+
+ // Mask of all valid meta states.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final int META_ALL_MASK = META_MODIFIER_MASK | META_LOCK_MASK;
+
+ // Mask of all synthetic meta states that are reserved for API compatibility with
+ // historical uses in MetaKeyKeyListener.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final int META_SYNTHETIC_MASK =
+ META_CAP_LOCKED | META_ALT_LOCKED | META_SYM_LOCKED | META_SELECTING;
+
+ // Mask of all meta states that are not valid use in specifying a modifier key.
+ // These bits are known to be used for purposes other than specifying modifiers.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final int META_INVALID_MODIFIER_MASK =
+ META_LOCK_MASK | META_SYNTHETIC_MASK;
+
+ /**
+ * Gets a mask that includes all valid modifier key meta state bits.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, the mask specifically excludes
+ * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}.
+ * </p>
+ *
+ * @return The modifier meta state mask which is a combination of
+ * {@link #META_SHIFT_ON}, {@link #META_SHIFT_LEFT_ON}, {@link #META_SHIFT_RIGHT_ON},
+ * {@link #META_ALT_ON}, {@link #META_ALT_LEFT_ON}, {@link #META_ALT_RIGHT_ON},
+ * {@link #META_CTRL_ON}, {@link #META_CTRL_LEFT_ON}, {@link #META_CTRL_RIGHT_ON},
+ * {@link #META_META_ON}, {@link #META_META_LEFT_ON}, {@link #META_META_RIGHT_ON},
+ * {@link #META_SYM_ON}, {@link #META_FUNCTION_ON}.
+ */
+ public static int getModifierMetaStateMask() {
+ return META_MODIFIER_MASK;
+ }
+
+ /**
+ * Returns true if this key code is a modifier key.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, this function return false
+ * for those keys.
+ * </p>
+ *
+ * @return True if the key code is one of
+ * {@link #KEYCODE_SHIFT_LEFT} {@link #KEYCODE_SHIFT_RIGHT},
+ * {@link #KEYCODE_ALT_LEFT}, {@link #KEYCODE_ALT_RIGHT},
+ * {@link #KEYCODE_CTRL_LEFT}, {@link #KEYCODE_CTRL_RIGHT},
+ * {@link #KEYCODE_META_LEFT}, or {@link #KEYCODE_META_RIGHT},
+ * {@link #KEYCODE_SYM}, {@link #KEYCODE_NUM}, {@link #KEYCODE_FUNCTION}.
+ */
+ public static boolean isModifierKey(int keyCode) {
+ switch (keyCode) {
+ case KEYCODE_SHIFT_LEFT:
+ case KEYCODE_SHIFT_RIGHT:
+ case KEYCODE_ALT_LEFT:
+ case KEYCODE_ALT_RIGHT:
+ case KEYCODE_CTRL_LEFT:
+ case KEYCODE_CTRL_RIGHT:
+ case KEYCODE_META_LEFT:
+ case KEYCODE_META_RIGHT:
+ case KEYCODE_SYM:
+ case KEYCODE_NUM:
+ case KEYCODE_FUNCTION:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Normalizes the specified meta state.
+ * <p>
+ * The meta state is normalized such that if either the left or right modifier meta state
+ * bits are set then the result will also include the universal bit for that modifier.
+ * </p><p>
+ * If the specified meta state contains {@link #META_ALT_LEFT_ON} then
+ * the result will also contain {@link #META_ALT_ON} in addition to {@link #META_ALT_LEFT_ON}
+ * and the other bits that were specified in the input. The same is process is
+ * performed for shift, control and meta.
+ * </p><p>
+ * If the specified meta state contains synthetic meta states defined by
+ * {@link MetaKeyKeyListener}, then those states are translated here and the original
+ * synthetic meta states are removed from the result.
+ * {@link MetaKeyKeyListener#META_CAP_LOCKED} is translated to {@link #META_CAPS_LOCK_ON}.
+ * {@link MetaKeyKeyListener#META_ALT_LOCKED} is translated to {@link #META_ALT_ON}.
+ * {@link MetaKeyKeyListener#META_SYM_LOCKED} is translated to {@link #META_SYM_ON}.
+ * </p><p>
+ * Undefined meta state bits are removed.
+ * </p>
+ *
+ * @param metaState The meta state.
+ * @return The normalized meta state.
+ */
+ public static int normalizeMetaState(int metaState) {
+ if ((metaState & (META_SHIFT_LEFT_ON | META_SHIFT_RIGHT_ON)) != 0) {
+ metaState |= META_SHIFT_ON;
+ }
+ if ((metaState & (META_ALT_LEFT_ON | META_ALT_RIGHT_ON)) != 0) {
+ metaState |= META_ALT_ON;
+ }
+ if ((metaState & (META_CTRL_LEFT_ON | META_CTRL_RIGHT_ON)) != 0) {
+ metaState |= META_CTRL_ON;
+ }
+ if ((metaState & (META_META_LEFT_ON | META_META_RIGHT_ON)) != 0) {
+ metaState |= META_META_ON;
+ }
+ if ((metaState & MetaKeyKeyListener.META_CAP_LOCKED) != 0) {
+ metaState |= META_CAPS_LOCK_ON;
+ }
+ if ((metaState & MetaKeyKeyListener.META_ALT_LOCKED) != 0) {
+ metaState |= META_ALT_ON;
+ }
+ if ((metaState & MetaKeyKeyListener.META_SYM_LOCKED) != 0) {
+ metaState |= META_SYM_ON;
+ }
+ return metaState & META_ALL_MASK;
+ }
+
+ /**
+ * Returns true if no modifiers keys are pressed according to the specified meta state.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, this function ignores
+ * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}.
+ * </p><p>
+ * The meta state is normalized prior to comparison using {@link #normalizeMetaState(int)}.
+ * </p>
+ *
+ * @param metaState The meta state to consider.
+ * @return True if no modifier keys are pressed.
+ * @see #hasNoModifiers()
+ */
+ public static boolean metaStateHasNoModifiers(int metaState) {
+ return (normalizeMetaState(metaState) & META_MODIFIER_MASK) == 0;
+ }
+
+ /**
+ * Returns true if only the specified modifier keys are pressed according to
+ * the specified meta state. Returns false if a different combination of modifier
+ * keys are pressed.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, this function ignores
+ * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}.
+ * </p><p>
+ * If the specified modifier mask includes directional modifiers, such as
+ * {@link #META_SHIFT_LEFT_ON}, then this method ensures that the
+ * modifier is pressed on that side.
+ * If the specified modifier mask includes non-directional modifiers, such as
+ * {@link #META_SHIFT_ON}, then this method ensures that the modifier
+ * is pressed on either side.
+ * If the specified modifier mask includes both directional and non-directional modifiers
+ * for the same type of key, such as {@link #META_SHIFT_ON} and {@link #META_SHIFT_LEFT_ON},
+ * then this method throws an illegal argument exception.
+ * </p>
+ *
+ * @param metaState The meta state to consider.
+ * @param modifiers The meta state of the modifier keys to check. May be a combination
+ * of modifier meta states as defined by {@link #getModifierMetaStateMask()}. May be 0 to
+ * ensure that no modifier keys are pressed.
+ * @return True if only the specified modifier keys are pressed.
+ * @throws IllegalArgumentException if the modifiers parameter contains invalid modifiers
+ * @see #hasModifiers
+ */
+ public static boolean metaStateHasModifiers(int metaState, int modifiers) {
+ // Note: For forward compatibility, we allow the parameter to contain meta states
+ // that we do not recognize but we explicitly disallow meta states that
+ // are not valid modifiers.
+ if ((modifiers & META_INVALID_MODIFIER_MASK) != 0) {
+ throw new IllegalArgumentException("modifiers must not contain "
+ + "META_CAPS_LOCK_ON, META_NUM_LOCK_ON, META_SCROLL_LOCK_ON, "
+ + "META_CAP_LOCKED, META_ALT_LOCKED, META_SYM_LOCKED, "
+ + "or META_SELECTING");
+ }
+
+ metaState = normalizeMetaState(metaState) & META_MODIFIER_MASK;
+ metaState = metaStateFilterDirectionalModifiers(metaState, modifiers,
+ META_SHIFT_ON, META_SHIFT_LEFT_ON, META_SHIFT_RIGHT_ON);
+ metaState = metaStateFilterDirectionalModifiers(metaState, modifiers,
+ META_ALT_ON, META_ALT_LEFT_ON, META_ALT_RIGHT_ON);
+ metaState = metaStateFilterDirectionalModifiers(metaState, modifiers,
+ META_CTRL_ON, META_CTRL_LEFT_ON, META_CTRL_RIGHT_ON);
+ metaState = metaStateFilterDirectionalModifiers(metaState, modifiers,
+ META_META_ON, META_META_LEFT_ON, META_META_RIGHT_ON);
+ return metaState == modifiers;
+ }
+
+ private static int metaStateFilterDirectionalModifiers(int metaState,
+ int modifiers, int basic, int left, int right) {
+ final boolean wantBasic = (modifiers & basic) != 0;
+ final int directional = left | right;
+ final boolean wantLeftOrRight = (modifiers & directional) != 0;
+
+ if (wantBasic) {
+ if (wantLeftOrRight) {
+ throw new IllegalArgumentException("modifiers must not contain "
+ + metaStateToString(basic) + " combined with "
+ + metaStateToString(left) + " or " + metaStateToString(right));
+ }
+ return metaState & ~directional;
+ } else if (wantLeftOrRight) {
+ return metaState & ~basic;
+ } else {
+ return metaState;
+ }
+ }
+
+ /**
+ * Returns true if no modifier keys are pressed.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, this function ignores
+ * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}.
+ * </p><p>
+ * The meta state is normalized prior to comparison using {@link #normalizeMetaState(int)}.
+ * </p>
+ *
+ * @return True if no modifier keys are pressed.
+ * @see #metaStateHasNoModifiers
+ */
+ public final boolean hasNoModifiers() {
+ return metaStateHasNoModifiers(mMetaState);
+ }
+
+ /**
+ * Returns true if only the specified modifiers keys are pressed.
+ * Returns false if a different combination of modifier keys are pressed.
+ * <p>
+ * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK},
+ * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are
+ * not considered modifier keys. Consequently, this function ignores
+ * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}.
+ * </p><p>
+ * If the specified modifier mask includes directional modifiers, such as
+ * {@link #META_SHIFT_LEFT_ON}, then this method ensures that the
+ * modifier is pressed on that side.
+ * If the specified modifier mask includes non-directional modifiers, such as
+ * {@link #META_SHIFT_ON}, then this method ensures that the modifier
+ * is pressed on either side.
+ * If the specified modifier mask includes both directional and non-directional modifiers
+ * for the same type of key, such as {@link #META_SHIFT_ON} and {@link #META_SHIFT_LEFT_ON},
+ * then this method throws an illegal argument exception.
+ * </p>
+ *
+ * @param modifiers The meta state of the modifier keys to check. May be a combination
+ * of modifier meta states as defined by {@link #getModifierMetaStateMask()}. May be 0 to
+ * ensure that no modifier keys are pressed.
+ * @return True if only the specified modifier keys are pressed.
+ * @throws IllegalArgumentException if the modifiers parameter contains invalid modifiers
+ * @see #metaStateHasModifiers
+ */
+ public final boolean hasModifiers(int modifiers) {
+ return metaStateHasModifiers(mMetaState, modifiers);
+ }
+
+ /**
+ * <p>Returns the pressed state of the ALT meta key.</p>
+ *
+ * @return true if the ALT key is pressed, false otherwise
+ *
+ * @see #KEYCODE_ALT_LEFT
+ * @see #KEYCODE_ALT_RIGHT
+ * @see #META_ALT_ON
+ */
+ public final boolean isAltPressed() {
+ return (mMetaState & META_ALT_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the SHIFT meta key.</p>
+ *
+ * @return true if the SHIFT key is pressed, false otherwise
+ *
+ * @see #KEYCODE_SHIFT_LEFT
+ * @see #KEYCODE_SHIFT_RIGHT
+ * @see #META_SHIFT_ON
+ */
+ public final boolean isShiftPressed() {
+ return (mMetaState & META_SHIFT_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the SYM meta key.</p>
+ *
+ * @return true if the SYM key is pressed, false otherwise
+ *
+ * @see #KEYCODE_SYM
+ * @see #META_SYM_ON
+ */
+ public final boolean isSymPressed() {
+ return (mMetaState & META_SYM_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the CTRL meta key.</p>
+ *
+ * @return true if the CTRL key is pressed, false otherwise
+ *
+ * @see #KEYCODE_CTRL_LEFT
+ * @see #KEYCODE_CTRL_RIGHT
+ * @see #META_CTRL_ON
+ */
+ public final boolean isCtrlPressed() {
+ return (mMetaState & META_CTRL_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the META meta key.</p>
+ *
+ * @return true if the META key is pressed, false otherwise
+ *
+ * @see #KEYCODE_META_LEFT
+ * @see #KEYCODE_META_RIGHT
+ * @see #META_META_ON
+ */
+ public final boolean isMetaPressed() {
+ return (mMetaState & META_META_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the FUNCTION meta key.</p>
+ *
+ * @return true if the FUNCTION key is pressed, false otherwise
+ *
+ * @see #KEYCODE_FUNCTION
+ * @see #META_FUNCTION_ON
+ */
+ public final boolean isFunctionPressed() {
+ return (mMetaState & META_FUNCTION_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the locked state of the CAPS LOCK meta key.</p>
+ *
+ * @return true if the CAPS LOCK key is on, false otherwise
+ *
+ * @see #KEYCODE_CAPS_LOCK
+ * @see #META_CAPS_LOCK_ON
+ */
+ public final boolean isCapsLockOn() {
+ return (mMetaState & META_CAPS_LOCK_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the locked state of the NUM LOCK meta key.</p>
+ *
+ * @return true if the NUM LOCK key is on, false otherwise
+ *
+ * @see #KEYCODE_NUM_LOCK
+ * @see #META_NUM_LOCK_ON
+ */
+ public final boolean isNumLockOn() {
+ return (mMetaState & META_NUM_LOCK_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the locked state of the SCROLL LOCK meta key.</p>
+ *
+ * @return true if the SCROLL LOCK key is on, false otherwise
+ *
+ * @see #KEYCODE_SCROLL_LOCK
+ * @see #META_SCROLL_LOCK_ON
+ */
+ public final boolean isScrollLockOn() {
+ return (mMetaState & META_SCROLL_LOCK_ON) != 0;
+ }
+
+ /**
+ * Retrieve the action of this key event. May be either
+ * {@link #ACTION_DOWN}, {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ *
+ * @return The event action: ACTION_DOWN, ACTION_UP, or ACTION_MULTIPLE.
+ */
+ public final int getAction() {
+ return mAction;
+ }
+
+ /**
+ * For {@link #ACTION_UP} events, indicates that the event has been
+ * canceled as per {@link #FLAG_CANCELED}.
+ */
+ public final boolean isCanceled() {
+ return (mFlags&FLAG_CANCELED) != 0;
+ }
+
+ /**
+ * Set {@link #FLAG_CANCELED} flag for the key event.
+ *
+ * @hide
+ */
+ @Override
+ public final void cancel() {
+ mFlags |= FLAG_CANCELED;
+ }
+
+ /**
+ * Call this during {@link Callback#onKeyDown} to have the system track
+ * the key through its final up (possibly including a long press). Note
+ * that only one key can be tracked at a time -- if another key down
+ * event is received while a previous one is being tracked, tracking is
+ * stopped on the previous event.
+ */
+ public final void startTracking() {
+ mFlags |= FLAG_START_TRACKING;
+ }
+
+ /**
+ * For {@link #ACTION_UP} events, indicates that the event is still being
+ * tracked from its initial down event as per
+ * {@link #FLAG_TRACKING}.
+ */
+ public final boolean isTracking() {
+ return (mFlags&FLAG_TRACKING) != 0;
+ }
+
+ /**
+ * For {@link #ACTION_DOWN} events, indicates that the event has been
+ * canceled as per {@link #FLAG_LONG_PRESS}.
+ */
+ public final boolean isLongPress() {
+ return (mFlags&FLAG_LONG_PRESS) != 0;
+ }
+
+ /**
+ * Retrieve the key code of the key event. This is the physical key that
+ * was pressed, <em>not</em> the Unicode character.
+ *
+ * @return The key code of the event.
+ */
+ public final int getKeyCode() {
+ return mKeyCode;
+ }
+
+ /**
+ * For the special case of a {@link #ACTION_MULTIPLE} event with key
+ * code of {@link #KEYCODE_UNKNOWN}, this is a raw string of characters
+ * associated with the event. In all other cases it is null.
+ *
+ * @return Returns a String of 1 or more characters associated with
+ * the event.
+ *
+ * @deprecated no longer used by the input system.
+ */
+ @Deprecated
+ public final String getCharacters() {
+ return mCharacters;
+ }
+
+ /**
+ * Retrieve the hardware key id of this key event. These values are not
+ * reliable and vary from device to device.
+ *
+ * {@more}
+ * Mostly this is here for debugging purposes.
+ */
+ public final int getScanCode() {
+ return mScanCode;
+ }
+
+ /**
+ * Retrieve the repeat count of the event. For key down events,
+ * this is the number of times the key has repeated with the first
+ * down starting at 0 and counting up from there. For key up events,
+ * this is always equal to zero. For multiple key events,
+ * this is the number of down/up pairs that have occurred.
+ *
+ * @return The number of times the key has repeated.
+ */
+ public final int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ /**
+ * Modifies the down time and the event time of the event.
+ *
+ * @param downTime The new down time (in {@link android.os.SystemClock#uptimeMillis}) of the
+ * event.
+ * @param eventTime The new event time (in {@link android.os.SystemClock#uptimeMillis}) of the
+ * event.
+ * @hide
+ */
+ public final void setTime(long downTime, long eventTime) {
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ }
+
+ /**
+ * Retrieve the time of the most recent key down event,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base. If this
+ * is a down event, this will be the same as {@link #getEventTime()}.
+ * Note that when chording keys, this value is the down time of the
+ * most recently pressed key, which may <em>not</em> be the same physical
+ * key of this event.
+ *
+ * @return Returns the most recent key down time, in the
+ * {@link android.os.SystemClock#uptimeMillis} time base
+ */
+ public final long getDownTime() {
+ return mDownTime;
+ }
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ */
+ @Override
+ public final long getEventTime() {
+ return mEventTime;
+ }
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond (instead of millisecond) precision.
+ * <p>
+ * The value is in nanosecond precision but it may not have nanosecond accuracy.
+ * </p>
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond (instead of millisecond) precision.
+ *
+ * @hide
+ */
+ @Override
+ public final long getEventTimeNano() {
+ return mEventTime * 1000000L;
+ }
+
+ /**
+ * Renamed to {@link #getDeviceId}.
+ *
+ * @hide
+ * @deprecated use {@link #getDeviceId()} instead.
+ */
+ @Deprecated
+ public final int getKeyboardDevice() {
+ return mDeviceId;
+ }
+
+ /**
+ * Gets the {@link KeyCharacterMap} associated with the keyboard device.
+ *
+ * @return The associated key character map.
+ * @throws {@link KeyCharacterMap.UnavailableException} if the key character map
+ * could not be loaded because it was malformed or the default key character map
+ * is missing from the system.
+ *
+ * @see KeyCharacterMap#load
+ */
+ public final KeyCharacterMap getKeyCharacterMap() {
+ return KeyCharacterMap.load(mDeviceId);
+ }
+
+ /**
+ * Gets the primary character for this key.
+ * In other words, the label that is physically printed on it.
+ *
+ * @return The display label character, or 0 if none (eg. for non-printing keys).
+ */
+ public char getDisplayLabel() {
+ return getKeyCharacterMap().getDisplayLabel(mKeyCode);
+ }
+
+ /**
+ * Gets the Unicode character generated by the specified key and meta
+ * key state combination.
+ * <p>
+ * Returns the Unicode character that the specified key would produce
+ * when the specified meta bits (see {@link MetaKeyKeyListener})
+ * were active.
+ * </p><p>
+ * Returns 0 if the key is not one that is used to type Unicode
+ * characters.
+ * </p><p>
+ * If the return value has bit {@link KeyCharacterMap#COMBINING_ACCENT} set, the
+ * key is a "dead key" that should be combined with another to
+ * actually produce a character -- see {@link KeyCharacterMap#getDeadChar} --
+ * after masking with {@link KeyCharacterMap#COMBINING_ACCENT_MASK}.
+ * </p>
+ *
+ * @return The associated character or combining accent, or 0 if none.
+ */
+ public int getUnicodeChar() {
+ return getUnicodeChar(mMetaState);
+ }
+
+ /**
+ * Gets the Unicode character generated by the specified key and meta
+ * key state combination.
+ * <p>
+ * Returns the Unicode character that the specified key would produce
+ * when the specified meta bits (see {@link MetaKeyKeyListener})
+ * were active.
+ * </p><p>
+ * Returns 0 if the key is not one that is used to type Unicode
+ * characters.
+ * </p><p>
+ * If the return value has bit {@link KeyCharacterMap#COMBINING_ACCENT} set, the
+ * key is a "dead key" that should be combined with another to
+ * actually produce a character -- see {@link KeyCharacterMap#getDeadChar} --
+ * after masking with {@link KeyCharacterMap#COMBINING_ACCENT_MASK}.
+ * </p>
+ *
+ * @param metaState The meta key modifier state.
+ * @return The associated character or combining accent, or 0 if none.
+ */
+ public int getUnicodeChar(int metaState) {
+ return getKeyCharacterMap().get(mKeyCode, metaState);
+ }
+
+ /**
+ * Get the character conversion data for a given key code.
+ *
+ * @param results A {@link KeyCharacterMap.KeyData} instance that will be
+ * filled with the results.
+ * @return True if the key was mapped. If the key was not mapped, results is not modified.
+ *
+ * @deprecated instead use {@link #getDisplayLabel()},
+ * {@link #getNumber()} or {@link #getUnicodeChar(int)}.
+ */
+ @Deprecated
+ public boolean getKeyData(KeyData results) {
+ return getKeyCharacterMap().getKeyData(mKeyCode, results);
+ }
+
+ /**
+ * Gets the first character in the character array that can be generated
+ * by the specified key code.
+ * <p>
+ * This is a convenience function that returns the same value as
+ * {@link #getMatch(char[],int) getMatch(chars, 0)}.
+ * </p>
+ *
+ * @param chars The array of matching characters to consider.
+ * @return The matching associated character, or 0 if none.
+ */
+ public char getMatch(char[] chars) {
+ return getMatch(chars, 0);
+ }
+
+ /**
+ * Gets the first character in the character array that can be generated
+ * by the specified key code. If there are multiple choices, prefers
+ * the one that would be generated with the specified meta key modifier state.
+ *
+ * @param chars The array of matching characters to consider.
+ * @param metaState The preferred meta key modifier state.
+ * @return The matching associated character, or 0 if none.
+ */
+ public char getMatch(char[] chars, int metaState) {
+ return getKeyCharacterMap().getMatch(mKeyCode, chars, metaState);
+ }
+
+ /**
+ * Gets the number or symbol associated with the key.
+ * <p>
+ * The character value is returned, not the numeric value.
+ * If the key is not a number, but is a symbol, the symbol is retuned.
+ * </p><p>
+ * This method is intended to to support dial pads and other numeric or
+ * symbolic entry on keyboards where certain keys serve dual function
+ * as alphabetic and symbolic keys. This method returns the number
+ * or symbol associated with the key independent of whether the user
+ * has pressed the required modifier.
+ * </p><p>
+ * For example, on one particular keyboard the keys on the top QWERTY row generate
+ * numbers when ALT is pressed such that ALT-Q maps to '1'. So for that keyboard
+ * when {@link #getNumber} is called with {@link KeyEvent#KEYCODE_Q} it returns '1'
+ * so that the user can type numbers without pressing ALT when it makes sense.
+ * </p>
+ *
+ * @return The associated numeric or symbolic character, or 0 if none.
+ */
+ public char getNumber() {
+ return getKeyCharacterMap().getNumber(mKeyCode);
+ }
+
+ /**
+ * Returns true if this key produces a glyph.
+ *
+ * @return True if the key is a printing key.
+ */
+ public boolean isPrintingKey() {
+ return getKeyCharacterMap().isPrintingKey(mKeyCode);
+ }
+
+ /**
+ * @deprecated Use {@link #dispatch(Callback, DispatcherState, Object)} instead.
+ */
+ @Deprecated
+ public final boolean dispatch(Callback receiver) {
+ return dispatch(receiver, null, null);
+ }
+
+ /**
+ * Deliver this key event to a {@link Callback} interface. If this is
+ * an ACTION_MULTIPLE event and it is not handled, then an attempt will
+ * be made to deliver a single normal event.
+ *
+ * @param receiver The Callback that will be given the event.
+ * @param state State information retained across events.
+ * @param target The target of the dispatch, for use in tracking.
+ *
+ * @return The return value from the Callback method that was called.
+ */
+ public final boolean dispatch(Callback receiver, DispatcherState state,
+ Object target) {
+ switch (mAction) {
+ case ACTION_DOWN: {
+ mFlags &= ~FLAG_START_TRACKING;
+ if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
+ + ": " + this);
+ boolean res = receiver.onKeyDown(mKeyCode, this);
+ if (state != null) {
+ if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
+ if (DEBUG) Log.v(TAG, " Start tracking!");
+ state.startTracking(this, target);
+ } else if (isLongPress() && state.isTracking(this)) {
+ try {
+ if (receiver.onKeyLongPress(mKeyCode, this)) {
+ if (DEBUG) Log.v(TAG, " Clear from long press!");
+ state.performedLongPress(this);
+ res = true;
+ }
+ } catch (AbstractMethodError e) {
+ }
+ }
+ }
+ return res;
+ }
+ case ACTION_UP:
+ if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
+ + ": " + this);
+ if (state != null) {
+ state.handleUpEvent(this);
+ }
+ return receiver.onKeyUp(mKeyCode, this);
+ case ACTION_MULTIPLE:
+ final int count = mRepeatCount;
+ final int code = mKeyCode;
+ if (receiver.onKeyMultiple(code, count, this)) {
+ return true;
+ }
+ if (code != KeyEvent.KEYCODE_UNKNOWN) {
+ mAction = ACTION_DOWN;
+ mRepeatCount = 0;
+ boolean handled = receiver.onKeyDown(code, this);
+ if (handled) {
+ mAction = ACTION_UP;
+ receiver.onKeyUp(code, this);
+ }
+ mAction = ACTION_MULTIPLE;
+ mRepeatCount = count;
+ return handled;
+ }
+ return false;
+ }
+ return false;
+ }
+
+ /**
+ * Use with {@link KeyEvent#dispatch(Callback, DispatcherState, Object)}
+ * for more advanced key dispatching, such as long presses.
+ */
+ public static class DispatcherState {
+ int mDownKeyCode;
+ Object mDownTarget;
+ SparseIntArray mActiveLongPresses = new SparseIntArray();
+
+ /**
+ * Reset back to initial state.
+ */
+ public void reset() {
+ if (DEBUG) Log.v(TAG, "Reset: " + this);
+ mDownKeyCode = 0;
+ mDownTarget = null;
+ mActiveLongPresses.clear();
+ }
+
+ /**
+ * Stop any tracking associated with this target.
+ */
+ public void reset(Object target) {
+ if (mDownTarget == target) {
+ if (DEBUG) Log.v(TAG, "Reset in " + target + ": " + this);
+ mDownKeyCode = 0;
+ mDownTarget = null;
+ }
+ }
+
+ /**
+ * Start tracking the key code associated with the given event. This
+ * can only be called on a key down. It will allow you to see any
+ * long press associated with the key, and will result in
+ * {@link KeyEvent#isTracking} return true on the long press and up
+ * events.
+ *
+ * <p>This is only needed if you are directly dispatching events, rather
+ * than handling them in {@link Callback#onKeyDown}.
+ */
+ public void startTracking(KeyEvent event, Object target) {
+ if (event.getAction() != ACTION_DOWN) {
+ throw new IllegalArgumentException(
+ "Can only start tracking on a down event");
+ }
+ if (DEBUG) Log.v(TAG, "Start trackingt in " + target + ": " + this);
+ mDownKeyCode = event.getKeyCode();
+ mDownTarget = target;
+ }
+
+ /**
+ * Return true if the key event is for a key code that is currently
+ * being tracked by the dispatcher.
+ */
+ public boolean isTracking(KeyEvent event) {
+ return mDownKeyCode == event.getKeyCode();
+ }
+
+ /**
+ * Keep track of the given event's key code as having performed an
+ * action with a long press, so no action should occur on the up.
+ * <p>This is only needed if you are directly dispatching events, rather
+ * than handling them in {@link Callback#onKeyLongPress}.
+ */
+ public void performedLongPress(KeyEvent event) {
+ mActiveLongPresses.put(event.getKeyCode(), 1);
+ }
+
+ /**
+ * Handle key up event to stop tracking. This resets the dispatcher state,
+ * and updates the key event state based on it.
+ * <p>This is only needed if you are directly dispatching events, rather
+ * than handling them in {@link Callback#onKeyUp}.
+ */
+ public void handleUpEvent(KeyEvent event) {
+ final int keyCode = event.getKeyCode();
+ if (DEBUG) Log.v(TAG, "Handle key up " + event + ": " + this);
+ int index = mActiveLongPresses.indexOfKey(keyCode);
+ if (index >= 0) {
+ if (DEBUG) Log.v(TAG, " Index: " + index);
+ event.mFlags |= FLAG_CANCELED | FLAG_CANCELED_LONG_PRESS;
+ mActiveLongPresses.removeAt(index);
+ }
+ if (mDownKeyCode == keyCode) {
+ if (DEBUG) Log.v(TAG, " Tracking!");
+ event.mFlags |= FLAG_TRACKING;
+ mDownKeyCode = 0;
+ mDownTarget = null;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder msg = new StringBuilder();
+ msg.append("KeyEvent { action=").append(actionToString(mAction));
+ msg.append(", keyCode=").append(keyCodeToString(mKeyCode));
+ msg.append(", scanCode=").append(mScanCode);
+ if (mCharacters != null) {
+ msg.append(", characters=\"").append(mCharacters).append("\"");
+ }
+ msg.append(", metaState=").append(metaStateToString(mMetaState));
+ msg.append(", flags=0x").append(Integer.toHexString(mFlags));
+ msg.append(", repeatCount=").append(mRepeatCount);
+ msg.append(", eventTime=").append(mEventTime);
+ msg.append(", downTime=").append(mDownTime);
+ msg.append(", deviceId=").append(mDeviceId);
+ msg.append(", source=0x").append(Integer.toHexString(mSource));
+ msg.append(", displayId=").append(mDisplayId);
+ msg.append(" }");
+ return msg.toString();
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified action
+ * such as "ACTION_DOWN", or an equivalent numeric constant such as "35" if unknown.
+ *
+ * @param action The action.
+ * @return The symbolic name of the specified action.
+ * @hide
+ */
+ @TestApi
+ public static String actionToString(int action) {
+ switch (action) {
+ case ACTION_DOWN:
+ return "ACTION_DOWN";
+ case ACTION_UP:
+ return "ACTION_UP";
+ case ACTION_MULTIPLE:
+ return "ACTION_MULTIPLE";
+ default:
+ return Integer.toString(action);
+ }
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified keycode
+ * such as "KEYCODE_A", "KEYCODE_DPAD_UP", or an equivalent numeric constant
+ * such as "1001" if unknown.
+ *
+ * This function is intended to be used mostly for debugging, logging, and testing. It is not
+ * locale-specific and is not intended to be used in a user-facing manner.
+ *
+ * @param keyCode The key code.
+ * @return The symbolic name of the specified keycode.
+ *
+ * @see KeyCharacterMap#getDisplayLabel
+ */
+ public static String keyCodeToString(int keyCode) {
+ String symbolicName = nativeKeyCodeToString(keyCode);
+ return symbolicName != null ? LABEL_PREFIX + symbolicName : Integer.toString(keyCode);
+ }
+
+ /**
+ * Gets a keycode by its symbolic name such as "KEYCODE_A" or an equivalent
+ * numeric constant such as "29". For symbolic names,
+ * starting in {@link android.os.Build.VERSION_CODES#Q} the prefix "KEYCODE_" is optional.
+ *
+ * @param symbolicName The symbolic name of the keycode.
+ * @return The keycode or {@link #KEYCODE_UNKNOWN} if not found.
+ * @see #keyCodeToString(int)
+ */
+ public static int keyCodeFromString(@NonNull String symbolicName) {
+ try {
+ int keyCode = Integer.parseInt(symbolicName);
+ if (keyCodeIsValid(keyCode)) {
+ return keyCode;
+ }
+ } catch (NumberFormatException ex) {
+ }
+
+ if (symbolicName.startsWith(LABEL_PREFIX)) {
+ symbolicName = symbolicName.substring(LABEL_PREFIX.length());
+ }
+ int keyCode = nativeKeyCodeFromString(symbolicName);
+ if (keyCodeIsValid(keyCode)) {
+ return keyCode;
+ }
+ return KEYCODE_UNKNOWN;
+ }
+
+ private static boolean keyCodeIsValid(int keyCode) {
+ return keyCode >= KEYCODE_UNKNOWN && keyCode <= LAST_KEYCODE;
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified combined meta
+ * key modifier state flags such as "0", "META_SHIFT_ON",
+ * "META_ALT_ON|META_SHIFT_ON" or an equivalent numeric constant such as "0x10000000"
+ * if unknown.
+ *
+ * @param metaState The meta state.
+ * @return The symbolic name of the specified combined meta state flags.
+ * @hide
+ */
+ public static String metaStateToString(int metaState) {
+ if (metaState == 0) {
+ return "0";
+ }
+ StringBuilder result = null;
+ int i = 0;
+ while (metaState != 0) {
+ final boolean isSet = (metaState & 1) != 0;
+ metaState >>>= 1; // unsigned shift!
+ if (isSet) {
+ final String name = META_SYMBOLIC_NAMES[i];
+ if (result == null) {
+ if (metaState == 0) {
+ return name;
+ }
+ result = new StringBuilder(name);
+ } else {
+ result.append('|');
+ result.append(name);
+ }
+ }
+ i += 1;
+ }
+ return result.toString();
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<KeyEvent> CREATOR
+ = new Parcelable.Creator<KeyEvent>() {
+ @Override
+ public KeyEvent createFromParcel(Parcel in) {
+ in.readInt(); // skip token, we already know this is a KeyEvent
+ return KeyEvent.createFromParcelBody(in);
+ }
+
+ @Override
+ public KeyEvent[] newArray(int size) {
+ return new KeyEvent[size];
+ }
+ };
+
+ /** @hide */
+ public static KeyEvent createFromParcelBody(Parcel in) {
+ return new KeyEvent(in);
+ }
+
+ private KeyEvent(Parcel in) {
+ mId = in.readInt();
+ mDeviceId = in.readInt();
+ mSource = in.readInt();
+ mDisplayId = in.readInt();
+ mHmac = in.createByteArray();
+ mAction = in.readInt();
+ mKeyCode = in.readInt();
+ mRepeatCount = in.readInt();
+ mMetaState = in.readInt();
+ mScanCode = in.readInt();
+ mFlags = in.readInt();
+ mDownTime = in.readLong();
+ mEventTime = in.readLong();
+ mCharacters = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(PARCEL_TOKEN_KEY_EVENT);
+
+ out.writeInt(mId);
+ out.writeInt(mDeviceId);
+ out.writeInt(mSource);
+ out.writeInt(mDisplayId);
+ out.writeByteArray(mHmac);
+ out.writeInt(mAction);
+ out.writeInt(mKeyCode);
+ out.writeInt(mRepeatCount);
+ out.writeInt(mMetaState);
+ out.writeInt(mScanCode);
+ out.writeInt(mFlags);
+ out.writeLong(mDownTime);
+ out.writeLong(mEventTime);
+ out.writeString(mCharacters);
+ }
+}
diff --git a/android/view/KeyboardShortcutGroup.java b/android/view/KeyboardShortcutGroup.java
new file mode 100644
index 0000000..763ca26
--- /dev/null
+++ b/android/view/KeyboardShortcutGroup.java
@@ -0,0 +1,137 @@
+/*
+ * 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.view;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A group of {@link KeyboardShortcutInfo}.
+ */
+public final class KeyboardShortcutGroup implements Parcelable {
+ private final CharSequence mLabel;
+ private final List<KeyboardShortcutInfo> mItems;
+ // The system group looks different UI wise.
+ private boolean mSystemGroup;
+
+ /**
+ * @param label The title to be used for this group, or null if there is none.
+ * @param items The set of items to be included.
+ */
+ public KeyboardShortcutGroup(@Nullable CharSequence label,
+ @NonNull List<KeyboardShortcutInfo> items) {
+ mLabel = label;
+ mItems = new ArrayList<>(checkNotNull(items));
+ }
+
+ /**
+ * @param label The title to be used for this group, or null if there is none.
+ */
+ public KeyboardShortcutGroup(@Nullable CharSequence label) {
+ this(label, Collections.<KeyboardShortcutInfo>emptyList());
+ }
+
+ /**
+ * @param label The title to be used for this group, or null if there is none.
+ * @param items The set of items to be included.
+ * @param isSystemGroup Set this to {@code true} if this is s system group.
+ * @hide
+ */
+ @TestApi
+ public KeyboardShortcutGroup(@Nullable CharSequence label,
+ @NonNull List<KeyboardShortcutInfo> items, boolean isSystemGroup) {
+ mLabel = label;
+ mItems = new ArrayList<>(checkNotNull(items));
+ mSystemGroup = isSystemGroup;
+ }
+
+ /**
+ * @param label The title to be used for this group, or null if there is none.
+ * @param isSystemGroup Set this to {@code true} if this is s system group.
+ * @hide
+ */
+ @TestApi
+ public KeyboardShortcutGroup(@Nullable CharSequence label, boolean isSystemGroup) {
+ this(label, Collections.<KeyboardShortcutInfo>emptyList(), isSystemGroup);
+ }
+
+ private KeyboardShortcutGroup(Parcel source) {
+ mItems = new ArrayList<>();
+ mLabel = source.readCharSequence();
+ source.readTypedList(mItems, KeyboardShortcutInfo.CREATOR);
+ mSystemGroup = source.readInt() == 1;
+ }
+
+ /**
+ * Returns the label to be used to describe this group.
+ */
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Returns the list of items included in this group.
+ */
+ public List<KeyboardShortcutInfo> getItems() {
+ return mItems;
+ }
+
+ /** @hide **/
+ @TestApi
+ public boolean isSystemGroup() {
+ return mSystemGroup;
+ }
+
+ /**
+ * Adds an item to the existing list.
+ *
+ * @param item The item to be added.
+ */
+ public void addItem(KeyboardShortcutInfo item) {
+ mItems.add(item);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mLabel);
+ dest.writeTypedList(mItems);
+ dest.writeInt(mSystemGroup ? 1 : 0);
+ }
+
+ public static final @android.annotation.NonNull Creator<KeyboardShortcutGroup> CREATOR =
+ new Creator<KeyboardShortcutGroup>() {
+ public KeyboardShortcutGroup createFromParcel(Parcel source) {
+ return new KeyboardShortcutGroup(source);
+ }
+ public KeyboardShortcutGroup[] newArray(int size) {
+ return new KeyboardShortcutGroup[size];
+ }
+ };
+}
diff --git a/android/view/KeyboardShortcutInfo.java b/android/view/KeyboardShortcutInfo.java
new file mode 100644
index 0000000..2660e74
--- /dev/null
+++ b/android/view/KeyboardShortcutInfo.java
@@ -0,0 +1,168 @@
+/*
+ * 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.view;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.lang.Character.MIN_VALUE;
+
+import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information about a Keyboard Shortcut.
+ */
+public final class KeyboardShortcutInfo implements Parcelable {
+ private final CharSequence mLabel;
+ private final Icon mIcon;
+ private final char mBaseCharacter;
+ private final int mKeycode;
+ private final int mModifiers;
+
+ /**
+ * @param label The label that identifies the action performed by this shortcut.
+ * @param icon An icon that identifies the action performed by this shortcut.
+ * @param keycode The keycode that triggers the shortcut. This should be a valid constant
+ * defined in {@link KeyEvent}.
+ * @param modifiers The set of modifiers that, combined with the key, trigger the shortcut.
+ * These should be a combination of {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_META_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_FUNCTION_ON} and
+ * {@link KeyEvent#META_SYM_ON}.
+ *
+ * @hide
+ */
+ public KeyboardShortcutInfo(
+ @Nullable CharSequence label, @Nullable Icon icon, int keycode, int modifiers) {
+ mLabel = label;
+ mIcon = icon;
+ mBaseCharacter = MIN_VALUE;
+ checkArgument(keycode >= KeyEvent.KEYCODE_UNKNOWN && keycode <= KeyEvent.getMaxKeyCode());
+ mKeycode = keycode;
+ mModifiers = modifiers;
+ }
+
+ /**
+ * @param label The label that identifies the action performed by this shortcut.
+ * @param keycode The keycode that triggers the shortcut. This should be a valid constant
+ * defined in {@link KeyEvent}.
+ * @param modifiers The set of modifiers that, combined with the key, trigger the shortcut.
+ * These should be a combination of {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_META_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_FUNCTION_ON} and
+ * {@link KeyEvent#META_SYM_ON}.
+ */
+ public KeyboardShortcutInfo(CharSequence label, int keycode, int modifiers) {
+ this(label, null, keycode, modifiers);
+ }
+
+ /**
+ * @param label The label that identifies the action performed by this shortcut.
+ * @param baseCharacter The character that triggers the shortcut.
+ * @param modifiers The set of modifiers that, combined with the key, trigger the shortcut.
+ * These should be a combination of {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_META_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_FUNCTION_ON} and
+ * {@link KeyEvent#META_SYM_ON}.
+ */
+ public KeyboardShortcutInfo(CharSequence label, char baseCharacter, int modifiers) {
+ mLabel = label;
+ checkArgument(baseCharacter != MIN_VALUE);
+ mBaseCharacter = baseCharacter;
+ mKeycode = KeyEvent.KEYCODE_UNKNOWN;
+ mModifiers = modifiers;
+ mIcon = null;
+ }
+
+ private KeyboardShortcutInfo(Parcel source) {
+ mLabel = source.readCharSequence();
+ mIcon = source.readParcelable(null);
+ mBaseCharacter = (char) source.readInt();
+ mKeycode = source.readInt();
+ mModifiers = source.readInt();
+ }
+
+ /**
+ * Returns the label to be used to describe this shortcut.
+ */
+ @Nullable
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Returns the icon to be used to describe this shortcut.
+ *
+ * @hide
+ */
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Returns the base keycode that, combined with the modifiers, triggers this shortcut. If the
+ * base character was set instead, returns {@link KeyEvent#KEYCODE_UNKNOWN}. Valid keycodes are
+ * defined as constants in {@link KeyEvent}.
+ */
+ public int getKeycode() {
+ return mKeycode;
+ }
+
+ /**
+ * Returns the base character that, combined with the modifiers, triggers this shortcut. If the
+ * keycode was set instead, returns {@link Character#MIN_VALUE}.
+ */
+ public char getBaseCharacter() {
+ return mBaseCharacter;
+ }
+
+ /**
+ * Returns the set of modifiers that, combined with the key, trigger this shortcut. These can
+ * be a combination of {@link KeyEvent#META_CTRL_ON}, {@link KeyEvent#META_SHIFT_ON},
+ * {@link KeyEvent#META_META_ON}, {@link KeyEvent#META_ALT_ON},
+ * {@link KeyEvent#META_FUNCTION_ON} and {@link KeyEvent#META_SYM_ON}.
+ */
+ public int getModifiers() {
+ return mModifiers;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mLabel);
+ dest.writeParcelable(mIcon, 0);
+ dest.writeInt(mBaseCharacter);
+ dest.writeInt(mKeycode);
+ dest.writeInt(mModifiers);
+ }
+
+ public static final @android.annotation.NonNull Creator<KeyboardShortcutInfo> CREATOR =
+ new Creator<KeyboardShortcutInfo>() {
+ public KeyboardShortcutInfo createFromParcel(Parcel source) {
+ return new KeyboardShortcutInfo(source);
+ }
+ public KeyboardShortcutInfo[] newArray(int size) {
+ return new KeyboardShortcutInfo[size];
+ }
+ };
+}
\ No newline at end of file
diff --git a/android/view/LayoutInflater.java b/android/view/LayoutInflater.java
new file mode 100644
index 0000000..df78827
--- /dev/null
+++ b/android/view/LayoutInflater.java
@@ -0,0 +1,1366 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.annotation.UiContext;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Canvas;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.widget.FrameLayout;
+
+import com.android.internal.R;
+
+import dalvik.system.PathClassLoader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * Instantiates a layout XML file into its corresponding {@link android.view.View}
+ * objects. It is never used directly. Instead, use
+ * {@link android.app.Activity#getLayoutInflater()} or
+ * {@link Context#getSystemService} to retrieve a standard LayoutInflater instance
+ * that is already hooked up to the current context and correctly configured
+ * for the device you are running on.
+ * <p>
+ * To create a new LayoutInflater with an additional {@link Factory} for your
+ * own views, you can use {@link #cloneInContext} to clone an existing
+ * ViewFactory, and then call {@link #setFactory} on it to include your
+ * Factory.
+ * <p>
+ * For performance reasons, view inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use LayoutInflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource
+ * (R.<em>something</em> file.)
+ * <p>
+ * <strong>Note:</strong> This class is <strong>not</strong> thread-safe and a given
+ * instance should only be accessed by a single thread.
+ */
+@SystemService(Context.LAYOUT_INFLATER_SERVICE)
+public abstract class LayoutInflater {
+
+ private static final String TAG = LayoutInflater.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final String COMPILED_VIEW_DEX_FILE_NAME = "/compiled_view.dex";
+ /**
+ * Whether or not we use the precompiled layout.
+ */
+ private static final String USE_PRECOMPILED_LAYOUT = "view.precompiled_layout_enabled";
+
+ /** Empty stack trace used to avoid log spam in re-throw exceptions. */
+ private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
+
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ // TODO(b/182007470): Use @ConfigurationContext instead
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ @UiContext
+ protected final Context mContext;
+
+ // these are optional, set by the caller
+ /**
+ * If any developer has desire to change this value, they should instead use
+ * {@link #cloneInContext(Context)} and set the new factory in thew newly-created
+ * LayoutInflater.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private boolean mFactorySet;
+ @UnsupportedAppUsage
+ private Factory mFactory;
+ @UnsupportedAppUsage
+ private Factory2 mFactory2;
+ @UnsupportedAppUsage
+ private Factory2 mPrivateFactory;
+ private Filter mFilter;
+
+ // Indicates whether we should try to inflate layouts using a precompiled layout instead of
+ // inflating from the XML resource.
+ private boolean mUseCompiledView;
+ // This variable holds the classloader that will be used to look for precompiled layouts. The
+ // The classloader includes the generated compiled_view.dex file.
+ private ClassLoader mPrecompiledClassLoader;
+
+ /**
+ * This is not a public API. Two APIs are now available to alleviate the need to access
+ * this directly: {@link #createView(Context, String, String, AttributeSet)} and
+ * {@link #onCreateView(Context, View, String, AttributeSet)}.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ final Object[] mConstructorArgs = new Object[2];
+
+ @UnsupportedAppUsage
+ static final Class<?>[] mConstructorSignature = new Class[] {
+ Context.class, AttributeSet.class};
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769490)
+ private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
+ new HashMap<String, Constructor<? extends View>>();
+
+ private HashMap<String, Boolean> mFilterMap;
+
+ private TypedValue mTempValue;
+
+ private static final String TAG_MERGE = "merge";
+ private static final String TAG_INCLUDE = "include";
+ private static final String TAG_1995 = "blink";
+ private static final String TAG_REQUEST_FOCUS = "requestFocus";
+ private static final String TAG_TAG = "tag";
+
+ private static final String ATTR_LAYOUT = "layout";
+
+ @UnsupportedAppUsage
+ private static final int[] ATTRS_THEME = new int[] {
+ com.android.internal.R.attr.theme };
+
+ /**
+ * Hook to allow clients of the LayoutInflater to restrict the set of Views that are allowed
+ * to be inflated.
+ *
+ */
+ public interface Filter {
+ /**
+ * Hook to allow clients of the LayoutInflater to restrict the set of Views
+ * that are allowed to be inflated.
+ *
+ * @param clazz The class object for the View that is about to be inflated
+ *
+ * @return True if this class is allowed to be inflated, or false otherwise
+ */
+ @SuppressWarnings("unchecked")
+ boolean onLoadClass(Class clazz);
+ }
+
+ public interface Factory {
+ /**
+ * Hook you can supply that is called when inflating from a LayoutInflater.
+ * You can use this to customize the tag names available in your XML
+ * layout files.
+ *
+ * <p>
+ * Note that it is good practice to prefix these custom names with your
+ * package (i.e., com.coolcompany.apps) to avoid conflicts with system
+ * names.
+ *
+ * @param name Tag name to be inflated.
+ * @param context The context the view is being created in.
+ * @param attrs Inflation attributes as specified in XML file.
+ *
+ * @return View Newly created view. Return null for the default
+ * behavior.
+ */
+ @Nullable
+ View onCreateView(@NonNull String name, @NonNull Context context,
+ @NonNull AttributeSet attrs);
+ }
+
+ public interface Factory2 extends Factory {
+ /**
+ * Version of {@link #onCreateView(String, Context, AttributeSet)}
+ * that also supplies the parent that the view created view will be
+ * placed in.
+ *
+ * @param parent The parent that the created view will be placed
+ * in; <em>note that this may be null</em>.
+ * @param name Tag name to be inflated.
+ * @param context The context the view is being created in.
+ * @param attrs Inflation attributes as specified in XML file.
+ *
+ * @return View Newly created view. Return null for the default
+ * behavior.
+ */
+ @Nullable
+ View onCreateView(@Nullable View parent, @NonNull String name,
+ @NonNull Context context, @NonNull AttributeSet attrs);
+ }
+
+ private static class FactoryMerger implements Factory2 {
+ private final Factory mF1, mF2;
+ private final Factory2 mF12, mF22;
+
+ FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
+ mF1 = f1;
+ mF2 = f2;
+ mF12 = f12;
+ mF22 = f22;
+ }
+
+ @Nullable
+ public View onCreateView(@NonNull String name, @NonNull Context context,
+ @NonNull AttributeSet attrs) {
+ View v = mF1.onCreateView(name, context, attrs);
+ if (v != null) return v;
+ return mF2.onCreateView(name, context, attrs);
+ }
+
+ @Nullable
+ public View onCreateView(@Nullable View parent, @NonNull String name,
+ @NonNull Context context, @NonNull AttributeSet attrs) {
+ View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
+ : mF1.onCreateView(name, context, attrs);
+ if (v != null) return v;
+ return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
+ : mF2.onCreateView(name, context, attrs);
+ }
+ }
+
+ /**
+ * Create a new LayoutInflater instance associated with a particular Context.
+ * Applications will almost always want to use
+ * {@link Context#getSystemService Context.getSystemService()} to retrieve
+ * the standard {@link Context#LAYOUT_INFLATER_SERVICE Context.INFLATER_SERVICE}.
+ *
+ * @param context The Context in which this LayoutInflater will create its
+ * Views; most importantly, this supplies the theme from which the default
+ * values for their attributes are retrieved.
+ */
+ protected LayoutInflater(Context context) {
+ StrictMode.assertConfigurationContext(context, "LayoutInflater");
+ mContext = context;
+ initPrecompiledViews();
+ }
+
+ /**
+ * Create a new LayoutInflater instance that is a copy of an existing
+ * LayoutInflater, optionally with its Context changed. For use in
+ * implementing {@link #cloneInContext}.
+ *
+ * @param original The original LayoutInflater to copy.
+ * @param newContext The new Context to use.
+ */
+ protected LayoutInflater(LayoutInflater original, Context newContext) {
+ StrictMode.assertConfigurationContext(newContext, "LayoutInflater");
+ mContext = newContext;
+ mFactory = original.mFactory;
+ mFactory2 = original.mFactory2;
+ mPrivateFactory = original.mPrivateFactory;
+ setFilter(original.mFilter);
+ initPrecompiledViews();
+ }
+
+ /**
+ * Obtains the LayoutInflater from the given context.
+ */
+ public static LayoutInflater from(@UiContext Context context) {
+ LayoutInflater LayoutInflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ if (LayoutInflater == null) {
+ throw new AssertionError("LayoutInflater not found.");
+ }
+ return LayoutInflater;
+ }
+
+ /**
+ * Create a copy of the existing LayoutInflater object, with the copy
+ * pointing to a different Context than the original. This is used by
+ * {@link ContextThemeWrapper} to create a new LayoutInflater to go along
+ * with the new Context theme.
+ *
+ * @param newContext The new Context to associate with the new LayoutInflater.
+ * May be the same as the original Context if desired.
+ *
+ * @return Returns a brand spanking new LayoutInflater object associated with
+ * the given Context.
+ */
+ public abstract LayoutInflater cloneInContext(Context newContext);
+
+ /**
+ * Return the context we are running in, for access to resources, class
+ * loader, etc.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the current {@link Factory} (or null). This is called on each element
+ * name. If the factory returns a View, add that to the hierarchy. If it
+ * returns null, proceed to call onCreateView(name).
+ */
+ public final Factory getFactory() {
+ return mFactory;
+ }
+
+ /**
+ * Return the current {@link Factory2}. Returns null if no factory is set
+ * or the set factory does not implement the {@link Factory2} interface.
+ * This is called on each element
+ * name. If the factory returns a View, add that to the hierarchy. If it
+ * returns null, proceed to call onCreateView(name).
+ */
+ public final Factory2 getFactory2() {
+ return mFactory2;
+ }
+
+ /**
+ * Attach a custom Factory interface for creating views while using
+ * this LayoutInflater. This must not be null, and can only be set once;
+ * after setting, you can not change the factory. This is
+ * called on each element name as the xml is parsed. If the factory returns
+ * a View, that is added to the hierarchy. If it returns null, the next
+ * factory default {@link #onCreateView} method is called.
+ *
+ * <p>If you have an existing
+ * LayoutInflater and want to add your own factory to it, use
+ * {@link #cloneInContext} to clone the existing instance and then you
+ * can use this function (once) on the returned new instance. This will
+ * merge your own factory with whatever factory the original instance is
+ * using.
+ */
+ public void setFactory(Factory factory) {
+ if (mFactorySet) {
+ throw new IllegalStateException("A factory has already been set on this LayoutInflater");
+ }
+ if (factory == null) {
+ throw new NullPointerException("Given factory can not be null");
+ }
+ mFactorySet = true;
+ if (mFactory == null) {
+ mFactory = factory;
+ } else {
+ mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
+ }
+ }
+
+ /**
+ * Like {@link #setFactory}, but allows you to set a {@link Factory2}
+ * interface.
+ */
+ public void setFactory2(Factory2 factory) {
+ if (mFactorySet) {
+ throw new IllegalStateException("A factory has already been set on this LayoutInflater");
+ }
+ if (factory == null) {
+ throw new NullPointerException("Given factory can not be null");
+ }
+ mFactorySet = true;
+ if (mFactory == null) {
+ mFactory = mFactory2 = factory;
+ } else {
+ mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
+ }
+ }
+
+ /**
+ * @hide for use by framework
+ */
+ @UnsupportedAppUsage
+ public void setPrivateFactory(Factory2 factory) {
+ if (mPrivateFactory == null) {
+ mPrivateFactory = factory;
+ } else {
+ mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
+ }
+ }
+
+ /**
+ * @return The {@link Filter} currently used by this LayoutInflater to restrict the set of Views
+ * that are allowed to be inflated.
+ */
+ public Filter getFilter() {
+ return mFilter;
+ }
+
+ /**
+ * Sets the {@link Filter} to by this LayoutInflater. If a view is attempted to be inflated
+ * which is not allowed by the {@link Filter}, the {@link #inflate(int, ViewGroup)} call will
+ * throw an {@link InflateException}. This filter will replace any previous filter set on this
+ * LayoutInflater.
+ *
+ * @param filter The Filter which restricts the set of Views that are allowed to be inflated.
+ * This filter will replace any previous filter set on this LayoutInflater.
+ */
+ public void setFilter(Filter filter) {
+ mFilter = filter;
+ if (filter != null) {
+ mFilterMap = new HashMap<String, Boolean>();
+ }
+ }
+
+ private void initPrecompiledViews() {
+ // Precompiled layouts are not supported in this release.
+ boolean enabled = false;
+ initPrecompiledViews(enabled);
+ }
+
+ private void initPrecompiledViews(boolean enablePrecompiledViews) {
+ mUseCompiledView = enablePrecompiledViews;
+
+ if (!mUseCompiledView) {
+ mPrecompiledClassLoader = null;
+ return;
+ }
+
+ // Make sure the application allows code generation
+ ApplicationInfo appInfo = mContext.getApplicationInfo();
+ if (appInfo.isEmbeddedDexUsed() || appInfo.isPrivilegedApp()) {
+ mUseCompiledView = false;
+ return;
+ }
+
+ // Try to load the precompiled layout file.
+ try {
+ mPrecompiledClassLoader = mContext.getClassLoader();
+ String dexFile = mContext.getCodeCacheDir() + COMPILED_VIEW_DEX_FILE_NAME;
+ if (new File(dexFile).exists()) {
+ mPrecompiledClassLoader = new PathClassLoader(dexFile, mPrecompiledClassLoader);
+ } else {
+ // If the precompiled layout file doesn't exist, then disable precompiled
+ // layouts.
+ mUseCompiledView = false;
+ }
+ } catch (Throwable e) {
+ if (DEBUG) {
+ Log.e(TAG, "Failed to initialized precompiled views layouts", e);
+ }
+ mUseCompiledView = false;
+ }
+ if (!mUseCompiledView) {
+ mPrecompiledClassLoader = null;
+ }
+ }
+
+ /**
+ * @hide for use by CTS tests
+ */
+ @TestApi
+ public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
+ initPrecompiledViews(enablePrecompiledLayouts);
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified xml resource. Throws
+ * {@link InflateException} if there is an error.
+ *
+ * @param resource ID for an XML layout resource to load (e.g.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional view to be the parent of the generated hierarchy.
+ * @return The root View of the inflated hierarchy. If root was supplied,
+ * this is the root View; otherwise it is the root of the inflated
+ * XML file.
+ */
+ public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
+ return inflate(resource, root, root != null);
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified xml node. Throws
+ * {@link InflateException} if there is an error. *
+ * <p>
+ * <em><strong>Important</strong></em> For performance
+ * reasons, view inflation relies heavily on pre-processing of XML files
+ * that is done at build time. Therefore, it is not currently possible to
+ * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the view
+ * hierarchy.
+ * @param root Optional view to be the parent of the generated hierarchy.
+ * @return The root View of the inflated hierarchy. If root was supplied,
+ * this is the root View; otherwise it is the root of the inflated
+ * XML file.
+ */
+ public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
+ return inflate(parser, root, root != null);
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified xml resource. Throws
+ * {@link InflateException} if there is an error.
+ *
+ * @param resource ID for an XML layout resource to load (e.g.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional view to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of LayoutParams values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> is false.)
+ * @param attachToRoot Whether the inflated hierarchy should be attached to
+ * the root parameter? If false, root is only used to create the
+ * correct subclass of LayoutParams for the root view in the XML.
+ * @return The root View of the inflated hierarchy. If root was supplied and
+ * attachToRoot is true, this is root; otherwise it is the root of
+ * the inflated XML file.
+ */
+ public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
+ final Resources res = getContext().getResources();
+ if (DEBUG) {
+ Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ + Integer.toHexString(resource) + ")");
+ }
+
+ View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
+ if (view != null) {
+ return view;
+ }
+ XmlResourceParser parser = res.getLayout(resource);
+ try {
+ return inflate(parser, root, attachToRoot);
+ } finally {
+ parser.close();
+ }
+ }
+
+ private @Nullable
+ View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
+ boolean attachToRoot) {
+ if (!mUseCompiledView) {
+ return null;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
+
+ // Try to inflate using a precompiled layout.
+ String pkg = res.getResourcePackageName(resource);
+ String layout = res.getResourceEntryName(resource);
+
+ try {
+ Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
+ Method inflater = clazz.getMethod(layout, Context.class, int.class);
+ View view = (View) inflater.invoke(null, mContext, resource);
+
+ if (view != null && root != null) {
+ // We were able to use the precompiled inflater, but now we need to do some work to
+ // attach the view to the root correctly.
+ XmlResourceParser parser = res.getLayout(resource);
+ try {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ advanceToRootNode(parser);
+ ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
+
+ if (attachToRoot) {
+ root.addView(view, params);
+ } else {
+ view.setLayoutParams(params);
+ }
+ } finally {
+ parser.close();
+ }
+ }
+
+ return view;
+ } catch (Throwable e) {
+ if (DEBUG) {
+ Log.e(TAG, "Failed to use precompiled view", e);
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ return null;
+ }
+
+ /**
+ * Advances the given parser to the first START_TAG. Throws InflateException if no start tag is
+ * found.
+ */
+ private void advanceToRootNode(XmlPullParser parser)
+ throws InflateException, IOException, XmlPullParserException {
+ // Look for the root node.
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ // Empty
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new InflateException(parser.getPositionDescription()
+ + ": No start tag found!");
+ }
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified XML node. Throws
+ * {@link InflateException} if there is an error.
+ * <p>
+ * <em><strong>Important</strong></em> For performance
+ * reasons, view inflation relies heavily on pre-processing of XML files
+ * that is done at build time. Therefore, it is not currently possible to
+ * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the view
+ * hierarchy.
+ * @param root Optional view to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of LayoutParams values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> is false.)
+ * @param attachToRoot Whether the inflated hierarchy should be attached to
+ * the root parameter? If false, root is only used to create the
+ * correct subclass of LayoutParams for the root view in the XML.
+ * @return The root View of the inflated hierarchy. If root was supplied and
+ * attachToRoot is true, this is root; otherwise it is the root of
+ * the inflated XML file.
+ */
+ public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
+ synchronized (mConstructorArgs) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
+
+ final Context inflaterContext = mContext;
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ Context lastContext = (Context) mConstructorArgs[0];
+ mConstructorArgs[0] = inflaterContext;
+ View result = root;
+
+ try {
+ advanceToRootNode(parser);
+ final String name = parser.getName();
+
+ if (DEBUG) {
+ System.out.println("**************************");
+ System.out.println("Creating root view: "
+ + name);
+ System.out.println("**************************");
+ }
+
+ if (TAG_MERGE.equals(name)) {
+ if (root == null || !attachToRoot) {
+ throw new InflateException("<merge /> can be used only with a valid "
+ + "ViewGroup root and attachToRoot=true");
+ }
+
+ rInflate(parser, root, inflaterContext, attrs, false);
+ } else {
+ // Temp is the root view that was found in the xml
+ final View temp = createViewFromTag(root, name, inflaterContext, attrs);
+
+ ViewGroup.LayoutParams params = null;
+
+ if (root != null) {
+ if (DEBUG) {
+ System.out.println("Creating params from root: " +
+ root);
+ }
+ // Create layout params that match root, if supplied
+ params = root.generateLayoutParams(attrs);
+ if (!attachToRoot) {
+ // Set the layout params for temp if we are not
+ // attaching. (If we are, we use addView, below)
+ temp.setLayoutParams(params);
+ }
+ }
+
+ if (DEBUG) {
+ System.out.println("-----> start inflating children");
+ }
+
+ // Inflate all children under temp against its context.
+ rInflateChildren(parser, temp, attrs, true);
+
+ if (DEBUG) {
+ System.out.println("-----> done inflating children");
+ }
+
+ // We are supposed to attach all the views we found (int temp)
+ // to root. Do that now.
+ if (root != null && attachToRoot) {
+ root.addView(temp, params);
+ }
+
+ // Decide whether to return the root that was passed in or the
+ // top view found in xml.
+ if (root == null || !attachToRoot) {
+ result = temp;
+ }
+ }
+
+ } catch (XmlPullParserException e) {
+ final InflateException ie = new InflateException(e.getMessage(), e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+ } catch (Exception e) {
+ final InflateException ie = new InflateException(
+ getParserStateDescription(inflaterContext, attrs)
+ + ": " + e.getMessage(), e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+ } finally {
+ // Don't retain static reference on context.
+ mConstructorArgs[0] = lastContext;
+ mConstructorArgs[1] = null;
+
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ return result;
+ }
+ }
+
+ private static String getParserStateDescription(Context context, AttributeSet attrs) {
+ int sourceResId = Resources.getAttributeSetSourceResId(attrs);
+ if (sourceResId == Resources.ID_NULL) {
+ return attrs.getPositionDescription();
+ } else {
+ return attrs.getPositionDescription() + " in "
+ + context.getResources().getResourceName(sourceResId);
+ }
+ }
+
+ private static final ClassLoader BOOT_CLASS_LOADER = LayoutInflater.class.getClassLoader();
+
+ private final boolean verifyClassLoader(Constructor<? extends View> constructor) {
+ final ClassLoader constructorLoader = constructor.getDeclaringClass().getClassLoader();
+ if (constructorLoader == BOOT_CLASS_LOADER) {
+ // fast path for boot class loader (most common case?) - always ok
+ return true;
+ }
+ // in all normal cases (no dynamic code loading), we will exit the following loop on the
+ // first iteration (i.e. when the declaring classloader is the contexts class loader).
+ ClassLoader cl = mContext.getClassLoader();
+ do {
+ if (constructorLoader == cl) {
+ return true;
+ }
+ cl = cl.getParent();
+ } while (cl != null);
+ return false;
+ }
+ /**
+ * Low-level function for instantiating a view by name. This attempts to
+ * instantiate a view class of the given <var>name</var> found in this
+ * LayoutInflater's ClassLoader. To use an explicit Context in the View
+ * constructor, use {@link #createView(Context, String, String, AttributeSet)} instead.
+ *
+ * <p>
+ * There are two things that can happen in an error case: either the
+ * exception describing the error will be thrown, or a null will be
+ * returned. You must deal with both possibilities -- the former will happen
+ * the first time createView() is called for a class of a particular name,
+ * the latter every time there-after for that class name.
+ *
+ * @param name The full name of the class to be instantiated.
+ * @param attrs The XML attributes supplied for this instance.
+ *
+ * @return View The newly instantiated view, or null.
+ */
+ public final View createView(String name, String prefix, AttributeSet attrs)
+ throws ClassNotFoundException, InflateException {
+ Context context = (Context) mConstructorArgs[0];
+ if (context == null) {
+ context = mContext;
+ }
+ return createView(context, name, prefix, attrs);
+ }
+
+ /**
+ * Low-level function for instantiating a view by name. This attempts to
+ * instantiate a view class of the given <var>name</var> found in this
+ * LayoutInflater's ClassLoader.
+ *
+ * <p>
+ * There are two things that can happen in an error case: either the
+ * exception describing the error will be thrown, or a null will be
+ * returned. You must deal with both possibilities -- the former will happen
+ * the first time createView() is called for a class of a particular name,
+ * the latter every time there-after for that class name.
+ *
+ * @param viewContext The context used as the context parameter of the View constructor
+ * @param name The full name of the class to be instantiated.
+ * @param attrs The XML attributes supplied for this instance.
+ *
+ * @return View The newly instantiated view, or null.
+ */
+ @Nullable
+ public final View createView(@NonNull Context viewContext, @NonNull String name,
+ @Nullable String prefix, @Nullable AttributeSet attrs)
+ throws ClassNotFoundException, InflateException {
+ Objects.requireNonNull(viewContext);
+ Objects.requireNonNull(name);
+ Constructor<? extends View> constructor = sConstructorMap.get(name);
+ if (constructor != null && !verifyClassLoader(constructor)) {
+ constructor = null;
+ sConstructorMap.remove(name);
+ }
+ Class<? extends View> clazz = null;
+
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
+
+ if (constructor == null) {
+ // Class not found in the cache, see if it's real, and try to add it
+ clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
+ mContext.getClassLoader()).asSubclass(View.class);
+
+ if (mFilter != null && clazz != null) {
+ boolean allowed = mFilter.onLoadClass(clazz);
+ if (!allowed) {
+ failNotAllowed(name, prefix, viewContext, attrs);
+ }
+ }
+ constructor = clazz.getConstructor(mConstructorSignature);
+ constructor.setAccessible(true);
+ sConstructorMap.put(name, constructor);
+ } else {
+ // If we have a filter, apply it to cached constructor
+ if (mFilter != null) {
+ // Have we seen this name before?
+ Boolean allowedState = mFilterMap.get(name);
+ if (allowedState == null) {
+ // New class -- remember whether it is allowed
+ clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
+ mContext.getClassLoader()).asSubclass(View.class);
+
+ boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
+ mFilterMap.put(name, allowed);
+ if (!allowed) {
+ failNotAllowed(name, prefix, viewContext, attrs);
+ }
+ } else if (allowedState.equals(Boolean.FALSE)) {
+ failNotAllowed(name, prefix, viewContext, attrs);
+ }
+ }
+ }
+
+ Object lastContext = mConstructorArgs[0];
+ mConstructorArgs[0] = viewContext;
+ Object[] args = mConstructorArgs;
+ args[1] = attrs;
+
+ try {
+ final View view = constructor.newInstance(args);
+ if (view instanceof ViewStub) {
+ // Use the same context when inflating ViewStub later.
+ final ViewStub viewStub = (ViewStub) view;
+ viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
+ }
+ return view;
+ } finally {
+ mConstructorArgs[0] = lastContext;
+ }
+ } catch (NoSuchMethodException e) {
+ final InflateException ie = new InflateException(
+ getParserStateDescription(viewContext, attrs)
+ + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+
+ } catch (ClassCastException e) {
+ // If loaded class is not a View subclass
+ final InflateException ie = new InflateException(
+ getParserStateDescription(viewContext, attrs)
+ + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+ } catch (ClassNotFoundException e) {
+ // If loadClass fails, we should propagate the exception.
+ throw e;
+ } catch (Exception e) {
+ final InflateException ie = new InflateException(
+ getParserStateDescription(viewContext, attrs) + ": Error inflating class "
+ + (clazz == null ? "<unknown>" : clazz.getName()), e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ /**
+ * Throw an exception because the specified class is not allowed to be inflated.
+ */
+ private void failNotAllowed(String name, String prefix, Context context, AttributeSet attrs) {
+ throw new InflateException(getParserStateDescription(context, attrs)
+ + ": Class not allowed to be inflated "+ (prefix != null ? (prefix + name) : name));
+ }
+
+ /**
+ * This routine is responsible for creating the correct subclass of View
+ * given the xml element name. Override it to handle custom view objects. If
+ * you override this in your subclass be sure to call through to
+ * super.onCreateView(name) for names you do not recognize.
+ *
+ * @param name The fully qualified class name of the View to be create.
+ * @param attrs An AttributeSet of attributes to apply to the View.
+ *
+ * @return View The View created.
+ */
+ protected View onCreateView(String name, AttributeSet attrs)
+ throws ClassNotFoundException {
+ return createView(name, "android.view.", attrs);
+ }
+
+ /**
+ * Version of {@link #onCreateView(String, AttributeSet)} that also
+ * takes the future parent of the view being constructed. The default
+ * implementation simply calls {@link #onCreateView(String, AttributeSet)}.
+ *
+ * @param parent The future parent of the returned view. <em>Note that
+ * this may be null.</em>
+ * @param name The fully qualified class name of the View to be create.
+ * @param attrs An AttributeSet of attributes to apply to the View.
+ *
+ * @return View The View created.
+ */
+ protected View onCreateView(View parent, String name, AttributeSet attrs)
+ throws ClassNotFoundException {
+ return onCreateView(name, attrs);
+ }
+
+ /**
+ * Version of {@link #onCreateView(View, String, AttributeSet)} that also
+ * takes the inflation context. The default
+ * implementation simply calls {@link #onCreateView(View, String, AttributeSet)}.
+ *
+ * @param viewContext The Context to be used as a constructor parameter for the View
+ * @param parent The future parent of the returned view. <em>Note that
+ * this may be null.</em>
+ * @param name The fully qualified class name of the View to be create.
+ * @param attrs An AttributeSet of attributes to apply to the View.
+ *
+ * @return View The View created.
+ */
+ @Nullable
+ public View onCreateView(@NonNull Context viewContext, @Nullable View parent,
+ @NonNull String name, @Nullable AttributeSet attrs)
+ throws ClassNotFoundException {
+ return onCreateView(parent, name, attrs);
+ }
+
+ /**
+ * Convenience method for calling through to the five-arg createViewFromTag
+ * method. This method passes {@code false} for the {@code ignoreThemeAttr}
+ * argument and should be used for everything except {@code >include>}
+ * tag parsing.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
+ return createViewFromTag(parent, name, context, attrs, false);
+ }
+
+ /**
+ * Creates a view from a tag name using the supplied attribute set.
+ * <p>
+ * <strong>Note:</strong> Default visibility so the BridgeInflater can
+ * override it.
+ *
+ * @param parent the parent view, used to inflate layout params
+ * @param name the name of the XML tag used to define the view
+ * @param context the inflation context for the view, typically the
+ * {@code parent} or base layout inflater context
+ * @param attrs the attribute set for the XML tag used to define the view
+ * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
+ * attribute (if set) for the view being inflated,
+ * {@code false} otherwise
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
+ boolean ignoreThemeAttr) {
+ if (name.equals("view")) {
+ name = attrs.getAttributeValue(null, "class");
+ }
+
+ // Apply a theme wrapper, if allowed and one is specified.
+ if (!ignoreThemeAttr) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
+ final int themeResId = ta.getResourceId(0, 0);
+ if (themeResId != 0) {
+ context = new ContextThemeWrapper(context, themeResId);
+ }
+ ta.recycle();
+ }
+
+ try {
+ View view = tryCreateView(parent, name, context, attrs);
+
+ if (view == null) {
+ final Object lastContext = mConstructorArgs[0];
+ mConstructorArgs[0] = context;
+ try {
+ if (-1 == name.indexOf('.')) {
+ view = onCreateView(context, parent, name, attrs);
+ } else {
+ view = createView(context, name, null, attrs);
+ }
+ } finally {
+ mConstructorArgs[0] = lastContext;
+ }
+ }
+
+ return view;
+ } catch (InflateException e) {
+ throw e;
+
+ } catch (ClassNotFoundException e) {
+ final InflateException ie = new InflateException(
+ getParserStateDescription(context, attrs)
+ + ": Error inflating class " + name, e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+
+ } catch (Exception e) {
+ final InflateException ie = new InflateException(
+ getParserStateDescription(context, attrs)
+ + ": Error inflating class " + name, e);
+ ie.setStackTrace(EMPTY_STACK_TRACE);
+ throw ie;
+ }
+ }
+
+ /**
+ * Tries to create a view from a tag name using the supplied attribute set.
+ *
+ * This method gives the factory provided by {@link LayoutInflater#setFactory} and
+ * {@link LayoutInflater#setFactory2} a chance to create a view. However, it does not apply all
+ * of the general view creation logic, and thus may return {@code null} for some tags. This
+ * method is used by {@link LayoutInflater#inflate} in creating {@code View} objects.
+ *
+ * @hide for use by precompiled layouts.
+ *
+ * @param parent the parent view, used to inflate layout params
+ * @param name the name of the XML tag used to define the view
+ * @param context the inflation context for the view, typically the
+ * {@code parent} or base layout inflater context
+ * @param attrs the attribute set for the XML tag used to define the view
+ */
+ @UnsupportedAppUsage(trackingBug = 122360734)
+ @Nullable
+ public final View tryCreateView(@Nullable View parent, @NonNull String name,
+ @NonNull Context context,
+ @NonNull AttributeSet attrs) {
+ if (name.equals(TAG_1995)) {
+ // Let's party like it's 1995!
+ return new BlinkLayout(context, attrs);
+ }
+
+ View view;
+ if (mFactory2 != null) {
+ view = mFactory2.onCreateView(parent, name, context, attrs);
+ } else if (mFactory != null) {
+ view = mFactory.onCreateView(name, context, attrs);
+ } else {
+ view = null;
+ }
+
+ if (view == null && mPrivateFactory != null) {
+ view = mPrivateFactory.onCreateView(parent, name, context, attrs);
+ }
+
+ return view;
+ }
+
+ /**
+ * Recursive method used to inflate internal (non-root) children. This
+ * method calls through to {@link #rInflate} using the parent context as
+ * the inflation context.
+ * <strong>Note:</strong> Default visibility so the BridgeInflater can
+ * call it.
+ */
+ final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
+ boolean finishInflate) throws XmlPullParserException, IOException {
+ rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
+ }
+
+ /**
+ * Recursive method used to descend down the xml hierarchy and instantiate
+ * views, instantiate their children, and then call onFinishInflate().
+ * <p>
+ * <strong>Note:</strong> Default visibility so the BridgeInflater can
+ * override it.
+ */
+ void rInflate(XmlPullParser parser, View parent, Context context,
+ AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
+
+ final int depth = parser.getDepth();
+ int type;
+ boolean pendingRequestFocus = false;
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ final String name = parser.getName();
+
+ if (TAG_REQUEST_FOCUS.equals(name)) {
+ pendingRequestFocus = true;
+ consumeChildElements(parser);
+ } else if (TAG_TAG.equals(name)) {
+ parseViewTag(parser, parent, attrs);
+ } else if (TAG_INCLUDE.equals(name)) {
+ if (parser.getDepth() == 0) {
+ throw new InflateException("<include /> cannot be the root element");
+ }
+ parseInclude(parser, context, parent, attrs);
+ } else if (TAG_MERGE.equals(name)) {
+ throw new InflateException("<merge /> must be the root element");
+ } else {
+ final View view = createViewFromTag(parent, name, context, attrs);
+ final ViewGroup viewGroup = (ViewGroup) parent;
+ final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
+ rInflateChildren(parser, view, attrs, true);
+ viewGroup.addView(view, params);
+ }
+ }
+
+ if (pendingRequestFocus) {
+ parent.restoreDefaultFocus();
+ }
+
+ if (finishInflate) {
+ parent.onFinishInflate();
+ }
+ }
+
+ /**
+ * Parses a <code><tag></code> element and sets a keyed tag on the
+ * containing View.
+ */
+ private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+ final Context context = view.getContext();
+ final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
+ final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
+ final CharSequence value = ta.getText(R.styleable.ViewTag_value);
+ view.setTag(key, value);
+ ta.recycle();
+
+ consumeChildElements(parser);
+ }
+
+ @UnsupportedAppUsage
+ private void parseInclude(XmlPullParser parser, Context context, View parent,
+ AttributeSet attrs) throws XmlPullParserException, IOException {
+ int type;
+
+ if (!(parent instanceof ViewGroup)) {
+ throw new InflateException("<include /> can only be used inside of a ViewGroup");
+ }
+
+ // Apply a theme wrapper, if requested. This is sort of a weird
+ // edge case, since developers think the <include> overwrites
+ // values in the AttributeSet of the included View. So, if the
+ // included View has a theme attribute, we'll need to ignore it.
+ final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
+ final int themeResId = ta.getResourceId(0, 0);
+ final boolean hasThemeOverride = themeResId != 0;
+ if (hasThemeOverride) {
+ context = new ContextThemeWrapper(context, themeResId);
+ }
+ ta.recycle();
+
+ // If the layout is pointing to a theme attribute, we have to
+ // massage the value to get a resource identifier out of it.
+ int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
+ if (layout == 0) {
+ final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
+ if (value == null || value.length() <= 0) {
+ throw new InflateException("You must specify a layout in the"
+ + " include tag: <include layout=\"@layout/layoutID\" />");
+ }
+
+ // Attempt to resolve the "?attr/name" string to an attribute
+ // within the default (e.g. application) package.
+ layout = context.getResources().getIdentifier(
+ value.substring(1), "attr", context.getPackageName());
+
+ }
+
+ // The layout might be referencing a theme attribute.
+ if (mTempValue == null) {
+ mTempValue = new TypedValue();
+ }
+ if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
+ layout = mTempValue.resourceId;
+ }
+
+ if (layout == 0) {
+ final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
+ throw new InflateException("You must specify a valid layout "
+ + "reference. The layout ID " + value + " is not valid.");
+ }
+
+ final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
+ (ViewGroup) parent, /*attachToRoot=*/true);
+ if (precompiled == null) {
+ final XmlResourceParser childParser = context.getResources().getLayout(layout);
+
+ try {
+ final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
+
+ while ((type = childParser.next()) != XmlPullParser.START_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ // Empty.
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new InflateException(getParserStateDescription(context, childAttrs)
+ + ": No start tag found!");
+ }
+
+ final String childName = childParser.getName();
+
+ if (TAG_MERGE.equals(childName)) {
+ // The <merge> tag doesn't support android:theme, so
+ // nothing special to do here.
+ rInflate(childParser, parent, context, childAttrs, false);
+ } else {
+ final View view = createViewFromTag(parent, childName,
+ context, childAttrs, hasThemeOverride);
+ final ViewGroup group = (ViewGroup) parent;
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.Include);
+ final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
+ final int visibility = a.getInt(R.styleable.Include_visibility, -1);
+ a.recycle();
+
+ // We try to load the layout params set in the <include /> tag.
+ // If the parent can't generate layout params (ex. missing width
+ // or height for the framework ViewGroups, though this is not
+ // necessarily true of all ViewGroups) then we expect it to throw
+ // a runtime exception.
+ // We catch this exception and set localParams accordingly: true
+ // means we successfully loaded layout params from the <include>
+ // tag, false means we need to rely on the included layout params.
+ ViewGroup.LayoutParams params = null;
+ try {
+ params = group.generateLayoutParams(attrs);
+ } catch (RuntimeException e) {
+ // Ignore, just fail over to child attrs.
+ }
+ if (params == null) {
+ params = group.generateLayoutParams(childAttrs);
+ }
+ view.setLayoutParams(params);
+
+ // Inflate all children.
+ rInflateChildren(childParser, view, childAttrs, true);
+
+ if (id != View.NO_ID) {
+ view.setId(id);
+ }
+
+ switch (visibility) {
+ case 0:
+ view.setVisibility(View.VISIBLE);
+ break;
+ case 1:
+ view.setVisibility(View.INVISIBLE);
+ break;
+ case 2:
+ view.setVisibility(View.GONE);
+ break;
+ }
+
+ group.addView(view);
+ }
+ } finally {
+ childParser.close();
+ }
+ }
+ LayoutInflater.consumeChildElements(parser);
+ }
+
+ /**
+ * <strong>Note:</strong> default visibility so that
+ * LayoutInflater_Delegate can call it.
+ */
+ final static void consumeChildElements(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int type;
+ final int currentDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
+ // Empty
+ }
+ }
+
+ private static class BlinkLayout extends FrameLayout {
+ private static final int MESSAGE_BLINK = 0x42;
+ private static final int BLINK_DELAY = 500;
+
+ private boolean mBlink;
+ private boolean mBlinkState;
+ private final Handler mHandler;
+
+ public BlinkLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mHandler = new Handler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MESSAGE_BLINK) {
+ if (mBlink) {
+ mBlinkState = !mBlinkState;
+ makeBlink();
+ }
+ invalidate();
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ private void makeBlink() {
+ Message message = mHandler.obtainMessage(MESSAGE_BLINK);
+ mHandler.sendMessageDelayed(message, BLINK_DELAY);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mBlink = true;
+ mBlinkState = true;
+
+ makeBlink();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mBlink = false;
+ mBlinkState = true;
+
+ mHandler.removeMessages(MESSAGE_BLINK);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mBlinkState) {
+ super.dispatchDraw(canvas);
+ }
+ }
+ }
+}
diff --git a/android/view/LayoutInflater_Delegate.java b/android/view/LayoutInflater_Delegate.java
new file mode 100644
index 0000000..cb446e7
--- /dev/null
+++ b/android/view/LayoutInflater_Delegate.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import java.io.IOException;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link LayoutInflater}
+ *
+ * Through the layoutlib_create tool, the original methods of LayoutInflater have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class LayoutInflater_Delegate {
+ private static final String TAG_MERGE = "merge";
+
+ private static final String ATTR_LAYOUT = "layout";
+
+ private static final int[] ATTRS_THEME = new int[] {
+ com.android.internal.R.attr.theme };
+
+ public static boolean sIsInInclude = false;
+
+ /**
+ * Recursive method used to descend down the xml hierarchy and instantiate
+ * views, instantiate their children, and then call onFinishInflate().
+ *
+ * This implementation just records the merge status before calling the default implementation.
+ */
+ @LayoutlibDelegate
+ /* package */ static void rInflate(LayoutInflater thisInflater, XmlPullParser parser,
+ View parent, Context context, AttributeSet attrs, boolean finishInflate)
+ throws XmlPullParserException, IOException {
+
+ if (finishInflate == false) {
+ // this is a merge rInflate!
+ if (thisInflater instanceof BridgeInflater) {
+ ((BridgeInflater) thisInflater).setIsInMerge(true);
+ }
+ }
+
+ // ---- START DEFAULT IMPLEMENTATION.
+
+ thisInflater.rInflate_Original(parser, parent, context, attrs, finishInflate);
+
+ // ---- END DEFAULT IMPLEMENTATION.
+
+ if (finishInflate == false) {
+ // this is a merge rInflate!
+ if (thisInflater instanceof BridgeInflater) {
+ ((BridgeInflater) thisInflater).setIsInMerge(false);
+ }
+ }
+ }
+
+ @LayoutlibDelegate
+ public static void parseInclude(LayoutInflater thisInflater, XmlPullParser parser,
+ Context context, View parent, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+ int type;
+
+ if (parent instanceof ViewGroup) {
+ // Apply a theme wrapper, if requested. This is sort of a weird
+ // edge case, since developers think the <include> overwrites
+ // values in the AttributeSet of the included View. So, if the
+ // included View has a theme attribute, we'll need to ignore it.
+ final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
+ final int themeResId = ta.getResourceId(0, 0);
+ final boolean hasThemeOverride = themeResId != 0;
+ if (hasThemeOverride) {
+ context = new ContextThemeWrapper(context, themeResId);
+ }
+ ta.recycle();
+
+ // If the layout is pointing to a theme attribute, we have to
+ // massage the value to get a resource identifier out of it.
+ int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
+ if (layout == 0) {
+ final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
+ if (value == null || value.length() <= 0) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "You must specify a layout in the"
+ + " include tag: <include layout=\"@layout/layoutID\" />", null, null);
+ LayoutInflater.consumeChildElements(parser);
+ return;
+ }
+
+ // Attempt to resolve the "?attr/name" string to an identifier.
+ layout = context.getResources().getIdentifier(value.substring(1), null, null);
+ }
+
+ // The layout might be referencing a theme attribute.
+ // ---- START CHANGES
+ if (layout != 0) {
+ final TypedValue tempValue = new TypedValue();
+ if (context.getTheme().resolveAttribute(layout, tempValue, true)) {
+ layout = tempValue.resourceId;
+ }
+ }
+ // ---- END CHANGES
+
+ if (layout == 0) {
+ final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
+ if (value == null) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "You must specify a layout in the"
+ + " include tag: <include layout=\"@layout/layoutID\" />", null, null);
+ } else {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "You must specify a valid layout "
+ + "reference. The layout ID " + value + " is not valid.", null, null);
+ }
+ } else {
+ final XmlResourceParser childParser =
+ thisInflater.getContext().getResources().getLayout(layout);
+
+ try {
+ final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
+
+ while ((type = childParser.next()) != XmlPullParser.START_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ // Empty.
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN,
+ childParser.getPositionDescription() + ": No start tag found!",
+ null, null);
+ LayoutInflater.consumeChildElements(parser);
+ return;
+ }
+
+ final String childName = childParser.getName();
+
+ if (TAG_MERGE.equals(childName)) {
+ // Inflate all children.
+ thisInflater.rInflate(childParser, parent, context, childAttrs, false);
+ } else {
+ final View view = thisInflater.createViewFromTag(parent, childName,
+ context, childAttrs, hasThemeOverride);
+ final ViewGroup group = (ViewGroup) parent;
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.Include);
+ final int id = a.getResourceId(
+ com.android.internal.R.styleable.Include_id, View.NO_ID);
+ final int visibility = a.getInt(
+ com.android.internal.R.styleable.Include_visibility, -1);
+ a.recycle();
+
+ // We try to load the layout params set in the <include /> tag. If
+ // they don't exist, we will rely on the layout params set in the
+ // included XML file.
+ // During a layoutparams generation, a runtime exception is thrown
+ // if either layout_width or layout_height is missing. We catch
+ // this exception and set localParams accordingly: true means we
+ // successfully loaded layout params from the <include /> tag,
+ // false means we need to rely on the included layout params.
+ ViewGroup.LayoutParams params = null;
+ try {
+ // ---- START CHANGES
+ sIsInInclude = true;
+ // ---- END CHANGES
+
+ params = group.generateLayoutParams(attrs);
+ } catch (RuntimeException ignored) {
+ // Ignore, just fail over to child attrs.
+ } finally {
+ // ---- START CHANGES
+ sIsInInclude = false;
+ // ---- END CHANGES
+ }
+ if (params == null) {
+ params = group.generateLayoutParams(childAttrs);
+ }
+ view.setLayoutParams(params);
+
+ // Inflate all children.
+ thisInflater.rInflateChildren(childParser, view, childAttrs, true);
+
+ if (id != View.NO_ID) {
+ view.setId(id);
+ }
+
+ switch (visibility) {
+ case 0:
+ view.setVisibility(View.VISIBLE);
+ break;
+ case 1:
+ view.setVisibility(View.INVISIBLE);
+ break;
+ case 2:
+ view.setVisibility(View.GONE);
+ break;
+ }
+
+ group.addView(view);
+ }
+ } finally {
+ childParser.close();
+ }
+ }
+ } else {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN,
+ "<include /> can only be used inside of a ViewGroup",
+ null, null);
+ }
+
+ LayoutInflater.consumeChildElements(parser);
+ }
+
+ @LayoutlibDelegate
+ /* package */ static void initPrecompiledViews(LayoutInflater thisInflater) {
+ initPrecompiledViews(thisInflater, false);
+ }
+
+ @LayoutlibDelegate
+ /* package */ static void initPrecompiledViews(LayoutInflater thisInflater,
+ boolean enablePrecompiledViews) {
+ thisInflater.initPrecompiledViews_Original(enablePrecompiledViews);
+ }
+}
diff --git a/android/view/MagnificationSpec.java b/android/view/MagnificationSpec.java
new file mode 100644
index 0000000..50d3113
--- /dev/null
+++ b/android/view/MagnificationSpec.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class represents spec for performing screen magnification.
+ *
+ * @hide
+ */
+public class MagnificationSpec implements Parcelable {
+
+ /** The magnification scaling factor. */
+ public float scale = 1.0f;
+
+ /**
+ * The X coordinate, in unscaled screen-relative pixels, around which
+ * magnification is focused.
+ */
+ public float offsetX;
+
+ /**
+ * The Y coordinate, in unscaled screen-relative pixels, around which
+ * magnification is focused.
+ */
+ public float offsetY;
+
+ public void initialize(float scale, float offsetX, float offsetY) {
+ if (scale < 1) {
+ throw new IllegalArgumentException("Scale must be greater than or equal to one!");
+ }
+ this.scale = scale;
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+ }
+
+ public boolean isNop() {
+ return scale == 1.0f && offsetX == 0 && offsetY == 0;
+ }
+
+ public void clear() {
+ scale = 1.0f;
+ offsetX = 0.0f;
+ offsetY = 0.0f;
+ }
+
+ public void setTo(MagnificationSpec other) {
+ scale = other.scale;
+ offsetX = other.offsetX;
+ offsetY = other.offsetY;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeFloat(scale);
+ parcel.writeFloat(offsetX);
+ parcel.writeFloat(offsetY);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+
+ final MagnificationSpec s = (MagnificationSpec) other;
+ return scale == s.scale && offsetX == s.offsetX && offsetY == s.offsetY;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (scale != +0.0f ? Float.floatToIntBits(scale) : 0);
+ result = 31 * result + (offsetX != +0.0f ? Float.floatToIntBits(offsetX) : 0);
+ result = 31 * result + (offsetY != +0.0f ? Float.floatToIntBits(offsetY) : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("<scale:");
+ builder.append(Float.toString(scale));
+ builder.append(",offsetX:");
+ builder.append(Float.toString(offsetX));
+ builder.append(",offsetY:");
+ builder.append(Float.toString(offsetY));
+ builder.append(">");
+ return builder.toString();
+ }
+
+ private void initFromParcel(Parcel parcel) {
+ scale = parcel.readFloat();
+ offsetX = parcel.readFloat();
+ offsetY = parcel.readFloat();
+ }
+
+ public static final @android.annotation.NonNull Creator<MagnificationSpec> CREATOR = new Creator<MagnificationSpec>() {
+ @Override
+ public MagnificationSpec[] newArray(int size) {
+ return new MagnificationSpec[size];
+ }
+
+ @Override
+ public MagnificationSpec createFromParcel(Parcel parcel) {
+ MagnificationSpec spec = new MagnificationSpec();
+ spec.initFromParcel(parcel);
+ return spec;
+ }
+ };
+}
diff --git a/android/view/Menu.java b/android/view/Menu.java
new file mode 100644
index 0000000..6d1f740
--- /dev/null
+++ b/android/view/Menu.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.StringRes;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+
+/**
+ * Interface for managing the items in a menu.
+ * <p>
+ * By default, every Activity supports an options menu of actions or options.
+ * You can add items to this menu and handle clicks on your additions. The
+ * easiest way of adding menu items is inflating an XML file into the
+ * {@link Menu} via {@link MenuInflater}. The easiest way of attaching code to
+ * clicks is via {@link Activity#onOptionsItemSelected(MenuItem)} and
+ * {@link Activity#onContextItemSelected(MenuItem)}.
+ * <p>
+ * Different menu types support different features:
+ * <ol>
+ * <li><b>Context menus</b>: Do not support item shortcuts and item icons.
+ * <li><b>Options menus</b>: The <b>icon menus</b> do not support item check
+ * marks and only show the item's
+ * {@link MenuItem#setTitleCondensed(CharSequence) condensed title}. The
+ * <b>expanded menus</b> (only available if six or more menu items are visible,
+ * reached via the 'More' item in the icon menu) do not show item icons, and
+ * item check marks are discouraged.
+ * <li><b>Sub menus</b>: Do not support item icons, or nested sub menus.
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating menus, read the
+ * <a href="{@docRoot}guide/topics/ui/menus.html">Menus</a> developer guide.</p>
+ * </div>
+ */
+public interface Menu {
+
+ /**
+ * This is the part of an order integer that the user can provide.
+ * @hide
+ */
+ static final int USER_MASK = 0x0000ffff;
+ /**
+ * Bit shift of the user portion of the order integer.
+ * @hide
+ */
+ static final int USER_SHIFT = 0;
+
+ /**
+ * This is the part of an order integer that supplies the category of the
+ * item.
+ * @hide
+ */
+ static final int CATEGORY_MASK = 0xffff0000;
+ /**
+ * Bit shift of the category portion of the order integer.
+ * @hide
+ */
+ static final int CATEGORY_SHIFT = 16;
+
+ /**
+ * A mask of all supported modifiers for MenuItem's keyboard shortcuts
+ */
+ static final int SUPPORTED_MODIFIERS_MASK = KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON
+ | KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON
+ | KeyEvent.META_FUNCTION_ON;
+
+ /**
+ * Value to use for group and item identifier integers when you don't care
+ * about them.
+ */
+ static final int NONE = 0;
+
+ /**
+ * First value for group and item identifier integers.
+ */
+ static final int FIRST = 1;
+
+ // Implementation note: Keep these CATEGORY_* in sync with the category enum
+ // in attrs.xml
+
+ /**
+ * Category code for the order integer for items/groups that are part of a
+ * container -- or/add this with your base value.
+ */
+ static final int CATEGORY_CONTAINER = 0x00010000;
+
+ /**
+ * Category code for the order integer for items/groups that are provided by
+ * the system -- or/add this with your base value.
+ */
+ static final int CATEGORY_SYSTEM = 0x00020000;
+
+ /**
+ * Category code for the order integer for items/groups that are
+ * user-supplied secondary (infrequently used) options -- or/add this with
+ * your base value.
+ */
+ static final int CATEGORY_SECONDARY = 0x00030000;
+
+ /**
+ * Category code for the order integer for items/groups that are
+ * alternative actions on the data that is currently displayed -- or/add
+ * this with your base value.
+ */
+ static final int CATEGORY_ALTERNATIVE = 0x00040000;
+
+ /**
+ * Flag for {@link #addIntentOptions}: if set, do not automatically remove
+ * any existing menu items in the same group.
+ */
+ static final int FLAG_APPEND_TO_GROUP = 0x0001;
+
+ /**
+ * Flag for {@link #performShortcut}: if set, do not close the menu after
+ * executing the shortcut.
+ */
+ static final int FLAG_PERFORM_NO_CLOSE = 0x0001;
+
+ /**
+ * Flag for {@link #performShortcut(int, KeyEvent, int)}: if set, always
+ * close the menu after executing the shortcut. Closing the menu also resets
+ * the prepared state.
+ */
+ static final int FLAG_ALWAYS_PERFORM_CLOSE = 0x0002;
+
+ /**
+ * Add a new item to the menu. This item displays the given title for its
+ * label.
+ *
+ * @param title The text to display for the item.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(CharSequence title);
+
+ /**
+ * Add a new item to the menu. This item displays the given title for its
+ * label.
+ *
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(@StringRes int titleRes);
+
+ /**
+ * Add a new item to the menu. This item displays the given title for its
+ * label.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a
+ * group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care
+ * about the order. See {@link MenuItem#getOrder()}.
+ * @param title The text to display for the item.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(int groupId, int itemId, int order, CharSequence title);
+
+ /**
+ * Variation on {@link #add(int, int, int, CharSequence)} that takes a
+ * string resource identifier instead of the string itself.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a
+ * group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care
+ * about the order. See {@link MenuItem#getOrder()}.
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(int groupId, int itemId, int order, @StringRes int titleRes);
+
+ /**
+ * Add a new sub-menu to the menu. This item displays the given title for
+ * its label. To modify other attributes on the submenu's menu item, use
+ * {@link SubMenu#getItem()}.
+ *
+ * @param title The text to display for the item.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(final CharSequence title);
+
+ /**
+ * Add a new sub-menu to the menu. This item displays the given title for
+ * its label. To modify other attributes on the submenu's menu item, use
+ * {@link SubMenu#getItem()}.
+ *
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(@StringRes final int titleRes);
+
+ /**
+ * Add a new sub-menu to the menu. This item displays the given
+ * <var>title</var> for its label. To modify other attributes on the
+ * submenu's menu item, use {@link SubMenu#getItem()}.
+ *<p>
+ * Note that you can only have one level of sub-menus, i.e. you cannnot add
+ * a subMenu to a subMenu: An {@link UnsupportedOperationException} will be
+ * thrown if you try.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a
+ * group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care
+ * about the order. See {@link MenuItem#getOrder()}.
+ * @param title The text to display for the item.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(final int groupId, final int itemId, int order, final CharSequence title);
+
+ /**
+ * Variation on {@link #addSubMenu(int, int, int, CharSequence)} that takes
+ * a string resource identifier for the title instead of the string itself.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care about the
+ * order. See {@link MenuItem#getOrder()}.
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(int groupId, int itemId, int order, @StringRes int titleRes);
+
+ /**
+ * Add a group of menu items corresponding to actions that can be performed
+ * for a particular Intent. The Intent is most often configured with a null
+ * action, the data that the current activity is working with, and includes
+ * either the {@link Intent#CATEGORY_ALTERNATIVE} or
+ * {@link Intent#CATEGORY_SELECTED_ALTERNATIVE} to find activities that have
+ * said they would like to be included as optional action. You can, however,
+ * use any Intent you want.
+ *
+ * <p>
+ * See {@link android.content.pm.PackageManager#queryIntentActivityOptions}
+ * for more * details on the <var>caller</var>, <var>specifics</var>, and
+ * <var>intent</var> arguments. The list returned by that function is used
+ * to populate the resulting menu items.
+ *
+ * <p>
+ * All of the menu items of possible options for the intent will be added
+ * with the given group and id. You can use the group to control ordering of
+ * the items in relation to other items in the menu. Normally this function
+ * will automatically remove any existing items in the menu in the same
+ * group and place a divider above and below the added items; this behavior
+ * can be modified with the <var>flags</var> parameter. For each of the
+ * generated items {@link MenuItem#setIntent} is called to associate the
+ * appropriate Intent with the item; this means the activity will
+ * automatically be started for you without having to do anything else.
+ *
+ * @param groupId The group identifier that the items should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if the items should not be in
+ * a group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the items. Use {@link #NONE} if you do not
+ * care about the order. See {@link MenuItem#getOrder()}.
+ * @param caller The current activity component name as defined by
+ * queryIntentActivityOptions().
+ * @param specifics Specific items to place first as defined by
+ * queryIntentActivityOptions().
+ * @param intent Intent describing the kinds of items to populate in the
+ * list as defined by queryIntentActivityOptions().
+ * @param flags Additional options controlling how the items are added.
+ * @param outSpecificItems Optional array in which to place the menu items
+ * that were generated for each of the <var>specifics</var> that were
+ * requested. Entries may be null if no activity was found for that
+ * specific action.
+ * @return The number of menu items that were added.
+ *
+ * @see #FLAG_APPEND_TO_GROUP
+ * @see MenuItem#setIntent
+ * @see android.content.pm.PackageManager#queryIntentActivityOptions
+ */
+ public int addIntentOptions(int groupId, int itemId, int order,
+ ComponentName caller, Intent[] specifics,
+ Intent intent, int flags, MenuItem[] outSpecificItems);
+
+ /**
+ * Remove the item with the given identifier.
+ *
+ * @param id The item to be removed. If there is no item with this
+ * identifier, nothing happens.
+ */
+ public void removeItem(int id);
+
+ /**
+ * Remove all items in the given group.
+ *
+ * @param groupId The group to be removed. If there are no items in this
+ * group, nothing happens.
+ */
+ public void removeGroup(int groupId);
+
+ /**
+ * Remove all existing items from the menu, leaving it empty as if it had
+ * just been created.
+ */
+ public void clear();
+
+ /**
+ * Control whether a particular group of items can show a check mark. This
+ * is similar to calling {@link MenuItem#setCheckable} on all of the menu items
+ * with the given group identifier, but in addition you can control whether
+ * this group contains a mutually-exclusive set items. This should be called
+ * after the items of the group have been added to the menu.
+ *
+ * @param group The group of items to operate on.
+ * @param checkable Set to true to allow a check mark, false to
+ * disallow. The default is false.
+ * @param exclusive If set to true, only one item in this group can be
+ * checked at a time; checking an item will automatically
+ * uncheck all others in the group. If set to false, each
+ * item can be checked independently of the others.
+ *
+ * @see MenuItem#setCheckable
+ * @see MenuItem#setChecked
+ */
+ public void setGroupCheckable(int group, boolean checkable, boolean exclusive);
+
+ /**
+ * Show or hide all menu items that are in the given group.
+ *
+ * @param group The group of items to operate on.
+ * @param visible If true the items are visible, else they are hidden.
+ *
+ * @see MenuItem#setVisible
+ */
+ public void setGroupVisible(int group, boolean visible);
+
+ /**
+ * Enable or disable all menu items that are in the given group.
+ *
+ * @param group The group of items to operate on.
+ * @param enabled If true the items will be enabled, else they will be disabled.
+ *
+ * @see MenuItem#setEnabled
+ */
+ public void setGroupEnabled(int group, boolean enabled);
+
+ /**
+ * Return whether the menu currently has item items that are visible.
+ *
+ * @return True if there is one or more item visible,
+ * else false.
+ */
+ public boolean hasVisibleItems();
+
+ /**
+ * Return the menu item with a particular identifier.
+ *
+ * @param id The identifier to find.
+ *
+ * @return The menu item object, or null if there is no item with
+ * this identifier.
+ */
+ public MenuItem findItem(int id);
+
+ /**
+ * Get the number of items in the menu. Note that this will change any
+ * times items are added or removed from the menu.
+ *
+ * @return The item count.
+ */
+ public int size();
+
+ /**
+ * Gets the menu item at the given index.
+ *
+ * @param index The index of the menu item to return.
+ * @return The menu item.
+ * @exception IndexOutOfBoundsException
+ * when {@code index < 0 || >= size()}
+ */
+ public MenuItem getItem(int index);
+
+ /**
+ * Closes the menu, if open.
+ */
+ public void close();
+
+ /**
+ * Execute the menu item action associated with the given shortcut
+ * character.
+ *
+ * @param keyCode The keycode of the shortcut key.
+ * @param event Key event message.
+ * @param flags Additional option flags or 0.
+ *
+ * @return If the given shortcut exists and is shown, returns
+ * true; else returns false.
+ *
+ * @see #FLAG_PERFORM_NO_CLOSE
+ */
+ public boolean performShortcut(int keyCode, KeyEvent event, int flags);
+
+ /**
+ * Is a keypress one of the defined shortcut keys for this window.
+ * @param keyCode the key code from {@link KeyEvent} to check.
+ * @param event the {@link KeyEvent} to use to help check.
+ */
+ boolean isShortcutKey(int keyCode, KeyEvent event);
+
+ /**
+ * Execute the menu item action associated with the given menu identifier.
+ *
+ * @param id Identifier associated with the menu item.
+ * @param flags Additional option flags or 0.
+ *
+ * @return If the given identifier exists and is shown, returns
+ * true; else returns false.
+ *
+ * @see #FLAG_PERFORM_NO_CLOSE
+ */
+ public boolean performIdentifierAction(int id, int flags);
+
+
+ /**
+ * Control whether the menu should be running in qwerty mode (alphabetic
+ * shortcuts) or 12-key mode (numeric shortcuts).
+ *
+ * @param isQwerty If true the menu will use alphabetic shortcuts; else it
+ * will use numeric shortcuts.
+ */
+ public void setQwertyMode(boolean isQwerty);
+
+ /**
+ * Enable or disable the group dividers.
+ */
+ default void setGroupDividerEnabled(boolean groupDividerEnabled) {
+ }
+}
\ No newline at end of file
diff --git a/android/view/MenuInflater.java b/android/view/MenuInflater.java
new file mode 100644
index 0000000..b6b11ab
--- /dev/null
+++ b/android/view/MenuInflater.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.MenuRes;
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.BlendMode;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.view.menu.MenuItemImpl;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+/**
+ * This class is used to instantiate menu XML files into Menu objects.
+ * <p>
+ * For performance reasons, menu inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource (R.
+ * <em>something</em> file.)
+ */
+public class MenuInflater {
+ private static final String LOG_TAG = "MenuInflater";
+
+ /** Menu tag name in XML. */
+ private static final String XML_MENU = "menu";
+
+ /** Group tag name in XML. */
+ private static final String XML_GROUP = "group";
+
+ /** Item tag name in XML. */
+ private static final String XML_ITEM = "item";
+
+ private static final int NO_ID = 0;
+
+ private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};
+
+ private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE;
+
+ private final Object[] mActionViewConstructorArguments;
+
+ private final Object[] mActionProviderConstructorArguments;
+
+ private Context mContext;
+ private Object mRealOwner;
+
+ /**
+ * Constructs a menu inflater.
+ *
+ * @see Activity#getMenuInflater()
+ */
+ public MenuInflater(Context context) {
+ mContext = context;
+ mActionViewConstructorArguments = new Object[] {context};
+ mActionProviderConstructorArguments = mActionViewConstructorArguments;
+ }
+
+ /**
+ * Constructs a menu inflater.
+ *
+ * @see Activity#getMenuInflater()
+ * @hide
+ */
+ public MenuInflater(Context context, Object realOwner) {
+ mContext = context;
+ mRealOwner = realOwner;
+ mActionViewConstructorArguments = new Object[] {context};
+ mActionProviderConstructorArguments = mActionViewConstructorArguments;
+ }
+
+ /**
+ * Inflate a menu hierarchy from the specified XML resource. Throws
+ * {@link InflateException} if there is an error.
+ *
+ * @param menuRes Resource ID for an XML layout resource to load (e.g.,
+ * <code>R.menu.main_activity</code>)
+ * @param menu The Menu to inflate into. The items and submenus will be
+ * added to this Menu.
+ */
+ public void inflate(@MenuRes int menuRes, Menu menu) {
+ XmlResourceParser parser = null;
+ try {
+ parser = mContext.getResources().getLayout(menuRes);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ parseMenu(parser, attrs, menu);
+ } catch (XmlPullParserException e) {
+ throw new InflateException("Error inflating menu XML", e);
+ } catch (IOException e) {
+ throw new InflateException("Error inflating menu XML", e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ /**
+ * Called internally to fill the given menu. If a sub menu is seen, it will
+ * call this recursively.
+ */
+ private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
+ throws XmlPullParserException, IOException {
+ MenuState menuState = new MenuState(menu);
+
+ int eventType = parser.getEventType();
+ String tagName;
+ boolean lookingForEndOfUnknownTag = false;
+ String unknownTagName = null;
+
+ // This loop will skip to the menu start tag
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ tagName = parser.getName();
+ if (tagName.equals(XML_MENU)) {
+ // Go to next tag
+ eventType = parser.next();
+ break;
+ }
+
+ throw new RuntimeException("Expecting menu, got " + tagName);
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ boolean reachedEndOfMenu = false;
+ while (!reachedEndOfMenu) {
+ switch (eventType) {
+ case XmlPullParser.START_TAG:
+ if (lookingForEndOfUnknownTag) {
+ break;
+ }
+
+ tagName = parser.getName();
+ if (tagName.equals(XML_GROUP)) {
+ menuState.readGroup(attrs);
+ } else if (tagName.equals(XML_ITEM)) {
+ menuState.readItem(attrs);
+ } else if (tagName.equals(XML_MENU)) {
+ // A menu start tag denotes a submenu for an item
+ SubMenu subMenu = menuState.addSubMenuItem();
+ registerMenu(subMenu, attrs);
+
+ // Parse the submenu into returned SubMenu
+ parseMenu(parser, attrs, subMenu);
+ } else {
+ lookingForEndOfUnknownTag = true;
+ unknownTagName = tagName;
+ }
+ break;
+
+ case XmlPullParser.END_TAG:
+ tagName = parser.getName();
+ if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
+ lookingForEndOfUnknownTag = false;
+ unknownTagName = null;
+ } else if (tagName.equals(XML_GROUP)) {
+ menuState.resetGroup();
+ } else if (tagName.equals(XML_ITEM)) {
+ // Add the item if it hasn't been added (if the item was
+ // a submenu, it would have been added already)
+ if (!menuState.hasAddedItem()) {
+ if (menuState.itemActionProvider != null &&
+ menuState.itemActionProvider.hasSubMenu()) {
+ registerMenu(menuState.addSubMenuItem(), attrs);
+ } else {
+ registerMenu(menuState.addItem(), attrs);
+ }
+ }
+ } else if (tagName.equals(XML_MENU)) {
+ reachedEndOfMenu = true;
+ }
+ break;
+
+ case XmlPullParser.END_DOCUMENT:
+ throw new RuntimeException("Unexpected end of document");
+ }
+
+ eventType = parser.next();
+ }
+ }
+
+ /**
+ * The method is a hook for layoutlib to do its magic.
+ * Nothing is needed outside of LayoutLib. However, it should not be deleted because it
+ * appears to do nothing.
+ */
+ private void registerMenu(@SuppressWarnings("unused") MenuItem item,
+ @SuppressWarnings("unused") AttributeSet set) {
+ }
+
+ /**
+ * The method is a hook for layoutlib to do its magic.
+ * Nothing is needed outside of LayoutLib. However, it should not be deleted because it
+ * appears to do nothing.
+ */
+ private void registerMenu(@SuppressWarnings("unused") SubMenu subMenu,
+ @SuppressWarnings("unused") AttributeSet set) {
+ }
+
+ // Needed by layoutlib.
+ /*package*/ Context getContext() {
+ return mContext;
+ }
+
+ private static class InflatedOnMenuItemClickListener
+ implements MenuItem.OnMenuItemClickListener {
+ private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
+
+ private Object mRealOwner;
+ private Method mMethod;
+
+ public InflatedOnMenuItemClickListener(Object realOwner, String methodName) {
+ mRealOwner = realOwner;
+ Class<?> c = realOwner.getClass();
+ try {
+ mMethod = c.getMethod(methodName, PARAM_TYPES);
+ } catch (Exception e) {
+ InflateException ex = new InflateException(
+ "Couldn't resolve menu item onClick handler " + methodName +
+ " in class " + c.getName());
+ ex.initCause(e);
+ throw ex;
+ }
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ try {
+ if (mMethod.getReturnType() == Boolean.TYPE) {
+ return (Boolean) mMethod.invoke(mRealOwner, item);
+ } else {
+ mMethod.invoke(mRealOwner, item);
+ return true;
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private Object getRealOwner() {
+ if (mRealOwner == null) {
+ mRealOwner = findRealOwner(mContext);
+ }
+ return mRealOwner;
+ }
+
+ private Object findRealOwner(Object owner) {
+ if (owner instanceof Activity) {
+ return owner;
+ }
+ if (owner instanceof ContextWrapper) {
+ return findRealOwner(((ContextWrapper) owner).getBaseContext());
+ }
+ return owner;
+ }
+
+ /**
+ * State for the current menu.
+ * <p>
+ * Groups can not be nested unless there is another menu (which will have
+ * its state class).
+ */
+ private class MenuState {
+ private Menu menu;
+
+ /*
+ * Group state is set on items as they are added, allowing an item to
+ * override its group state. (As opposed to set on items at the group end tag.)
+ */
+ private int groupId;
+ private int groupCategory;
+ private int groupOrder;
+ private int groupCheckable;
+ private boolean groupVisible;
+ private boolean groupEnabled;
+
+ private boolean itemAdded;
+ private int itemId;
+ private int itemCategoryOrder;
+ private CharSequence itemTitle;
+ private CharSequence itemTitleCondensed;
+ private int itemIconResId;
+ private ColorStateList itemIconTintList = null;
+ private BlendMode mItemIconBlendMode = null;
+ private char itemAlphabeticShortcut;
+ private int itemAlphabeticModifiers;
+ private char itemNumericShortcut;
+ private int itemNumericModifiers;
+ /**
+ * Sync to attrs.xml enum:
+ * - 0: none
+ * - 1: all
+ * - 2: exclusive
+ */
+ private int itemCheckable;
+ private boolean itemChecked;
+ private boolean itemVisible;
+ private boolean itemEnabled;
+
+ /**
+ * Sync to attrs.xml enum, values in MenuItem:
+ * - 0: never
+ * - 1: ifRoom
+ * - 2: always
+ * - -1: Safe sentinel for "no value".
+ */
+ private int itemShowAsAction;
+
+ private int itemActionViewLayout;
+ private String itemActionViewClassName;
+ private String itemActionProviderClassName;
+
+ private String itemListenerMethodName;
+
+ private ActionProvider itemActionProvider;
+
+ private CharSequence itemContentDescription;
+ private CharSequence itemTooltipText;
+
+ private static final int defaultGroupId = NO_ID;
+ private static final int defaultItemId = NO_ID;
+ private static final int defaultItemCategory = 0;
+ private static final int defaultItemOrder = 0;
+ private static final int defaultItemCheckable = 0;
+ private static final boolean defaultItemChecked = false;
+ private static final boolean defaultItemVisible = true;
+ private static final boolean defaultItemEnabled = true;
+
+ public MenuState(final Menu menu) {
+ this.menu = menu;
+
+ resetGroup();
+ }
+
+ public void resetGroup() {
+ groupId = defaultGroupId;
+ groupCategory = defaultItemCategory;
+ groupOrder = defaultItemOrder;
+ groupCheckable = defaultItemCheckable;
+ groupVisible = defaultItemVisible;
+ groupEnabled = defaultItemEnabled;
+ }
+
+ /**
+ * Called when the parser is pointing to a group tag.
+ */
+ public void readGroup(AttributeSet attrs) {
+ TypedArray a = mContext.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.MenuGroup);
+
+ groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);
+ groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);
+ groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);
+ groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);
+ groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);
+ groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);
+
+ a.recycle();
+ }
+
+ /**
+ * Called when the parser is pointing to an item tag.
+ */
+ public void readItem(AttributeSet attrs) {
+ TypedArray a = mContext.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.MenuItem);
+
+ // Inherit attributes from the group as default value
+ itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
+ final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
+ final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder);
+ itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
+ itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title);
+ itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed);
+ itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0);
+ if (a.hasValue(com.android.internal.R.styleable.MenuItem_iconTintMode)) {
+ mItemIconBlendMode = Drawable.parseBlendMode(a.getInt(
+ com.android.internal.R.styleable.MenuItem_iconTintMode, -1),
+ mItemIconBlendMode);
+ } else {
+ // Reset to null so that it's not carried over to the next item
+ mItemIconBlendMode = null;
+ }
+ if (a.hasValue(com.android.internal.R.styleable.MenuItem_iconTint)) {
+ itemIconTintList = a.getColorStateList(
+ com.android.internal.R.styleable.MenuItem_iconTint);
+ } else {
+ // Reset to null so that it's not carried over to the next item
+ itemIconTintList = null;
+ }
+
+ itemAlphabeticShortcut =
+ getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut));
+ itemAlphabeticModifiers =
+ a.getInt(com.android.internal.R.styleable.MenuItem_alphabeticModifiers,
+ KeyEvent.META_CTRL_ON);
+ itemNumericShortcut =
+ getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut));
+ itemNumericModifiers =
+ a.getInt(com.android.internal.R.styleable.MenuItem_numericModifiers,
+ KeyEvent.META_CTRL_ON);
+ if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) {
+ // Item has attribute checkable, use it
+ itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0;
+ } else {
+ // Item does not have attribute, use the group's (group can have one more state
+ // for checkable that represents the exclusive checkable)
+ itemCheckable = groupCheckable;
+ }
+ itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
+ itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
+ itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
+ itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1);
+ itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick);
+ itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0);
+ itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass);
+ itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass);
+
+ final boolean hasActionProvider = itemActionProviderClassName != null;
+ if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
+ itemActionProvider = newInstance(itemActionProviderClassName,
+ ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
+ mActionProviderConstructorArguments);
+ } else {
+ if (hasActionProvider) {
+ Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
+ + " Action view already specified.");
+ }
+ itemActionProvider = null;
+ }
+
+ itemContentDescription =
+ a.getText(com.android.internal.R.styleable.MenuItem_contentDescription);
+ itemTooltipText = a.getText(com.android.internal.R.styleable.MenuItem_tooltipText);
+
+ a.recycle();
+
+ itemAdded = false;
+ }
+
+ private char getShortcut(String shortcutString) {
+ if (shortcutString == null) {
+ return 0;
+ } else {
+ return shortcutString.charAt(0);
+ }
+ }
+
+ private void setItem(MenuItem item) {
+ item.setChecked(itemChecked)
+ .setVisible(itemVisible)
+ .setEnabled(itemEnabled)
+ .setCheckable(itemCheckable >= 1)
+ .setTitleCondensed(itemTitleCondensed)
+ .setIcon(itemIconResId)
+ .setAlphabeticShortcut(itemAlphabeticShortcut, itemAlphabeticModifiers)
+ .setNumericShortcut(itemNumericShortcut, itemNumericModifiers);
+
+ if (itemShowAsAction >= 0) {
+ item.setShowAsAction(itemShowAsAction);
+ }
+
+ if (mItemIconBlendMode != null) {
+ item.setIconTintBlendMode(mItemIconBlendMode);
+ }
+
+ if (itemIconTintList != null) {
+ item.setIconTintList(itemIconTintList);
+ }
+
+ if (itemListenerMethodName != null) {
+ if (mContext.isRestricted()) {
+ throw new IllegalStateException("The android:onClick attribute cannot "
+ + "be used within a restricted context");
+ }
+ item.setOnMenuItemClickListener(
+ new InflatedOnMenuItemClickListener(getRealOwner(), itemListenerMethodName));
+ }
+
+ if (item instanceof MenuItemImpl) {
+ MenuItemImpl impl = (MenuItemImpl) item;
+ if (itemCheckable >= 2) {
+ impl.setExclusiveCheckable(true);
+ }
+ }
+
+ boolean actionViewSpecified = false;
+ if (itemActionViewClassName != null) {
+ View actionView = (View) newInstance(itemActionViewClassName,
+ ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
+ item.setActionView(actionView);
+ actionViewSpecified = true;
+ }
+ if (itemActionViewLayout > 0) {
+ if (!actionViewSpecified) {
+ item.setActionView(itemActionViewLayout);
+ actionViewSpecified = true;
+ } else {
+ Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
+ + " Action view already specified.");
+ }
+ }
+ if (itemActionProvider != null) {
+ item.setActionProvider(itemActionProvider);
+ }
+
+ item.setContentDescription(itemContentDescription);
+ item.setTooltipText(itemTooltipText);
+ }
+
+ public MenuItem addItem() {
+ itemAdded = true;
+ MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);
+ setItem(item);
+ return item;
+ }
+
+ public SubMenu addSubMenuItem() {
+ itemAdded = true;
+ SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
+ setItem(subMenu.getItem());
+ return subMenu;
+ }
+
+ public boolean hasAddedItem() {
+ return itemAdded;
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> T newInstance(String className, Class<?>[] constructorSignature,
+ Object[] arguments) {
+ try {
+ Class<?> clazz = mContext.getClassLoader().loadClass(className);
+ Constructor<?> constructor = clazz.getConstructor(constructorSignature);
+ constructor.setAccessible(true);
+ return (T) constructor.newInstance(arguments);
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
+ }
+ return null;
+ }
+ }
+}
diff --git a/android/view/MenuInflater_Delegate.java b/android/view/MenuInflater_Delegate.java
new file mode 100644
index 0000000..818d741
--- /dev/null
+++ b/android/view/MenuInflater_Delegate.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.internal.view.menu.BridgeMenuItemImpl;
+import com.android.internal.view.menu.MenuView;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link MenuInflater}
+ * <p/>
+ * Through the layoutlib_create tool, the original methods of MenuInflater have been
+ * replaced by calls to methods of the same name in this delegate class.
+ * <p/>
+ * The main purpose of the class is to get the view key from the menu xml parser and add it to
+ * the menu item. The view key is used by the IDE to match the individual view elements to the
+ * corresponding xml tag in the menu/layout file.
+ * <p/>
+ * For Menus, the views may be reused and the {@link MenuItem} is a better object to hold the
+ * view key than the {@link MenuView.ItemView}. At the time of computation of the rest of {@link
+ * 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) {
+ if (menuItem instanceof BridgeMenuItemImpl) {
+ Context context = thisInflater.getContext();
+ context = BridgeContext.getBaseContext(context);
+ if (context instanceof BridgeContext) {
+ Object viewKey = BridgeInflater.getViewKeyFromParser(
+ attrs, ((BridgeContext) context), null, false);
+ ((BridgeMenuItemImpl) menuItem).setViewCookie(viewKey);
+ return;
+ }
+ }
+
+ String menuItemName = menuItem != null ? menuItem.getClass().getName() : null;
+ if (menuItemName == null ||
+ !menuItemName.startsWith("android.support.") ||
+ !menuItemName.startsWith("androidx.")) {
+ // 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(ILayoutLog.TAG_BROKEN,
+ "Action Bar Menu rendering may be incorrect.", null, null);
+ }
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void registerMenu(MenuInflater thisInflater, SubMenu subMenu,
+ AttributeSet parser) {
+ registerMenu(thisInflater, subMenu.getItem(), parser);
+ }
+
+}
diff --git a/android/view/MenuItem.java b/android/view/MenuItem.java
new file mode 100644
index 0000000..a2fb596
--- /dev/null
+++ b/android/view/MenuItem.java
@@ -0,0 +1,866 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.DrawableRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.BlendMode;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+
+/**
+ * Interface for direct access to a previously created menu item.
+ * <p>
+ * An Item is returned by calling one of the {@link android.view.Menu#add}
+ * methods.
+ * <p>
+ * For a feature set of specific menu types, see {@link Menu}.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about creating menus, read the
+ * <a href="{@docRoot}guide/topics/ui/menus.html">Menus</a> developer guide.</p>
+ * </div>
+ */
+public interface MenuItem {
+ /*
+ * These should be kept in sync with attrs.xml enum constants for showAsAction
+ */
+ /** Never show this item as a button in an Action Bar. */
+ public static final int SHOW_AS_ACTION_NEVER = 0;
+ /** Show this item as a button in an Action Bar if the system decides there is room for it. */
+ public static final int SHOW_AS_ACTION_IF_ROOM = 1;
+ /**
+ * Always show this item as a button in an Action Bar.
+ * Use sparingly! If too many items are set to always show in the Action Bar it can
+ * crowd the Action Bar and degrade the user experience on devices with smaller screens.
+ * A good rule of thumb is to have no more than 2 items set to always show at a time.
+ */
+ public static final int SHOW_AS_ACTION_ALWAYS = 2;
+
+ /**
+ * When this item is in the action bar, always show it with a text label even if
+ * it also has an icon specified.
+ */
+ public static final int SHOW_AS_ACTION_WITH_TEXT = 4;
+
+ /**
+ * This item's action view collapses to a normal menu item.
+ * When expanded, the action view temporarily takes over
+ * a larger segment of its container.
+ */
+ public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8;
+
+ /**
+ * Interface definition for a callback to be invoked when a menu item is
+ * clicked.
+ *
+ * @see Activity#onContextItemSelected(MenuItem)
+ * @see Activity#onOptionsItemSelected(MenuItem)
+ */
+ public interface OnMenuItemClickListener {
+ /**
+ * Called when a menu item has been invoked. This is the first code
+ * that is executed; if it returns true, no other callbacks will be
+ * executed.
+ *
+ * @param item The menu item that was invoked.
+ *
+ * @return Return true to consume this click and prevent others from
+ * executing.
+ */
+ public boolean onMenuItemClick(MenuItem item);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a menu item
+ * marked with {@link MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} is
+ * expanded or collapsed.
+ *
+ * @see MenuItem#expandActionView()
+ * @see MenuItem#collapseActionView()
+ * @see MenuItem#setShowAsActionFlags(int)
+ */
+ public interface OnActionExpandListener {
+ /**
+ * Called when a menu item with {@link MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}
+ * is expanded.
+ * @param item Item that was expanded
+ * @return true if the item should expand, false if expansion should be suppressed.
+ */
+ public boolean onMenuItemActionExpand(MenuItem item);
+
+ /**
+ * Called when a menu item with {@link MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}
+ * is collapsed.
+ * @param item Item that was collapsed
+ * @return true if the item should collapse, false if collapsing should be suppressed.
+ */
+ public boolean onMenuItemActionCollapse(MenuItem item);
+ }
+
+ /**
+ * Return the identifier for this menu item. The identifier can not
+ * be changed after the menu is created.
+ *
+ * @return The menu item's identifier.
+ */
+ public int getItemId();
+
+ /**
+ * Return the group identifier that this menu item is part of. The group
+ * identifier can not be changed after the menu is created.
+ *
+ * @return The menu item's group identifier.
+ */
+ public int getGroupId();
+
+ /**
+ * Return the category and order within the category of this item. This
+ * item will be shown before all items (within its category) that have
+ * order greater than this value.
+ * <p>
+ * An order integer contains the item's category (the upper bits of the
+ * integer; set by or/add the category with the order within the
+ * category) and the ordering of the item within that category (the
+ * lower bits). Example categories are {@link Menu#CATEGORY_SYSTEM},
+ * {@link Menu#CATEGORY_SECONDARY}, {@link Menu#CATEGORY_ALTERNATIVE},
+ * {@link Menu#CATEGORY_CONTAINER}. See {@link Menu} for a full list.
+ *
+ * @return The order of this item.
+ */
+ public int getOrder();
+
+ /**
+ * Change the title associated with this item.
+ *
+ * @param title The new text to be displayed.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setTitle(CharSequence title);
+
+ /**
+ * Change the title associated with this item.
+ * <p>
+ * Some menu types do not sufficient space to show the full title, and
+ * instead a condensed title is preferred. See {@link Menu} for more
+ * information.
+ *
+ * @param title The resource id of the new text to be displayed.
+ * @return This Item so additional setters can be called.
+ * @see #setTitleCondensed(CharSequence)
+ */
+
+ public MenuItem setTitle(@StringRes int title);
+
+ /**
+ * Retrieve the current title of the item.
+ *
+ * @return The title.
+ */
+ public CharSequence getTitle();
+
+ /**
+ * Change the condensed title associated with this item. The condensed
+ * title is used in situations where the normal title may be too long to
+ * be displayed.
+ *
+ * @param title The new text to be displayed as the condensed title.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setTitleCondensed(CharSequence title);
+
+ /**
+ * Retrieve the current condensed title of the item. If a condensed
+ * title was never set, it will return the normal title.
+ *
+ * @return The condensed title, if it exists.
+ * Otherwise the normal title.
+ */
+ public CharSequence getTitleCondensed();
+
+ /**
+ * Change the icon associated with this item. This icon will not always be
+ * shown, so the title should be sufficient in describing this item. See
+ * {@link Menu} for the menu types that support icons.
+ *
+ * @param icon The new icon (as a Drawable) to be displayed.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setIcon(Drawable icon);
+
+ /**
+ * Change the icon associated with this item. This icon will not always be
+ * shown, so the title should be sufficient in describing this item. See
+ * {@link Menu} for the menu types that support icons.
+ * <p>
+ * This method will set the resource ID of the icon which will be used to
+ * lazily get the Drawable when this item is being shown.
+ *
+ * @param iconRes The new icon (as a resource ID) to be displayed.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setIcon(@DrawableRes int iconRes);
+
+ /**
+ * Returns the icon for this item as a Drawable (getting it from resources if it hasn't been
+ * loaded before). Note that if you call {@link #setIconTintList(ColorStateList)} or
+ * {@link #setIconTintMode(PorterDuff.Mode)} on this item, and you use a custom menu presenter
+ * in your application, you have to apply the tinting explicitly on the {@link Drawable}
+ * returned by this method.
+ *
+ * @return The icon as a Drawable.
+ */
+ public Drawable getIcon();
+
+ /**
+ * Applies a tint to this item's icon. Does not modify the
+ * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setIcon(Drawable)} or {@link #setIcon(int)} will
+ * automatically mutate the icon and apply the specified tint and
+ * tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#MenuItem_iconTint
+ * @see #getIconTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public default MenuItem setIconTintList(@Nullable ColorStateList tint) { return this; }
+
+ /**
+ * @return the tint applied to this item's icon
+ * @attr ref android.R.styleable#MenuItem_iconTint
+ * @see #setIconTintList(ColorStateList)
+ */
+ @Nullable
+ public default ColorStateList getIconTintList() { return null; }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setIconTintList(ColorStateList)} to this item's icon. The default mode is
+ * {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#MenuItem_iconTintMode
+ * @see #setIconTintList(ColorStateList)
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ * @see Drawable#setTintBlendMode(BlendMode)
+ */
+ default @NonNull MenuItem setIconTintMode(@Nullable PorterDuff.Mode tintMode) {
+ return this;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setIconTintList(ColorStateList)} to this item's icon. The default mode is
+ * {@link BlendMode#SRC_IN}.
+ *
+ * @param blendMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#MenuItem_iconTintMode
+ * @see #setIconTintList(ColorStateList)
+ */
+ default @NonNull MenuItem setIconTintBlendMode(@Nullable BlendMode blendMode) {
+ PorterDuff.Mode mode = BlendMode.blendModeToPorterDuffMode(blendMode);
+ if (mode != null) {
+ return setIconTintMode(mode);
+ } else {
+ return this;
+ }
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to this item's icon, if specified.
+ *
+ * @return the blending mode used to apply the tint to this item's icon
+ * @attr ref android.R.styleable#MenuItem_iconTintMode
+ * @see #setIconTintMode(PorterDuff.Mode)
+ * @see #setIconTintBlendMode(BlendMode)
+ *
+ */
+ @Nullable
+ public default PorterDuff.Mode getIconTintMode() { return null; }
+
+ /**
+ * Returns the blending mode used to apply the tint to this item's icon, if specified.
+ *
+ * @return the blending mode used to apply the tint to this item's icon
+ * @attr ref android.R.styleable#MenuItem_iconTintMode
+ * @see #setIconTintBlendMode(BlendMode)
+ *
+ */
+ @Nullable
+ default BlendMode getIconTintBlendMode() {
+ PorterDuff.Mode mode = getIconTintMode();
+ if (mode != null) {
+ return BlendMode.fromValue(mode.nativeInt);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Change the Intent associated with this item. By default there is no
+ * Intent associated with a menu item. If you set one, and nothing
+ * else handles the item, then the default behavior will be to call
+ * {@link android.content.Context#startActivity} with the given Intent.
+ *
+ * <p>Note that setIntent() can not be used with the versions of
+ * {@link Menu#add} that take a Runnable, because {@link Runnable#run}
+ * does not return a value so there is no way to tell if it handled the
+ * item. In this case it is assumed that the Runnable always handles
+ * the item, and the intent will never be started.
+ *
+ * @see #getIntent
+ * @param intent The Intent to associated with the item. This Intent
+ * object is <em>not</em> copied, so be careful not to
+ * modify it later.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setIntent(Intent intent);
+
+ /**
+ * Return the Intent associated with this item. This returns a
+ * reference to the Intent which you can change as desired to modify
+ * what the Item is holding.
+ *
+ * @see #setIntent
+ * @return Returns the last value supplied to {@link #setIntent}, or
+ * null.
+ */
+ public Intent getIntent();
+
+ /**
+ * Change both the numeric and alphabetic shortcut associated with this
+ * item. Note that the shortcut will be triggered when the key that
+ * generates the given character is pressed along with the corresponding
+ * modifier key. The default modifier is {@link KeyEvent#META_CTRL_ON} in
+ * case nothing is specified. Also note that case is not significant and
+ * that alphabetic shortcut characters will be handled in lower case.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param numericChar The numeric shortcut key. This is the shortcut when
+ * using a numeric (e.g., 12-key) keyboard.
+ * @param alphaChar The alphabetic shortcut key. This is the shortcut when
+ * using a keyboard with alphabetic keys.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setShortcut(char numericChar, char alphaChar);
+
+ /**
+ * Change both the numeric and alphabetic shortcut associated with this
+ * item. Note that the shortcut will be triggered when the key that
+ * generates the given character is pressed along with the corresponding
+ * modifier key. Also note that case is not significant and that alphabetic
+ * shortcut characters will be handled in lower case.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param numericChar The numeric shortcut key. This is the shortcut when
+ * using a numeric (e.g., 12-key) keyboard.
+ * @param numericModifiers The numeric modifier associated with the shortcut. It should
+ * be a combination of {@link KeyEvent#META_META_ON}, {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_SHIFT_ON},
+ * {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
+ * @param alphaChar The alphabetic shortcut key. This is the shortcut when
+ * using a keyboard with alphabetic keys.
+ * @param alphaModifiers The alphabetic modifier associated with the shortcut. It should
+ * be a combination of {@link KeyEvent#META_META_ON}, {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_SHIFT_ON},
+ * {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
+ * @return This Item so additional setters can be called.
+ */
+ default public MenuItem setShortcut(char numericChar, char alphaChar, int numericModifiers,
+ int alphaModifiers) {
+ if ((alphaModifiers & Menu.SUPPORTED_MODIFIERS_MASK) == KeyEvent.META_CTRL_ON
+ && (numericModifiers & Menu.SUPPORTED_MODIFIERS_MASK) == KeyEvent.META_CTRL_ON) {
+ return setShortcut(numericChar, alphaChar);
+ } else {
+ return this;
+ }
+ }
+
+ /**
+ * Change the numeric shortcut associated with this item.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param numericChar The numeric shortcut key. This is the shortcut when
+ * using a 12-key (numeric) keyboard.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setNumericShortcut(char numericChar);
+
+ /**
+ * Change the numeric shortcut and modifiers associated with this item.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param numericChar The numeric shortcut key. This is the shortcut when
+ * using a 12-key (numeric) keyboard.
+ * @param numericModifiers The modifier associated with the shortcut. It should
+ * be a combination of {@link KeyEvent#META_META_ON}, {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_SHIFT_ON},
+ * {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
+ * @return This Item so additional setters can be called.
+ */
+ default public MenuItem setNumericShortcut(char numericChar, int numericModifiers) {
+ if ((numericModifiers & Menu.SUPPORTED_MODIFIERS_MASK) == KeyEvent.META_CTRL_ON) {
+ return setNumericShortcut(numericChar);
+ } else {
+ return this;
+ }
+ }
+
+ /**
+ * Return the char for this menu item's numeric (12-key) shortcut.
+ *
+ * @return Numeric character to use as a shortcut.
+ */
+ public char getNumericShortcut();
+
+ /**
+ * Return the modifiers for this menu item's numeric (12-key) shortcut.
+ * The modifier is a combination of {@link KeyEvent#META_META_ON},
+ * {@link KeyEvent#META_CTRL_ON}, {@link KeyEvent#META_ALT_ON},
+ * {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_SYM_ON},
+ * {@link KeyEvent#META_FUNCTION_ON}.
+ * For example, {@link KeyEvent#META_FUNCTION_ON}|{@link KeyEvent#META_CTRL_ON}
+ *
+ * @return Modifier associated with the numeric shortcut.
+ */
+ default public int getNumericModifiers() {
+ return KeyEvent.META_CTRL_ON;
+ }
+
+ /**
+ * Change the alphabetic shortcut associated with this item. The shortcut
+ * will be triggered when the key that generates the given character is
+ * pressed along with the corresponding modifier key. The default modifier
+ * is {@link KeyEvent#META_CTRL_ON} in case nothing is specified. Case is
+ * not significant and shortcut characters will be displayed in lower case.
+ * Note that menu items with the characters '\b' or '\n' as shortcuts will
+ * get triggered by the Delete key or Carriage Return key, respectively.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param alphaChar The alphabetic shortcut key. This is the shortcut when
+ * using a keyboard with alphabetic keys.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setAlphabeticShortcut(char alphaChar);
+
+ /**
+ * Change the alphabetic shortcut associated with this item. The shortcut
+ * will be triggered when the key that generates the given character is
+ * pressed along with the modifier keys. Case is not significant and shortcut
+ * characters will be displayed in lower case. Note that menu items with
+ * the characters '\b' or '\n' as shortcuts will get triggered by the
+ * Delete key or Carriage Return key, respectively.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param alphaChar The alphabetic shortcut key. This is the shortcut when
+ * using a keyboard with alphabetic keys.
+ * @param alphaModifiers The modifier associated with the shortcut. It should
+ * be a combination of {@link KeyEvent#META_META_ON}, {@link KeyEvent#META_CTRL_ON},
+ * {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_SHIFT_ON},
+ * {@link KeyEvent#META_SYM_ON}, {@link KeyEvent#META_FUNCTION_ON}.
+ * @return This Item so additional setters can be called.
+ */
+ default public MenuItem setAlphabeticShortcut(char alphaChar, int alphaModifiers) {
+ if ((alphaModifiers & Menu.SUPPORTED_MODIFIERS_MASK) == KeyEvent.META_CTRL_ON) {
+ return setAlphabeticShortcut(alphaChar);
+ } else {
+ return this;
+ }
+ }
+
+ /**
+ * Return the char for this menu item's alphabetic shortcut.
+ *
+ * @return Alphabetic character to use as a shortcut.
+ */
+ public char getAlphabeticShortcut();
+
+ /**
+ * Return the modifier for this menu item's alphabetic shortcut.
+ * The modifier is a combination of {@link KeyEvent#META_META_ON},
+ * {@link KeyEvent#META_CTRL_ON}, {@link KeyEvent#META_ALT_ON},
+ * {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_SYM_ON},
+ * {@link KeyEvent#META_FUNCTION_ON}.
+ * For example, {@link KeyEvent#META_FUNCTION_ON}|{@link KeyEvent#META_CTRL_ON}
+ *
+ * @return Modifier associated with the keyboard shortcut.
+ */
+ default public int getAlphabeticModifiers() {
+ return KeyEvent.META_CTRL_ON;
+ }
+
+ /**
+ * Control whether this item can display a check mark. Setting this does
+ * not actually display a check mark (see {@link #setChecked} for that);
+ * rather, it ensures there is room in the item in which to display a
+ * check mark.
+ * <p>
+ * See {@link Menu} for the menu types that support check marks.
+ *
+ * @param checkable Set to true to allow a check mark, false to
+ * disallow. The default is false.
+ * @see #setChecked
+ * @see #isCheckable
+ * @see Menu#setGroupCheckable
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setCheckable(boolean checkable);
+
+ /**
+ * Return whether the item can currently display a check mark.
+ *
+ * @return If a check mark can be displayed, returns true.
+ *
+ * @see #setCheckable
+ */
+ public boolean isCheckable();
+
+ /**
+ * Control whether this item is shown with a check mark. Note that you
+ * must first have enabled checking with {@link #setCheckable} or else
+ * the check mark will not appear. If this item is a member of a group that contains
+ * mutually-exclusive items (set via {@link Menu#setGroupCheckable(int, boolean, boolean)},
+ * the other items in the group will be unchecked.
+ * <p>
+ * See {@link Menu} for the menu types that support check marks.
+ *
+ * @see #setCheckable
+ * @see #isChecked
+ * @see Menu#setGroupCheckable
+ * @param checked Set to true to display a check mark, false to hide
+ * it. The default value is false.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setChecked(boolean checked);
+
+ /**
+ * Return whether the item is currently displaying a check mark.
+ *
+ * @return If a check mark is displayed, returns true.
+ *
+ * @see #setChecked
+ */
+ public boolean isChecked();
+
+ /**
+ * Sets the visibility of the menu item. Even if a menu item is not visible,
+ * it may still be invoked via its shortcut (to completely disable an item,
+ * set it to invisible and {@link #setEnabled(boolean) disabled}).
+ *
+ * @param visible If true then the item will be visible; if false it is
+ * hidden.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setVisible(boolean visible);
+
+ /**
+ * Return the visibility of the menu item.
+ *
+ * @return If true the item is visible; else it is hidden.
+ */
+ public boolean isVisible();
+
+ /**
+ * Sets whether the menu item is enabled. Disabling a menu item will not
+ * allow it to be invoked via its shortcut. The menu item will still be
+ * visible.
+ *
+ * @param enabled If true then the item will be invokable; if false it is
+ * won't be invokable.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setEnabled(boolean enabled);
+
+ /**
+ * Return the enabled state of the menu item.
+ *
+ * @return If true the item is enabled and hence invokable; else it is not.
+ */
+ public boolean isEnabled();
+
+ /**
+ * Check whether this item has an associated sub-menu. I.e. it is a
+ * sub-menu of another menu.
+ *
+ * @return If true this item has a menu; else it is a
+ * normal item.
+ */
+ public boolean hasSubMenu();
+
+ /**
+ * Get the sub-menu to be invoked when this item is selected, if it has
+ * one. See {@link #hasSubMenu()}.
+ *
+ * @return The associated menu if there is one, else null
+ */
+ public SubMenu getSubMenu();
+
+ /**
+ * Set a custom listener for invocation of this menu item. In most
+ * situations, it is more efficient and easier to use
+ * {@link Activity#onOptionsItemSelected(MenuItem)} or
+ * {@link Activity#onContextItemSelected(MenuItem)}.
+ *
+ * @param menuItemClickListener The object to receive invokations.
+ * @return This Item so additional setters can be called.
+ * @see Activity#onOptionsItemSelected(MenuItem)
+ * @see Activity#onContextItemSelected(MenuItem)
+ */
+ public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener menuItemClickListener);
+
+ /**
+ * Gets the extra information linked to this menu item. This extra
+ * information is set by the View that added this menu item to the
+ * menu.
+ *
+ * @see OnCreateContextMenuListener
+ * @return The extra information linked to the View that added this
+ * menu item to the menu. This can be null.
+ */
+ public ContextMenuInfo getMenuInfo();
+
+ /**
+ * Sets how this item should display in the presence of an Action Bar.
+ * The parameter actionEnum is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS},
+ * {@link #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER} should
+ * be used, and you may optionally OR the value with {@link #SHOW_AS_ACTION_WITH_TEXT}.
+ * SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as an action,
+ * it should be shown with a text label.
+ *
+ * @param actionEnum How the item should display. One of
+ * {@link #SHOW_AS_ACTION_ALWAYS}, {@link #SHOW_AS_ACTION_IF_ROOM}, or
+ * {@link #SHOW_AS_ACTION_NEVER}. SHOW_AS_ACTION_NEVER is the default.
+ *
+ * @see android.app.ActionBar
+ * @see #setActionView(View)
+ */
+ public void setShowAsAction(int actionEnum);
+
+ /**
+ * Sets how this item should display in the presence of an Action Bar.
+ * The parameter actionEnum is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS},
+ * {@link #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER} should
+ * be used, and you may optionally OR the value with {@link #SHOW_AS_ACTION_WITH_TEXT}.
+ * SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as an action,
+ * it should be shown with a text label.
+ *
+ * <p>Note: This method differs from {@link #setShowAsAction(int)} only in that it
+ * returns the current MenuItem instance for call chaining.
+ *
+ * @param actionEnum How the item should display. One of
+ * {@link #SHOW_AS_ACTION_ALWAYS}, {@link #SHOW_AS_ACTION_IF_ROOM}, or
+ * {@link #SHOW_AS_ACTION_NEVER}. SHOW_AS_ACTION_NEVER is the default.
+ *
+ * @see android.app.ActionBar
+ * @see #setActionView(View)
+ * @return This MenuItem instance for call chaining.
+ */
+ public MenuItem setShowAsActionFlags(int actionEnum);
+
+ /**
+ * Set an action view for this menu item. An action view will be displayed in place
+ * of an automatically generated menu item element in the UI when this item is shown
+ * as an action within a parent.
+ * <p>
+ * <strong>Note:</strong> Setting an action view overrides the action provider
+ * set via {@link #setActionProvider(ActionProvider)}.
+ * </p>
+ *
+ * @param view View to use for presenting this item to the user.
+ * @return This Item so additional setters can be called.
+ *
+ * @see #setShowAsAction(int)
+ */
+ public MenuItem setActionView(View view);
+
+ /**
+ * Set an action view for this menu item. An action view will be displayed in place
+ * of an automatically generated menu item element in the UI when this item is shown
+ * as an action within a parent.
+ * <p>
+ * <strong>Note:</strong> Setting an action view overrides the action provider
+ * set via {@link #setActionProvider(ActionProvider)}.
+ * </p>
+ *
+ * @param resId Layout resource to use for presenting this item to the user.
+ * @return This Item so additional setters can be called.
+ *
+ * @see #setShowAsAction(int)
+ */
+ public MenuItem setActionView(@LayoutRes int resId);
+
+ /**
+ * Returns the currently set action view for this menu item.
+ *
+ * @return This item's action view
+ *
+ * @see #setActionView(View)
+ * @see #setShowAsAction(int)
+ */
+ public View getActionView();
+
+ /**
+ * Sets the {@link ActionProvider} responsible for creating an action view if
+ * the item is placed on the action bar. The provider also provides a default
+ * action invoked if the item is placed in the overflow menu.
+ * <p>
+ * <strong>Note:</strong> Setting an action provider overrides the action view
+ * set via {@link #setActionView(int)} or {@link #setActionView(View)}.
+ * </p>
+ *
+ * @param actionProvider The action provider.
+ * @return This Item so additional setters can be called.
+ *
+ * @see ActionProvider
+ */
+ public MenuItem setActionProvider(ActionProvider actionProvider);
+
+ /**
+ * Gets the {@link ActionProvider}.
+ *
+ * @return The action provider.
+ *
+ * @see ActionProvider
+ * @see #setActionProvider(ActionProvider)
+ */
+ public ActionProvider getActionProvider();
+
+ /**
+ * Expand the action view associated with this menu item.
+ * The menu item must have an action view set, as well as
+ * the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}.
+ * If a listener has been set using {@link #setOnActionExpandListener(OnActionExpandListener)}
+ * it will have its {@link OnActionExpandListener#onMenuItemActionExpand(MenuItem)}
+ * method invoked. The listener may return false from this method to prevent expanding
+ * the action view.
+ *
+ * @return true if the action view was expanded, false otherwise.
+ */
+ public boolean expandActionView();
+
+ /**
+ * Collapse the action view associated with this menu item.
+ * The menu item must have an action view set, as well as the showAsAction flag
+ * {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a listener has been set using
+ * {@link #setOnActionExpandListener(OnActionExpandListener)} it will have its
+ * {@link OnActionExpandListener#onMenuItemActionCollapse(MenuItem)} method invoked.
+ * The listener may return false from this method to prevent collapsing the action view.
+ *
+ * @return true if the action view was collapsed, false otherwise.
+ */
+ public boolean collapseActionView();
+
+ /**
+ * Returns true if this menu item's action view has been expanded.
+ *
+ * @return true if the item's action view is expanded, false otherwise.
+ *
+ * @see #expandActionView()
+ * @see #collapseActionView()
+ * @see #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
+ * @see OnActionExpandListener
+ */
+ public boolean isActionViewExpanded();
+
+ /**
+ * Set an {@link OnActionExpandListener} on this menu item to be notified when
+ * the associated action view is expanded or collapsed. The menu item must
+ * be configured to expand or collapse its action view using the flag
+ * {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}.
+ *
+ * @param listener Listener that will respond to expand/collapse events
+ * @return This menu item instance for call chaining
+ */
+ public MenuItem setOnActionExpandListener(OnActionExpandListener listener);
+
+ /**
+ * Change the content description associated with this menu item.
+ *
+ * @param contentDescription The new content description.
+ */
+ default MenuItem setContentDescription(CharSequence contentDescription) {
+ return this;
+ }
+
+ /**
+ * Retrieve the content description associated with this menu item.
+ *
+ * @return The content description.
+ */
+ default CharSequence getContentDescription() {
+ return null;
+ }
+
+ /**
+ * Change the tooltip text associated with this menu item.
+ *
+ * @param tooltipText The new tooltip text.
+ */
+ default MenuItem setTooltipText(CharSequence tooltipText) {
+ return this;
+ }
+
+ /**
+ * Retrieve the tooltip text associated with this menu item.
+ *
+ * @return The tooltip text.
+ */
+ default CharSequence getTooltipText() {
+ return null;
+ }
+
+ /**
+ * Returns true if {@link #setShowAsAction(int)} was set to {@link #SHOW_AS_ACTION_ALWAYS}.
+ * Default value is {@code false}.
+ *
+ * @hide
+ */
+ default boolean requiresActionButton() {
+ return false;
+ }
+
+ /**
+ * Returns true if {@link #setShowAsAction(int)} was set to {@link #SHOW_AS_ACTION_NEVER}.
+ * Default value is {@code true}.
+ *
+ * @hide
+ */
+ default boolean requiresOverflow() {
+ return true;
+ }
+}
diff --git a/android/view/MotionEvent.java b/android/view/MotionEvent.java
new file mode 100644
index 0000000..69ff64f
--- /dev/null
+++ b/android/view/MotionEvent.java
@@ -0,0 +1,4260 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.IInputConstants.INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Matrix;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
+import java.lang.annotation.Retention;
+import java.util.Objects;
+
+/**
+ * Object used to report movement (mouse, pen, finger, trackball) events.
+ * Motion events may hold either absolute or relative movements and other data,
+ * depending on the type of device.
+ *
+ * <h3>Overview</h3>
+ * <p>
+ * Motion events describe movements in terms of an action code and a set of axis values.
+ * The action code specifies the state change that occurred such as a pointer going
+ * down or up. The axis values describe the position and other movement properties.
+ * </p><p>
+ * For example, when the user first touches the screen, the system delivers a touch
+ * event to the appropriate {@link View} with the action code {@link #ACTION_DOWN}
+ * and a set of axis values that include the X and Y coordinates of the touch and
+ * information about the pressure, size and orientation of the contact area.
+ * </p><p>
+ * Some devices can report multiple movement traces at the same time. Multi-touch
+ * screens emit one movement trace for each finger. The individual fingers or
+ * other objects that generate movement traces are referred to as <em>pointers</em>.
+ * Motion events contain information about all of the pointers that are currently active
+ * even if some of them have not moved since the last event was delivered.
+ * </p><p>
+ * The number of pointers only ever changes by one as individual pointers go up and down,
+ * except when the gesture is canceled.
+ * </p><p>
+ * Each pointer has a unique id that is assigned when it first goes down
+ * (indicated by {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}). A pointer id
+ * remains valid until the pointer eventually goes up (indicated by {@link #ACTION_UP}
+ * or {@link #ACTION_POINTER_UP}) or when the gesture is canceled (indicated by
+ * {@link #ACTION_CANCEL}).
+ * </p><p>
+ * The MotionEvent class provides many methods to query the position and other properties of
+ * pointers, such as {@link #getX(int)}, {@link #getY(int)}, {@link #getAxisValue},
+ * {@link #getPointerId(int)}, {@link #getToolType(int)}, and many others. Most of these
+ * methods accept the pointer index as a parameter rather than the pointer id.
+ * The pointer index of each pointer in the event ranges from 0 to one less than the value
+ * returned by {@link #getPointerCount()}.
+ * </p><p>
+ * The order in which individual pointers appear within a motion event is undefined.
+ * Thus the pointer index of a pointer can change from one event to the next but
+ * the pointer id of a pointer is guaranteed to remain constant as long as the pointer
+ * remains active. Use the {@link #getPointerId(int)} method to obtain the
+ * pointer id of a pointer to track it across all subsequent motion events in a gesture.
+ * Then for successive motion events, use the {@link #findPointerIndex(int)} method
+ * to obtain the pointer index for a given pointer id in that motion event.
+ * </p><p>
+ * Mouse and stylus buttons can be retrieved using {@link #getButtonState()}. It is a
+ * good idea to check the button state while handling {@link #ACTION_DOWN} as part
+ * of a touch event. The application may choose to perform some different action
+ * if the touch event starts due to a secondary button click, such as presenting a
+ * context menu.
+ * </p>
+ *
+ * <h3>Batching</h3>
+ * <p>
+ * For efficiency, motion events with {@link #ACTION_MOVE} may batch together
+ * multiple movement samples within a single object. The most current
+ * pointer coordinates are available using {@link #getX(int)} and {@link #getY(int)}.
+ * Earlier coordinates within the batch are accessed using {@link #getHistoricalX(int, int)}
+ * and {@link #getHistoricalY(int, int)}. The coordinates are "historical" only
+ * insofar as they are older than the current coordinates in the batch; however,
+ * they are still distinct from any other coordinates reported in prior motion events.
+ * To process all coordinates in the batch in time order, first consume the historical
+ * coordinates then consume the current coordinates.
+ * </p><p>
+ * Example: Consuming all samples for all pointers in a motion event in time order.
+ * </p><p><pre><code>
+ * void printSamples(MotionEvent ev) {
+ * final int historySize = ev.getHistorySize();
+ * final int pointerCount = ev.getPointerCount();
+ * for (int h = 0; h < historySize; h++) {
+ * System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
+ * for (int p = 0; p < pointerCount; p++) {
+ * System.out.printf(" pointer %d: (%f,%f)",
+ * ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
+ * }
+ * }
+ * System.out.printf("At time %d:", ev.getEventTime());
+ * for (int p = 0; p < pointerCount; p++) {
+ * System.out.printf(" pointer %d: (%f,%f)",
+ * ev.getPointerId(p), ev.getX(p), ev.getY(p));
+ * }
+ * }
+ * </code></pre></p>
+ *
+ * <h3>Device Types</h3>
+ * <p>
+ * The interpretation of the contents of a MotionEvent varies significantly depending
+ * on the source class of the device.
+ * </p><p>
+ * On pointing devices with source class {@link InputDevice#SOURCE_CLASS_POINTER}
+ * such as touch screens, the pointer coordinates specify absolute
+ * positions such as view X/Y coordinates. Each complete gesture is represented
+ * by a sequence of motion events with actions that describe pointer state transitions
+ * and movements. A gesture starts with a motion event with {@link #ACTION_DOWN}
+ * that provides the location of the first pointer down. As each additional
+ * pointer that goes down or up, the framework will generate a motion event with
+ * {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} accordingly.
+ * Pointer movements are described by motion events with {@link #ACTION_MOVE}.
+ * Finally, a gesture end either when the final pointer goes up as represented
+ * by a motion event with {@link #ACTION_UP} or when gesture is canceled
+ * with {@link #ACTION_CANCEL}.
+ * </p><p>
+ * Some pointing devices such as mice may support vertical and/or horizontal scrolling.
+ * A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that
+ * includes the relative scroll offset in the {@link #AXIS_VSCROLL} and
+ * {@link #AXIS_HSCROLL} axes. See {@link #getAxisValue(int)} for information
+ * about retrieving these additional axes.
+ * </p><p>
+ * On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL},
+ * the pointer coordinates specify relative movements as X/Y deltas.
+ * A trackball gesture consists of a sequence of movements described by motion
+ * events with {@link #ACTION_MOVE} interspersed with occasional {@link #ACTION_DOWN}
+ * or {@link #ACTION_UP} motion events when the trackball button is pressed or released.
+ * </p><p>
+ * On joystick devices with source class {@link InputDevice#SOURCE_CLASS_JOYSTICK},
+ * the pointer coordinates specify the absolute position of the joystick axes.
+ * The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds
+ * to the center position. More information about the set of available axes and the
+ * range of motion can be obtained using {@link InputDevice#getMotionRange}.
+ * Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y},
+ * {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}.
+ * </p><p>
+ * Refer to {@link InputDevice} for more information about how different kinds of
+ * input devices and sources represent pointer coordinates.
+ * </p>
+ *
+ * <h3>Consistency Guarantees</h3>
+ * <p>
+ * Motion events are always delivered to views as a consistent stream of events.
+ * What constitutes a consistent stream varies depending on the type of device.
+ * For touch events, consistency implies that pointers go down one at a time,
+ * move around as a group and then go up one at a time or are canceled.
+ * </p><p>
+ * While the framework tries to deliver consistent streams of motion events to
+ * views, it cannot guarantee it. Some events may be dropped or modified by
+ * containing views in the application before they are delivered thereby making
+ * the stream of events inconsistent. Views should always be prepared to
+ * handle {@link #ACTION_CANCEL} and should tolerate anomalous
+ * situations such as receiving a new {@link #ACTION_DOWN} without first having
+ * received an {@link #ACTION_UP} for the prior gesture.
+ * </p>
+ */
+public final class MotionEvent extends InputEvent implements Parcelable {
+ private static final String TAG = "MotionEvent";
+ private static final long NS_PER_MS = 1000000;
+ private static final String LABEL_PREFIX = "AXIS_";
+
+ private static final boolean DEBUG_CONCISE_TOSTRING = false;
+
+ /**
+ * An invalid pointer id.
+ *
+ * This value (-1) can be used as a placeholder to indicate that a pointer id
+ * has not been assigned or is not available. It cannot appear as
+ * a pointer id inside a {@link MotionEvent}.
+ */
+ public static final int INVALID_POINTER_ID = -1;
+
+ /**
+ * Bit mask of the parts of the action code that are the action itself.
+ */
+ public static final int ACTION_MASK = 0xff;
+
+ /**
+ * Constant for {@link #getActionMasked}: A pressed gesture has started, the
+ * motion contains the initial starting location.
+ * <p>
+ * This is also a good time to check the button state to distinguish
+ * secondary and tertiary button clicks and handle them appropriately.
+ * Use {@link #getButtonState} to retrieve the button state.
+ * </p>
+ */
+ public static final int ACTION_DOWN = 0;
+
+ /**
+ * Constant for {@link #getActionMasked}: A pressed gesture has finished, the
+ * motion contains the final release location as well as any intermediate
+ * points since the last down or move event.
+ */
+ public static final int ACTION_UP = 1;
+
+ /**
+ * Constant for {@link #getActionMasked}: A change has happened during a
+ * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
+ * The motion contains the most recent point, as well as any intermediate
+ * points since the last down or move event.
+ */
+ public static final int ACTION_MOVE = 2;
+
+ /**
+ * Constant for {@link #getActionMasked}: The current gesture has been aborted.
+ * You will not receive any more points in it. You should treat this as
+ * an up event, but not perform any action that you normally would.
+ */
+ public static final int ACTION_CANCEL = 3;
+
+ /**
+ * Constant for {@link #getActionMasked}: A movement has happened outside of the
+ * normal bounds of the UI element. This does not provide a full gesture,
+ * but only the initial location of the movement/touch.
+ * <p>
+ * Note: Because the location of any event will be outside the
+ * bounds of the view hierarchy, it will not get dispatched to
+ * any children of a ViewGroup by default. Therefore,
+ * movements with ACTION_OUTSIDE should be handled in either the
+ * root {@link View} or in the appropriate {@link Window.Callback}
+ * (e.g. {@link android.app.Activity} or {@link android.app.Dialog}).
+ * </p>
+ */
+ public static final int ACTION_OUTSIDE = 4;
+
+ /**
+ * Constant for {@link #getActionMasked}: A non-primary pointer has gone down.
+ * <p>
+ * Use {@link #getActionIndex} to retrieve the index of the pointer that changed.
+ * </p><p>
+ * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the
+ * unmasked action returned by {@link #getAction}.
+ * </p>
+ */
+ public static final int ACTION_POINTER_DOWN = 5;
+
+ /**
+ * Constant for {@link #getActionMasked}: A non-primary pointer has gone up.
+ * <p>
+ * Use {@link #getActionIndex} to retrieve the index of the pointer that changed.
+ * </p><p>
+ * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the
+ * unmasked action returned by {@link #getAction}.
+ * </p>
+ */
+ public static final int ACTION_POINTER_UP = 6;
+
+ /**
+ * Constant for {@link #getActionMasked}: A change happened but the pointer
+ * is not down (unlike {@link #ACTION_MOVE}). The motion contains the most
+ * recent point, as well as any intermediate points since the last
+ * hover move event.
+ * <p>
+ * This action is always delivered to the window or view under the pointer.
+ * </p><p>
+ * This action is not a touch event so it is delivered to
+ * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+ * {@link View#onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+ public static final int ACTION_HOVER_MOVE = 7;
+
+ /**
+ * Constant for {@link #getActionMasked}: The motion event contains relative
+ * vertical and/or horizontal scroll offsets. Use {@link #getAxisValue(int)}
+ * to retrieve the information from {@link #AXIS_VSCROLL} and {@link #AXIS_HSCROLL}.
+ * The pointer may or may not be down when this event is dispatched.
+ * <p>
+ * This action is always delivered to the window or view under the pointer, which
+ * may not be the window or view currently touched.
+ * </p><p>
+ * This action is not a touch event so it is delivered to
+ * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+ * {@link View#onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+ public static final int ACTION_SCROLL = 8;
+
+ /**
+ * Constant for {@link #getActionMasked}: The pointer is not down but has entered the
+ * boundaries of a window or view.
+ * <p>
+ * This action is always delivered to the window or view under the pointer.
+ * </p><p>
+ * This action is not a touch event so it is delivered to
+ * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+ * {@link View#onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+ public static final int ACTION_HOVER_ENTER = 9;
+
+ /**
+ * Constant for {@link #getActionMasked}: The pointer is not down but has exited the
+ * boundaries of a window or view.
+ * <p>
+ * This action is always delivered to the window or view that was previously under the pointer.
+ * </p><p>
+ * This action is not a touch event so it is delivered to
+ * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+ * {@link View#onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+ public static final int ACTION_HOVER_EXIT = 10;
+
+ /**
+ * Constant for {@link #getActionMasked}: A button has been pressed.
+ *
+ * <p>
+ * Use {@link #getActionButton()} to get which button was pressed.
+ * </p><p>
+ * This action is not a touch event so it is delivered to
+ * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+ * {@link View#onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+ public static final int ACTION_BUTTON_PRESS = 11;
+
+ /**
+ * Constant for {@link #getActionMasked}: A button has been released.
+ *
+ * <p>
+ * Use {@link #getActionButton()} to get which button was released.
+ * </p><p>
+ * This action is not a touch event so it is delivered to
+ * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+ * {@link View#onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+ public static final int ACTION_BUTTON_RELEASE = 12;
+
+ /**
+ * Bits in the action code that represent a pointer index, used with
+ * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Shifting
+ * down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer
+ * index where the data for the pointer going up or down can be found; you can
+ * get its identifier with {@link #getPointerId(int)} and the actual
+ * data with {@link #getX(int)} etc.
+ *
+ * @see #getActionIndex
+ */
+ public static final int ACTION_POINTER_INDEX_MASK = 0xff00;
+
+ /**
+ * Bit shift for the action bits holding the pointer index as
+ * defined by {@link #ACTION_POINTER_INDEX_MASK}.
+ *
+ * @see #getActionIndex
+ */
+ public static final int ACTION_POINTER_INDEX_SHIFT = 8;
+
+ /**
+ * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the
+ * data index associated with {@link #ACTION_POINTER_DOWN}.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_1_DOWN = ACTION_POINTER_DOWN | 0x0000;
+
+ /**
+ * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the
+ * data index associated with {@link #ACTION_POINTER_DOWN}.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_2_DOWN = ACTION_POINTER_DOWN | 0x0100;
+
+ /**
+ * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the
+ * data index associated with {@link #ACTION_POINTER_DOWN}.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_3_DOWN = ACTION_POINTER_DOWN | 0x0200;
+
+ /**
+ * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the
+ * data index associated with {@link #ACTION_POINTER_UP}.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_1_UP = ACTION_POINTER_UP | 0x0000;
+
+ /**
+ * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the
+ * data index associated with {@link #ACTION_POINTER_UP}.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_2_UP = ACTION_POINTER_UP | 0x0100;
+
+ /**
+ * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the
+ * data index associated with {@link #ACTION_POINTER_UP}.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_3_UP = ACTION_POINTER_UP | 0x0200;
+
+ /**
+ * @deprecated Renamed to {@link #ACTION_POINTER_INDEX_MASK} to match
+ * the actual data contained in these bits.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_ID_MASK = 0xff00;
+
+ /**
+ * @deprecated Renamed to {@link #ACTION_POINTER_INDEX_SHIFT} to match
+ * the actual data contained in these bits.
+ */
+ @Deprecated
+ public static final int ACTION_POINTER_ID_SHIFT = 8;
+
+ /**
+ * This flag indicates that the window that received this motion event is partly
+ * or wholly obscured by another visible window above it. This flag is set to true
+ * if the event directly passed through the obscured area.
+ *
+ * A security sensitive application can check this flag to identify situations in which
+ * a malicious application may have covered up part of its content for the purpose
+ * of misleading the user or hijacking touches. An appropriate response might be
+ * to drop the suspect touches or to take additional precautions to confirm the user's
+ * actual intent.
+ */
+ public static final int FLAG_WINDOW_IS_OBSCURED = 0x1;
+
+ /**
+ * This flag indicates that the window that received this motion event is partly
+ * or wholly obscured by another visible window above it. This flag is set to true
+ * even if the event did not directly pass through the obscured area.
+ *
+ * A security sensitive application can check this flag to identify situations in which
+ * a malicious application may have covered up part of its content for the purpose
+ * of misleading the user or hijacking touches. An appropriate response might be
+ * to drop the suspect touches or to take additional precautions to confirm the user's
+ * actual intent.
+ *
+ * Unlike FLAG_WINDOW_IS_OBSCURED, this is true even if the window that received this event is
+ * obstructed in areas other than the touched location.
+ */
+ public static final int FLAG_WINDOW_IS_PARTIALLY_OBSCURED = 0x2;
+
+ /**
+ * This private flag is only set on {@link #ACTION_HOVER_MOVE} events and indicates that
+ * this event will be immediately followed by a {@link #ACTION_HOVER_EXIT}. It is used to
+ * prevent generating redundant {@link #ACTION_HOVER_ENTER} events.
+ * @hide
+ */
+ public static final int FLAG_HOVER_EXIT_PENDING = 0x4;
+
+ /**
+ * This flag indicates that the event has been generated by a gesture generator. It
+ * provides a hint to the GestureDetector to not apply any touch slop.
+ *
+ * @hide
+ */
+ public static final int FLAG_IS_GENERATED_GESTURE = 0x8;
+
+ /**
+ * This flag associated with {@link #ACTION_POINTER_UP}, this indicates that the pointer
+ * has been canceled. Typically this is used for palm event when the user has accidental
+ * touches.
+ * @hide
+ */
+ public static final int FLAG_CANCELED = 0x20;
+
+ /**
+ * This flag indicates that the event will not cause a focus change if it is directed to an
+ * unfocused window, even if it an {@link #ACTION_DOWN}. This is typically used with pointer
+ * gestures to allow the user to direct gestures to an unfocused window without bringing the
+ * window into focus.
+ * @hide
+ */
+ public static final int FLAG_NO_FOCUS_CHANGE = 0x40;
+
+ /**
+ * This flag indicates that this event was modified by or generated from an accessibility
+ * service. Value = 0x800
+ * @hide
+ */
+ @TestApi
+ public static final int FLAG_IS_ACCESSIBILITY_EVENT = INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
+
+ /**
+ * Private flag that indicates when the system has detected that this motion event
+ * may be inconsistent with respect to the sequence of previously delivered motion events,
+ * such as when a pointer move event is sent but the pointer is not down.
+ *
+ * @hide
+ * @see #isTainted
+ * @see #setTainted
+ */
+ public static final int FLAG_TAINTED = 0x80000000;
+
+ /**
+ * Private flag indicating that this event was synthesized by the system and should be delivered
+ * to the accessibility focused view first. When being dispatched such an event is not handled
+ * by predecessors of the accessibility focused view and after the event reaches that view the
+ * flag is cleared and normal event dispatch is performed. This ensures that the platform can
+ * click on any view that has accessibility focus which is semantically equivalent to asking the
+ * view to perform a click accessibility action but more generic as views not implementing click
+ * action correctly can still be activated.
+ *
+ * @hide
+ * @see #isTargetAccessibilityFocus()
+ * @see #setTargetAccessibilityFocus(boolean)
+ */
+ public static final int FLAG_TARGET_ACCESSIBILITY_FOCUS = 0x40000000;
+
+ /**
+ * Flag indicating the motion event intersected the top edge of the screen.
+ */
+ public static final int EDGE_TOP = 0x00000001;
+
+ /**
+ * Flag indicating the motion event intersected the bottom edge of the screen.
+ */
+ public static final int EDGE_BOTTOM = 0x00000002;
+
+ /**
+ * Flag indicating the motion event intersected the left edge of the screen.
+ */
+ public static final int EDGE_LEFT = 0x00000004;
+
+ /**
+ * Flag indicating the motion event intersected the right edge of the screen.
+ */
+ public static final int EDGE_RIGHT = 0x00000008;
+
+ /**
+ * Axis constant: X axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen, reports the absolute X screen position of the center of
+ * the touch contact area. The units are display pixels.
+ * <li>For a touch pad, reports the absolute X surface position of the center of the touch
+ * contact area. The units are device-dependent; use {@link InputDevice#getMotionRange(int)}
+ * to query the effective range of values.
+ * <li>For a mouse, reports the absolute X screen position of the mouse pointer.
+ * The units are display pixels.
+ * <li>For a trackball, reports the relative horizontal displacement of the trackball.
+ * The value is normalized to a range from -1.0 (left) to 1.0 (right).
+ * <li>For a joystick, reports the absolute X position of the joystick.
+ * The value is normalized to a range from -1.0 (left) to 1.0 (right).
+ * </ul>
+ * </p>
+ *
+ * @see #getX(int)
+ * @see #getHistoricalX(int, int)
+ * @see MotionEvent.PointerCoords#x
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_X = 0;
+
+ /**
+ * Axis constant: Y axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen, reports the absolute Y screen position of the center of
+ * the touch contact area. The units are display pixels.
+ * <li>For a touch pad, reports the absolute Y surface position of the center of the touch
+ * contact area. The units are device-dependent; use {@link InputDevice#getMotionRange(int)}
+ * to query the effective range of values.
+ * <li>For a mouse, reports the absolute Y screen position of the mouse pointer.
+ * The units are display pixels.
+ * <li>For a trackball, reports the relative vertical displacement of the trackball.
+ * The value is normalized to a range from -1.0 (up) to 1.0 (down).
+ * <li>For a joystick, reports the absolute Y position of the joystick.
+ * The value is normalized to a range from -1.0 (up or far) to 1.0 (down or near).
+ * </ul>
+ * </p>
+ *
+ * @see #getY(int)
+ * @see #getHistoricalY(int, int)
+ * @see MotionEvent.PointerCoords#y
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_Y = 1;
+
+ /**
+ * Axis constant: Pressure axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen or touch pad, reports the approximate pressure applied to the surface
+ * by a finger or other tool. The value is normalized to a range from
+ * 0 (no pressure at all) to 1 (normal pressure), although values higher than 1
+ * may be generated depending on the calibration of the input device.
+ * <li>For a trackball, the value is set to 1 if the trackball button is pressed
+ * or 0 otherwise.
+ * <li>For a mouse, the value is set to 1 if the primary mouse button is pressed
+ * or 0 otherwise.
+ * </ul>
+ * </p>
+ *
+ * @see #getPressure(int)
+ * @see #getHistoricalPressure(int, int)
+ * @see MotionEvent.PointerCoords#pressure
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_PRESSURE = 2;
+
+ /**
+ * Axis constant: Size axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen or touch pad, reports the approximate size of the contact area in
+ * relation to the maximum detectable size for the device. The value is normalized
+ * to a range from 0 (smallest detectable size) to 1 (largest detectable size),
+ * although it is not a linear scale. This value is of limited use.
+ * To obtain calibrated size information, use
+ * {@link #AXIS_TOUCH_MAJOR} or {@link #AXIS_TOOL_MAJOR}.
+ * </ul>
+ * </p>
+ *
+ * @see #getSize(int)
+ * @see #getHistoricalSize(int, int)
+ * @see MotionEvent.PointerCoords#size
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_SIZE = 3;
+
+ /**
+ * Axis constant: TouchMajor axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen, reports the length of the major axis of an ellipse that
+ * represents the touch area at the point of contact.
+ * The units are display pixels.
+ * <li>For a touch pad, reports the length of the major axis of an ellipse that
+ * represents the touch area at the point of contact.
+ * The units are device-dependent; use {@link InputDevice#getMotionRange(int)}
+ * to query the effective range of values.
+ * </ul>
+ * </p>
+ *
+ * @see #getTouchMajor(int)
+ * @see #getHistoricalTouchMajor(int, int)
+ * @see MotionEvent.PointerCoords#touchMajor
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_TOUCH_MAJOR = 4;
+
+ /**
+ * Axis constant: TouchMinor axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen, reports the length of the minor axis of an ellipse that
+ * represents the touch area at the point of contact.
+ * The units are display pixels.
+ * <li>For a touch pad, reports the length of the minor axis of an ellipse that
+ * represents the touch area at the point of contact.
+ * The units are device-dependent; use {@link InputDevice#getMotionRange(int)}
+ * to query the effective range of values.
+ * </ul>
+ * </p><p>
+ * When the touch is circular, the major and minor axis lengths will be equal to one another.
+ * </p>
+ *
+ * @see #getTouchMinor(int)
+ * @see #getHistoricalTouchMinor(int, int)
+ * @see MotionEvent.PointerCoords#touchMinor
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_TOUCH_MINOR = 5;
+
+ /**
+ * Axis constant: ToolMajor axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen, reports the length of the major axis of an ellipse that
+ * represents the size of the approaching finger or tool used to make contact.
+ * <li>For a touch pad, reports the length of the major axis of an ellipse that
+ * represents the size of the approaching finger or tool used to make contact.
+ * The units are device-dependent; use {@link InputDevice#getMotionRange(int)}
+ * to query the effective range of values.
+ * </ul>
+ * </p><p>
+ * When the touch is circular, the major and minor axis lengths will be equal to one another.
+ * </p><p>
+ * The tool size may be larger than the touch size since the tool may not be fully
+ * in contact with the touch sensor.
+ * </p>
+ *
+ * @see #getToolMajor(int)
+ * @see #getHistoricalToolMajor(int, int)
+ * @see MotionEvent.PointerCoords#toolMajor
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_TOOL_MAJOR = 6;
+
+ /**
+ * Axis constant: ToolMinor axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen, reports the length of the minor axis of an ellipse that
+ * represents the size of the approaching finger or tool used to make contact.
+ * <li>For a touch pad, reports the length of the minor axis of an ellipse that
+ * represents the size of the approaching finger or tool used to make contact.
+ * The units are device-dependent; use {@link InputDevice#getMotionRange(int)}
+ * to query the effective range of values.
+ * </ul>
+ * </p><p>
+ * When the touch is circular, the major and minor axis lengths will be equal to one another.
+ * </p><p>
+ * The tool size may be larger than the touch size since the tool may not be fully
+ * in contact with the touch sensor.
+ * </p>
+ *
+ * @see #getToolMinor(int)
+ * @see #getHistoricalToolMinor(int, int)
+ * @see MotionEvent.PointerCoords#toolMinor
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_TOOL_MINOR = 7;
+
+ /**
+ * Axis constant: Orientation axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a touch screen or touch pad, reports the orientation of the finger
+ * or tool in radians relative to the vertical plane of the device.
+ * An angle of 0 radians indicates that the major axis of contact is oriented
+ * upwards, is perfectly circular or is of unknown orientation. A positive angle
+ * indicates that the major axis of contact is oriented to the right. A negative angle
+ * indicates that the major axis of contact is oriented to the left.
+ * The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians
+ * (finger pointing fully right).
+ * <li>For a stylus, the orientation indicates the direction in which the stylus
+ * is pointing in relation to the vertical axis of the current orientation of the screen.
+ * The range is from -PI radians to PI radians, where 0 is pointing up,
+ * -PI/2 radians is pointing left, -PI or PI radians is pointing down, and PI/2 radians
+ * is pointing right. See also {@link #AXIS_TILT}.
+ * </ul>
+ * </p>
+ *
+ * @see #getOrientation(int)
+ * @see #getHistoricalOrientation(int, int)
+ * @see MotionEvent.PointerCoords#orientation
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_ORIENTATION = 8;
+
+ /**
+ * Axis constant: Vertical Scroll axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a mouse, reports the relative movement of the vertical scroll wheel.
+ * The value is normalized to a range from -1.0 (down) to 1.0 (up).
+ * </ul>
+ * </p><p>
+ * This axis should be used to scroll views vertically.
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_VSCROLL = 9;
+
+ /**
+ * Axis constant: Horizontal Scroll axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a mouse, reports the relative movement of the horizontal scroll wheel.
+ * The value is normalized to a range from -1.0 (left) to 1.0 (right).
+ * </ul>
+ * </p><p>
+ * This axis should be used to scroll views horizontally.
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_HSCROLL = 10;
+
+ /**
+ * Axis constant: Z axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute Z position of the joystick.
+ * The value is normalized to a range from -1.0 (high) to 1.0 (low).
+ * <em>On game pads with two analog joysticks, this axis is often reinterpreted
+ * to report the absolute X position of the second joystick instead.</em>
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_Z = 11;
+
+ /**
+ * Axis constant: X Rotation axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute rotation angle about the X axis.
+ * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RX = 12;
+
+ /**
+ * Axis constant: Y Rotation axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute rotation angle about the Y axis.
+ * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RY = 13;
+
+ /**
+ * Axis constant: Z Rotation axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute rotation angle about the Z axis.
+ * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
+ * <em>On game pads with two analog joysticks, this axis is often reinterpreted
+ * to report the absolute Y position of the second joystick instead.</em>
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RZ = 14;
+
+ /**
+ * Axis constant: Hat X axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute X position of the directional hat control.
+ * The value is normalized to a range from -1.0 (left) to 1.0 (right).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_HAT_X = 15;
+
+ /**
+ * Axis constant: Hat Y axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute Y position of the directional hat control.
+ * The value is normalized to a range from -1.0 (up) to 1.0 (down).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_HAT_Y = 16;
+
+ /**
+ * Axis constant: Left Trigger axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the left trigger control.
+ * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_LTRIGGER = 17;
+
+ /**
+ * Axis constant: Right Trigger axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the right trigger control.
+ * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RTRIGGER = 18;
+
+ /**
+ * Axis constant: Throttle axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the throttle control.
+ * The value is normalized to a range from 0.0 (fully open) to 1.0 (fully closed).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_THROTTLE = 19;
+
+ /**
+ * Axis constant: Rudder axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the rudder control.
+ * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RUDDER = 20;
+
+ /**
+ * Axis constant: Wheel axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the steering wheel control.
+ * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_WHEEL = 21;
+
+ /**
+ * Axis constant: Gas axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the gas (accelerator) control.
+ * The value is normalized to a range from 0.0 (no acceleration)
+ * to 1.0 (maximum acceleration).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GAS = 22;
+
+ /**
+ * Axis constant: Brake axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a joystick, reports the absolute position of the brake control.
+ * The value is normalized to a range from 0.0 (no braking) to 1.0 (maximum braking).
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_BRAKE = 23;
+
+ /**
+ * Axis constant: Distance axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a stylus, reports the distance of the stylus from the screen.
+ * A value of 0.0 indicates direct contact and larger values indicate increasing
+ * distance from the surface.
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_DISTANCE = 24;
+
+ /**
+ * Axis constant: Tilt axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a stylus, reports the tilt angle of the stylus in radians where
+ * 0 radians indicates that the stylus is being held perpendicular to the
+ * surface, and PI/2 radians indicates that the stylus is being held flat
+ * against the surface.
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int, int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_TILT = 25;
+
+ /**
+ * Axis constant: Generic scroll axis of a motion event.
+ * <p>
+ * <ul>
+ * <li>Reports the relative movement of the generic scrolling device.
+ * </ul>
+ * </p><p>
+ * This axis should be used for scroll events that are neither strictly vertical nor horizontal.
+ * A good example would be the rotation of a rotary encoder input device.
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ */
+ public static final int AXIS_SCROLL = 26;
+
+ /**
+ * Axis constant: The movement of x position of a motion event.
+ * <p>
+ * <ul>
+ * <li>For a mouse, reports a difference of x position between the previous position.
+ * This is useful when pointer is captured, in that case the mouse pointer doesn't change
+ * the location but this axis reports the difference which allows the app to see
+ * how the mouse is moved.
+ * </ul>
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int, int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RELATIVE_X = 27;
+
+ /**
+ * Axis constant: The movement of y position of a motion event.
+ * <p>
+ * This is similar to {@link #AXIS_RELATIVE_X} but for y-axis.
+ * </p>
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int, int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_RELATIVE_Y = 28;
+
+ /**
+ * Axis constant: Generic 1 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_1 = 32;
+
+ /**
+ * Axis constant: Generic 2 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_2 = 33;
+
+ /**
+ * Axis constant: Generic 3 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_3 = 34;
+
+ /**
+ * Axis constant: Generic 4 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_4 = 35;
+
+ /**
+ * Axis constant: Generic 5 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_5 = 36;
+
+ /**
+ * Axis constant: Generic 6 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_6 = 37;
+
+ /**
+ * Axis constant: Generic 7 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_7 = 38;
+
+ /**
+ * Axis constant: Generic 8 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_8 = 39;
+
+ /**
+ * Axis constant: Generic 9 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_9 = 40;
+
+ /**
+ * Axis constant: Generic 10 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_10 = 41;
+
+ /**
+ * Axis constant: Generic 11 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_11 = 42;
+
+ /**
+ * Axis constant: Generic 12 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_12 = 43;
+
+ /**
+ * Axis constant: Generic 13 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_13 = 44;
+
+ /**
+ * Axis constant: Generic 14 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_14 = 45;
+
+ /**
+ * Axis constant: Generic 15 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_15 = 46;
+
+ /**
+ * Axis constant: Generic 16 axis of a motion event.
+ * The interpretation of a generic axis is device-specific.
+ *
+ * @see #getAxisValue(int, int)
+ * @see #getHistoricalAxisValue(int, int, int)
+ * @see MotionEvent.PointerCoords#getAxisValue(int)
+ * @see InputDevice#getMotionRange
+ */
+ public static final int AXIS_GENERIC_16 = 47;
+
+ // NOTE: If you add a new axis here you must also add it to:
+ // native/include/android/input.h
+ // frameworks/base/include/ui/KeycodeLabels.h
+
+ // Symbolic names of all axes.
+ private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>();
+ static {
+ SparseArray<String> names = AXIS_SYMBOLIC_NAMES;
+ names.append(AXIS_X, "AXIS_X");
+ names.append(AXIS_Y, "AXIS_Y");
+ names.append(AXIS_PRESSURE, "AXIS_PRESSURE");
+ names.append(AXIS_SIZE, "AXIS_SIZE");
+ names.append(AXIS_TOUCH_MAJOR, "AXIS_TOUCH_MAJOR");
+ names.append(AXIS_TOUCH_MINOR, "AXIS_TOUCH_MINOR");
+ names.append(AXIS_TOOL_MAJOR, "AXIS_TOOL_MAJOR");
+ names.append(AXIS_TOOL_MINOR, "AXIS_TOOL_MINOR");
+ names.append(AXIS_ORIENTATION, "AXIS_ORIENTATION");
+ names.append(AXIS_VSCROLL, "AXIS_VSCROLL");
+ names.append(AXIS_HSCROLL, "AXIS_HSCROLL");
+ names.append(AXIS_Z, "AXIS_Z");
+ names.append(AXIS_RX, "AXIS_RX");
+ names.append(AXIS_RY, "AXIS_RY");
+ names.append(AXIS_RZ, "AXIS_RZ");
+ names.append(AXIS_HAT_X, "AXIS_HAT_X");
+ names.append(AXIS_HAT_Y, "AXIS_HAT_Y");
+ names.append(AXIS_LTRIGGER, "AXIS_LTRIGGER");
+ names.append(AXIS_RTRIGGER, "AXIS_RTRIGGER");
+ names.append(AXIS_THROTTLE, "AXIS_THROTTLE");
+ names.append(AXIS_RUDDER, "AXIS_RUDDER");
+ names.append(AXIS_WHEEL, "AXIS_WHEEL");
+ names.append(AXIS_GAS, "AXIS_GAS");
+ names.append(AXIS_BRAKE, "AXIS_BRAKE");
+ names.append(AXIS_DISTANCE, "AXIS_DISTANCE");
+ names.append(AXIS_TILT, "AXIS_TILT");
+ names.append(AXIS_SCROLL, "AXIS_SCROLL");
+ names.append(AXIS_RELATIVE_X, "AXIS_REALTIVE_X");
+ names.append(AXIS_RELATIVE_Y, "AXIS_REALTIVE_Y");
+ names.append(AXIS_GENERIC_1, "AXIS_GENERIC_1");
+ names.append(AXIS_GENERIC_2, "AXIS_GENERIC_2");
+ names.append(AXIS_GENERIC_3, "AXIS_GENERIC_3");
+ names.append(AXIS_GENERIC_4, "AXIS_GENERIC_4");
+ names.append(AXIS_GENERIC_5, "AXIS_GENERIC_5");
+ names.append(AXIS_GENERIC_6, "AXIS_GENERIC_6");
+ names.append(AXIS_GENERIC_7, "AXIS_GENERIC_7");
+ names.append(AXIS_GENERIC_8, "AXIS_GENERIC_8");
+ names.append(AXIS_GENERIC_9, "AXIS_GENERIC_9");
+ names.append(AXIS_GENERIC_10, "AXIS_GENERIC_10");
+ names.append(AXIS_GENERIC_11, "AXIS_GENERIC_11");
+ names.append(AXIS_GENERIC_12, "AXIS_GENERIC_12");
+ names.append(AXIS_GENERIC_13, "AXIS_GENERIC_13");
+ names.append(AXIS_GENERIC_14, "AXIS_GENERIC_14");
+ names.append(AXIS_GENERIC_15, "AXIS_GENERIC_15");
+ names.append(AXIS_GENERIC_16, "AXIS_GENERIC_16");
+ }
+
+ /**
+ * Button constant: Primary button (left mouse button).
+ *
+ * This button constant is not set in response to simple touches with a finger
+ * or stylus tip. The user must actually push a button.
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_PRIMARY = 1 << 0;
+
+ /**
+ * Button constant: Secondary button (right mouse button).
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_SECONDARY = 1 << 1;
+
+ /**
+ * Button constant: Tertiary button (middle mouse button).
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_TERTIARY = 1 << 2;
+
+ /**
+ * Button constant: Back button pressed (mouse back button).
+ * <p>
+ * The system may send a {@link KeyEvent#KEYCODE_BACK} key press to the application
+ * when this button is pressed.
+ * </p>
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_BACK = 1 << 3;
+
+ /**
+ * Button constant: Forward button pressed (mouse forward button).
+ * <p>
+ * The system may send a {@link KeyEvent#KEYCODE_FORWARD} key press to the application
+ * when this button is pressed.
+ * </p>
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_FORWARD = 1 << 4;
+
+ /**
+ * Button constant: Primary stylus button pressed.
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_STYLUS_PRIMARY = 1 << 5;
+
+ /**
+ * Button constant: Secondary stylus button pressed.
+ *
+ * @see #getButtonState
+ */
+ public static final int BUTTON_STYLUS_SECONDARY = 1 << 6;
+
+ // NOTE: If you add a new axis here you must also add it to:
+ // native/include/android/input.h
+
+ // Symbolic names of all button states in bit order from least significant
+ // to most significant.
+ private static final String[] BUTTON_SYMBOLIC_NAMES = new String[] {
+ "BUTTON_PRIMARY",
+ "BUTTON_SECONDARY",
+ "BUTTON_TERTIARY",
+ "BUTTON_BACK",
+ "BUTTON_FORWARD",
+ "BUTTON_STYLUS_PRIMARY",
+ "BUTTON_STYLUS_SECONDARY",
+ "0x00000080",
+ "0x00000100",
+ "0x00000200",
+ "0x00000400",
+ "0x00000800",
+ "0x00001000",
+ "0x00002000",
+ "0x00004000",
+ "0x00008000",
+ "0x00010000",
+ "0x00020000",
+ "0x00040000",
+ "0x00080000",
+ "0x00100000",
+ "0x00200000",
+ "0x00400000",
+ "0x00800000",
+ "0x01000000",
+ "0x02000000",
+ "0x04000000",
+ "0x08000000",
+ "0x10000000",
+ "0x20000000",
+ "0x40000000",
+ "0x80000000",
+ };
+
+ /**
+ * Classification constant: None.
+ *
+ * No additional information is available about the current motion event stream.
+ *
+ * @see #getClassification
+ */
+ public static final int CLASSIFICATION_NONE = 0;
+
+ /**
+ * Classification constant: Ambiguous gesture.
+ *
+ * The user's intent with respect to the current event stream is not yet determined.
+ * Gestural actions, such as scrolling, should be inhibited until the classification resolves
+ * to another value or the event stream ends.
+ *
+ * @see #getClassification
+ */
+ public static final int CLASSIFICATION_AMBIGUOUS_GESTURE = 1;
+
+ /**
+ * Classification constant: Deep press.
+ *
+ * The current event stream represents the user intentionally pressing harder on the screen.
+ * This classification type should be used to accelerate the long press behaviour.
+ *
+ * @see #getClassification
+ */
+ public static final int CLASSIFICATION_DEEP_PRESS = 2;
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef(prefix = { "CLASSIFICATION" }, value = {
+ CLASSIFICATION_NONE, CLASSIFICATION_AMBIGUOUS_GESTURE, CLASSIFICATION_DEEP_PRESS})
+ public @interface Classification {};
+
+ /**
+ * Tool type constant: Unknown tool type.
+ * This constant is used when the tool type is not known or is not relevant,
+ * such as for a trackball or other non-pointing device.
+ *
+ * @see #getToolType
+ */
+ public static final int TOOL_TYPE_UNKNOWN = 0;
+
+ /**
+ * Tool type constant: The tool is a finger.
+ *
+ * @see #getToolType
+ */
+ public static final int TOOL_TYPE_FINGER = 1;
+
+ /**
+ * Tool type constant: The tool is a stylus.
+ *
+ * @see #getToolType
+ */
+ public static final int TOOL_TYPE_STYLUS = 2;
+
+ /**
+ * Tool type constant: The tool is a mouse.
+ *
+ * @see #getToolType
+ */
+ public static final int TOOL_TYPE_MOUSE = 3;
+
+ /**
+ * Tool type constant: The tool is an eraser or a stylus being used in an inverted posture.
+ *
+ * @see #getToolType
+ */
+ public static final int TOOL_TYPE_ERASER = 4;
+
+ // NOTE: If you add a new tool type here you must also add it to:
+ // native/include/android/input.h
+
+ // Symbolic names of all tool types.
+ private static final SparseArray<String> TOOL_TYPE_SYMBOLIC_NAMES = new SparseArray<String>();
+ static {
+ SparseArray<String> names = TOOL_TYPE_SYMBOLIC_NAMES;
+ names.append(TOOL_TYPE_UNKNOWN, "TOOL_TYPE_UNKNOWN");
+ names.append(TOOL_TYPE_FINGER, "TOOL_TYPE_FINGER");
+ names.append(TOOL_TYPE_STYLUS, "TOOL_TYPE_STYLUS");
+ names.append(TOOL_TYPE_MOUSE, "TOOL_TYPE_MOUSE");
+ names.append(TOOL_TYPE_ERASER, "TOOL_TYPE_ERASER");
+ }
+
+ // Private value for history pos that obtains the current sample.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final int HISTORY_CURRENT = -0x80000000;
+
+ // This is essentially the same as native AMOTION_EVENT_INVALID_CURSOR_POSITION as they're all
+ // NaN and we use isnan() everywhere to check validity.
+ private static final float INVALID_CURSOR_POSITION = Float.NaN;
+
+ private static final int MAX_RECYCLED = 10;
+ private static final Object gRecyclerLock = new Object();
+ private static int gRecyclerUsed;
+ private static MotionEvent gRecyclerTop;
+
+ // Shared temporary objects used when translating coordinates supplied by
+ // the caller into single element PointerCoords and pointer id arrays.
+ private static final Object gSharedTempLock = new Object();
+ private static PointerCoords[] gSharedTempPointerCoords;
+ private static PointerProperties[] gSharedTempPointerProperties;
+ private static int[] gSharedTempPointerIndexMap;
+
+ private static final void ensureSharedTempPointerCapacity(int desiredCapacity) {
+ if (gSharedTempPointerCoords == null
+ || gSharedTempPointerCoords.length < desiredCapacity) {
+ int capacity = gSharedTempPointerCoords != null ? gSharedTempPointerCoords.length : 8;
+ while (capacity < desiredCapacity) {
+ capacity *= 2;
+ }
+ gSharedTempPointerCoords = PointerCoords.createArray(capacity);
+ gSharedTempPointerProperties = PointerProperties.createArray(capacity);
+ gSharedTempPointerIndexMap = new int[capacity];
+ }
+ }
+
+ // Pointer to the native MotionEvent object that contains the actual data.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private long mNativePtr;
+
+ private MotionEvent mNext;
+
+ private static native long nativeInitialize(long nativePtr,
+ int deviceId, int source, int displayId, int action, int flags, int edgeFlags,
+ int metaState, int buttonState, @Classification int classification,
+ float xOffset, float yOffset, float xPrecision, float yPrecision,
+ long downTimeNanos, long eventTimeNanos,
+ int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords);
+ private static native void nativeDispose(long nativePtr);
+ private static native void nativeAddBatch(long nativePtr, long eventTimeNanos,
+ PointerCoords[] pointerCoords, int metaState);
+ private static native void nativeGetPointerCoords(long nativePtr,
+ int pointerIndex, int historyPos, PointerCoords outPointerCoords);
+ private static native void nativeGetPointerProperties(long nativePtr,
+ int pointerIndex, PointerProperties outPointerProperties);
+
+ private static native long nativeReadFromParcel(long nativePtr, Parcel parcel);
+ private static native void nativeWriteToParcel(long nativePtr, Parcel parcel);
+
+ private static native String nativeAxisToString(int axis);
+ private static native int nativeAxisFromString(String label);
+
+ // -------------- @FastNative -------------------------
+
+ @FastNative
+ private static native int nativeGetPointerId(long nativePtr, int pointerIndex);
+ @FastNative
+ private static native int nativeGetToolType(long nativePtr, int pointerIndex);
+ @FastNative
+ private static native long nativeGetEventTimeNanos(long nativePtr, int historyPos);
+ @FastNative
+ @UnsupportedAppUsage
+ private static native float nativeGetRawAxisValue(long nativePtr,
+ int axis, int pointerIndex, int historyPos);
+ @FastNative
+ private static native float nativeGetAxisValue(long nativePtr,
+ int axis, int pointerIndex, int historyPos);
+ @FastNative
+ private static native void nativeTransform(long nativePtr, Matrix matrix);
+ @FastNative
+ private static native void nativeApplyTransform(long nativePtr, Matrix matrix);
+
+ // -------------- @CriticalNative ----------------------
+
+ @CriticalNative
+ private static native long nativeCopy(long destNativePtr, long sourceNativePtr,
+ boolean keepHistory);
+ @CriticalNative
+ private static native int nativeGetId(long nativePtr);
+ @CriticalNative
+ private static native int nativeGetDeviceId(long nativePtr);
+ @CriticalNative
+ private static native int nativeGetSource(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetSource(long nativePtr, int source);
+ @CriticalNative
+ private static native int nativeGetDisplayId(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetDisplayId(long nativePtr, int displayId);
+ @CriticalNative
+ private static native int nativeGetAction(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetAction(long nativePtr, int action);
+ @CriticalNative
+ private static native boolean nativeIsTouchEvent(long nativePtr);
+ @CriticalNative
+ private static native int nativeGetFlags(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetFlags(long nativePtr, int flags);
+ @CriticalNative
+ private static native int nativeGetEdgeFlags(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetEdgeFlags(long nativePtr, int action);
+ @CriticalNative
+ private static native int nativeGetMetaState(long nativePtr);
+ @CriticalNative
+ private static native int nativeGetButtonState(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetButtonState(long nativePtr, int buttonState);
+ @CriticalNative
+ private static native int nativeGetClassification(long nativePtr);
+ @CriticalNative
+ private static native int nativeGetActionButton(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetActionButton(long nativePtr, int actionButton);
+ @CriticalNative
+ private static native void nativeOffsetLocation(long nativePtr, float deltaX, float deltaY);
+ @CriticalNative
+ private static native float nativeGetXOffset(long nativePtr);
+ @CriticalNative
+ private static native float nativeGetYOffset(long nativePtr);
+ @CriticalNative
+ private static native float nativeGetXPrecision(long nativePtr);
+ @CriticalNative
+ private static native float nativeGetYPrecision(long nativePtr);
+ @CriticalNative
+ private static native float nativeGetXCursorPosition(long nativePtr);
+ @CriticalNative
+ private static native float nativeGetYCursorPosition(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetCursorPosition(long nativePtr, float x, float y);
+ @CriticalNative
+ private static native long nativeGetDownTimeNanos(long nativePtr);
+ @CriticalNative
+ private static native void nativeSetDownTimeNanos(long nativePtr, long downTime);
+
+ @CriticalNative
+ private static native int nativeGetPointerCount(long nativePtr);
+ @CriticalNative
+ private static native int nativeFindPointerIndex(long nativePtr, int pointerId);
+
+ @CriticalNative
+ private static native int nativeGetHistorySize(long nativePtr);
+
+ @CriticalNative
+ private static native void nativeScale(long nativePtr, float scale);
+
+ private MotionEvent() {
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mNativePtr != 0) {
+ nativeDispose(mNativePtr);
+ mNativePtr = 0;
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @UnsupportedAppUsage
+ static private MotionEvent obtain() {
+ final MotionEvent ev;
+ synchronized (gRecyclerLock) {
+ ev = gRecyclerTop;
+ if (ev == null) {
+ return new MotionEvent();
+ }
+ gRecyclerTop = ev.mNext;
+ gRecyclerUsed -= 1;
+ }
+ ev.mNext = null;
+ ev.prepareForReuse();
+ return ev;
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param pointerCount The number of pointers that will be in this event.
+ * @param pointerProperties An array of <em>pointerCount</em> values providing
+ * a {@link PointerProperties} property object for each pointer, which must
+ * include the pointer identifier.
+ * @param pointerCoords An array of <em>pointerCount</em> values providing
+ * a {@link PointerCoords} coordinate object for each pointer.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param buttonState The state of buttons that are pressed.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ * @param source The source of this event.
+ * @param displayId The display ID associated with this event.
+ * @param flags The motion event flags.
+ * @hide
+ */
+ static public MotionEvent obtain(long downTime, long eventTime,
+ int action, int pointerCount, PointerProperties[] pointerProperties,
+ PointerCoords[] pointerCoords, int metaState, int buttonState,
+ float xPrecision, float yPrecision, int deviceId,
+ int edgeFlags, int source, int displayId, int flags) {
+ MotionEvent ev = obtain();
+ final boolean success = ev.initialize(deviceId, source, displayId, action, flags, edgeFlags,
+ metaState, buttonState, CLASSIFICATION_NONE, 0, 0, xPrecision, yPrecision,
+ downTime * NS_PER_MS, eventTime * NS_PER_MS,
+ pointerCount, pointerProperties, pointerCoords);
+ if (!success) {
+ Log.e(TAG, "Could not initialize MotionEvent");
+ ev.recycle();
+ return null;
+ }
+ return ev;
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param pointerCount The number of pointers that will be in this event.
+ * @param pointerProperties An array of <em>pointerCount</em> values providing
+ * a {@link PointerProperties} property object for each pointer, which must
+ * include the pointer identifier.
+ * @param pointerCoords An array of <em>pointerCount</em> values providing
+ * a {@link PointerCoords} coordinate object for each pointer.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param buttonState The state of buttons that are pressed.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ * @param source The source of this event.
+ * @param flags The motion event flags.
+ */
+ public static MotionEvent obtain(long downTime, long eventTime,
+ int action, int pointerCount, PointerProperties[] pointerProperties,
+ PointerCoords[] pointerCoords, int metaState, int buttonState,
+ float xPrecision, float yPrecision, int deviceId,
+ int edgeFlags, int source, int flags) {
+ return obtain(downTime, eventTime, action, pointerCount, pointerProperties, pointerCoords,
+ metaState, buttonState, xPrecision, yPrecision, deviceId, edgeFlags, source,
+ DEFAULT_DISPLAY, flags);
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param pointerCount The number of pointers that will be in this event.
+ * @param pointerIds An array of <em>pointerCount</em> values providing
+ * an identifier for each pointer.
+ * @param pointerCoords An array of <em>pointerCount</em> values providing
+ * a {@link PointerCoords} coordinate object for each pointer.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ * @param source The source of this event.
+ * @param flags The motion event flags.
+ *
+ * @deprecated Use {@link #obtain(long, long, int, int, PointerProperties[], PointerCoords[], int, int, float, float, int, int, int, int)}
+ * instead.
+ */
+ @Deprecated
+ static public MotionEvent obtain(long downTime, long eventTime,
+ int action, int pointerCount, int[] pointerIds, PointerCoords[] pointerCoords,
+ int metaState, float xPrecision, float yPrecision, int deviceId,
+ int edgeFlags, int source, int flags) {
+ synchronized (gSharedTempLock) {
+ ensureSharedTempPointerCapacity(pointerCount);
+ final PointerProperties[] pp = gSharedTempPointerProperties;
+ for (int i = 0; i < pointerCount; i++) {
+ pp[i].clear();
+ pp[i].id = pointerIds[i];
+ }
+ return obtain(downTime, eventTime, action, pointerCount, pp,
+ pointerCoords, metaState, 0, xPrecision, yPrecision, deviceId,
+ edgeFlags, source, flags);
+ }
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param x The X coordinate of this event.
+ * @param y The Y coordinate of this event.
+ * @param pressure The current pressure of this event. The pressure generally
+ * ranges from 0 (no pressure at all) to 1 (normal pressure), however
+ * values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ * @param size A scaled value of the approximate size of the area being pressed when
+ * touched with the finger. The actual value in pixels corresponding to the finger
+ * touch is normalized with a device specific range of values
+ * and scaled to a value between 0 and 1.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ */
+ static public MotionEvent obtain(long downTime, long eventTime, int action,
+ float x, float y, float pressure, float size, int metaState,
+ float xPrecision, float yPrecision, int deviceId, int edgeFlags) {
+ return obtain(downTime, eventTime, action, x, y, pressure, size, metaState,
+ xPrecision, yPrecision, deviceId, edgeFlags, InputDevice.SOURCE_UNKNOWN,
+ DEFAULT_DISPLAY);
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param x The X coordinate of this event.
+ * @param y The Y coordinate of this event.
+ * @param pressure The current pressure of this event. The pressure generally
+ * ranges from 0 (no pressure at all) to 1 (normal pressure), however
+ * values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ * @param size A scaled value of the approximate size of the area being pressed when
+ * touched with the finger. The actual value in pixels corresponding to the finger
+ * touch is normalized with a device specific range of values
+ * and scaled to a value between 0 and 1.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param source The source of this event.
+ * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ * @param displayId The display ID associated with this event.
+ * @hide
+ */
+ public static MotionEvent obtain(long downTime, long eventTime, int action,
+ float x, float y, float pressure, float size, int metaState,
+ float xPrecision, float yPrecision, int deviceId, int edgeFlags, int source,
+ int displayId) {
+ MotionEvent ev = obtain();
+ synchronized (gSharedTempLock) {
+ ensureSharedTempPointerCapacity(1);
+ final PointerProperties[] pp = gSharedTempPointerProperties;
+ pp[0].clear();
+ pp[0].id = 0;
+
+ final PointerCoords pc[] = gSharedTempPointerCoords;
+ pc[0].clear();
+ pc[0].x = x;
+ pc[0].y = y;
+ pc[0].pressure = pressure;
+ pc[0].size = size;
+
+ ev.initialize(deviceId, source, displayId,
+ action, 0, edgeFlags, metaState, 0 /*buttonState*/, CLASSIFICATION_NONE,
+ 0, 0, xPrecision, yPrecision,
+ downTime * NS_PER_MS, eventTime * NS_PER_MS,
+ 1, pp, pc);
+ return ev;
+ }
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param pointerCount The number of pointers that are active in this event.
+ * @param x The X coordinate of this event.
+ * @param y The Y coordinate of this event.
+ * @param pressure The current pressure of this event. The pressure generally
+ * ranges from 0 (no pressure at all) to 1 (normal pressure), however
+ * values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ * @param size A scaled value of the approximate size of the area being pressed when
+ * touched with the finger. The actual value in pixels corresponding to the finger
+ * touch is normalized with a device specific range of values
+ * and scaled to a value between 0 and 1.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ *
+ * @deprecated Use {@link #obtain(long, long, int, float, float, float, float, int, float, float, int, int)}
+ * instead.
+ */
+ @Deprecated
+ static public MotionEvent obtain(long downTime, long eventTime, int action,
+ int pointerCount, float x, float y, float pressure, float size, int metaState,
+ float xPrecision, float yPrecision, int deviceId, int edgeFlags) {
+ return obtain(downTime, eventTime, action, x, y, pressure, size,
+ metaState, xPrecision, yPrecision, deviceId, edgeFlags);
+ }
+
+ /**
+ * Create a new MotionEvent, filling in a subset of the basic motion
+ * values. Those not specified here are: device id (always 0), pressure
+ * and size (always 1), x and y precision (always 1), and edgeFlags (always 0).
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+ * @param x The X coordinate of this event.
+ * @param y The Y coordinate of this event.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ */
+ static public MotionEvent obtain(long downTime, long eventTime, int action,
+ float x, float y, int metaState) {
+ return obtain(downTime, eventTime, action, x, y, 1.0f, 1.0f,
+ metaState, 1.0f, 1.0f, 0, 0);
+ }
+
+ /**
+ * Create a new MotionEvent, copying from an existing one.
+ */
+ static public MotionEvent obtain(MotionEvent other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other motion event must not be null");
+ }
+
+ MotionEvent ev = obtain();
+ ev.mNativePtr = nativeCopy(ev.mNativePtr, other.mNativePtr, true /*keepHistory*/);
+ return ev;
+ }
+
+ /**
+ * Create a new MotionEvent, copying from an existing one, but not including
+ * any historical point information.
+ */
+ static public MotionEvent obtainNoHistory(MotionEvent other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other motion event must not be null");
+ }
+
+ MotionEvent ev = obtain();
+ ev.mNativePtr = nativeCopy(ev.mNativePtr, other.mNativePtr, false /*keepHistory*/);
+ return ev;
+ }
+
+ private boolean initialize(int deviceId, int source, int displayId, int action, int flags,
+ int edgeFlags, int metaState, int buttonState, @Classification int classification,
+ float xOffset, float yOffset, float xPrecision, float yPrecision,
+ long downTimeNanos, long eventTimeNanos,
+ int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords) {
+ mNativePtr = nativeInitialize(mNativePtr, deviceId, source, displayId, action, flags,
+ edgeFlags, metaState, buttonState, classification, xOffset, yOffset,
+ xPrecision, yPrecision, downTimeNanos, eventTimeNanos, pointerCount, pointerIds,
+ pointerCoords);
+ if (mNativePtr == 0) {
+ return false;
+ }
+ updateCursorPosition();
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ @UnsupportedAppUsage
+ public MotionEvent copy() {
+ return obtain(this);
+ }
+
+ /**
+ * Recycle the MotionEvent, to be re-used by a later caller. After calling
+ * this function you must not ever touch the event again.
+ */
+ @Override
+ public final void recycle() {
+ super.recycle();
+
+ synchronized (gRecyclerLock) {
+ if (gRecyclerUsed < MAX_RECYCLED) {
+ gRecyclerUsed++;
+ mNext = gRecyclerTop;
+ gRecyclerTop = this;
+ }
+ }
+ }
+
+ /**
+ * Applies a scale factor to all points within this event.
+ *
+ * This method is used to adjust touch events to simulate different density
+ * displays for compatibility mode. The values returned by {@link #getRawX()},
+ * {@link #getRawY()}, {@link #getXPrecision()} and {@link #getYPrecision()}
+ * are also affected by the scale factor.
+ *
+ * @param scale The scale factor to apply.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final void scale(float scale) {
+ if (scale != 1.0f) {
+ nativeScale(mNativePtr, scale);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public int getId() {
+ return nativeGetId(mNativePtr);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final int getDeviceId() {
+ return nativeGetDeviceId(mNativePtr);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final int getSource() {
+ return nativeGetSource(mNativePtr);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final void setSource(int source) {
+ if (source == getSource()) {
+ return;
+ }
+ nativeSetSource(mNativePtr, source);
+ updateCursorPosition();
+ }
+
+ /** @hide */
+ @Override
+ public int getDisplayId() {
+ return nativeGetDisplayId(mNativePtr);
+ }
+
+ /** @hide */
+ @TestApi
+ @Override
+ public void setDisplayId(int displayId) {
+ nativeSetDisplayId(mNativePtr, displayId);
+ }
+
+ /**
+ * Return the kind of action being performed.
+ * Consider using {@link #getActionMasked} and {@link #getActionIndex} to retrieve
+ * the separate masked action and pointer index.
+ * @return The action, such as {@link #ACTION_DOWN} or
+ * the combination of {@link #ACTION_POINTER_DOWN} with a shifted pointer index.
+ */
+ public final int getAction() {
+ return nativeGetAction(mNativePtr);
+ }
+
+ /**
+ * Return the masked action being performed, without pointer index information.
+ * Use {@link #getActionIndex} to return the index associated with pointer actions.
+ * @return The action, such as {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}.
+ */
+ public final int getActionMasked() {
+ return nativeGetAction(mNativePtr) & ACTION_MASK;
+ }
+
+ /**
+ * For {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP}
+ * as returned by {@link #getActionMasked}, this returns the associated
+ * pointer index.
+ * The index may be used with {@link #getPointerId(int)},
+ * {@link #getX(int)}, {@link #getY(int)}, {@link #getPressure(int)},
+ * and {@link #getSize(int)} to get information about the pointer that has
+ * gone down or up.
+ * @return The index associated with the action.
+ */
+ public final int getActionIndex() {
+ return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
+ >> ACTION_POINTER_INDEX_SHIFT;
+ }
+
+ /**
+ * Returns true if this motion event is a touch event.
+ * <p>
+ * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE},
+ * {@link #ACTION_HOVER_ENTER}, {@link #ACTION_HOVER_EXIT}, or {@link #ACTION_SCROLL}
+ * because they are not actually touch events (the pointer is not down).
+ * </p>
+ * @return True if this motion event is a touch event.
+ * @hide
+ */
+ public final boolean isTouchEvent() {
+ return nativeIsTouchEvent(mNativePtr);
+ }
+
+ /**
+ * Gets the motion event flags.
+ *
+ * @see #FLAG_WINDOW_IS_OBSCURED
+ */
+ public final int getFlags() {
+ return nativeGetFlags(mNativePtr);
+ }
+
+ /** @hide */
+ @Override
+ public final boolean isTainted() {
+ final int flags = getFlags();
+ return (flags & FLAG_TAINTED) != 0;
+ }
+
+ /** @hide */
+ @Override
+ public final void setTainted(boolean tainted) {
+ final int flags = getFlags();
+ nativeSetFlags(mNativePtr, tainted ? flags | FLAG_TAINTED : flags & ~FLAG_TAINTED);
+ }
+
+ /** @hide */
+ public boolean isTargetAccessibilityFocus() {
+ final int flags = getFlags();
+ return (flags & FLAG_TARGET_ACCESSIBILITY_FOCUS) != 0;
+ }
+
+ /** @hide */
+ public void setTargetAccessibilityFocus(boolean targetsFocus) {
+ final int flags = getFlags();
+ nativeSetFlags(mNativePtr, targetsFocus
+ ? flags | FLAG_TARGET_ACCESSIBILITY_FOCUS
+ : flags & ~FLAG_TARGET_ACCESSIBILITY_FOCUS);
+ }
+
+ /** @hide */
+ public final boolean isHoverExitPending() {
+ final int flags = getFlags();
+ return (flags & FLAG_HOVER_EXIT_PENDING) != 0;
+ }
+
+ /** @hide */
+ public void setHoverExitPending(boolean hoverExitPending) {
+ final int flags = getFlags();
+ nativeSetFlags(mNativePtr, hoverExitPending
+ ? flags | FLAG_HOVER_EXIT_PENDING
+ : flags & ~FLAG_HOVER_EXIT_PENDING);
+ }
+
+ /**
+ * Returns the time (in ms) when the user originally pressed down to start
+ * a stream of position events.
+ */
+ public final long getDownTime() {
+ return nativeGetDownTimeNanos(mNativePtr) / NS_PER_MS;
+ }
+
+ /**
+ * Sets the time (in ms) when the user originally pressed down to start
+ * a stream of position events.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final void setDownTime(long downTime) {
+ nativeSetDownTimeNanos(mNativePtr, downTime * NS_PER_MS);
+ }
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ */
+ @Override
+ public final long getEventTime() {
+ return nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT) / NS_PER_MS;
+ }
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond precision.
+ * <p>
+ * The value is in nanosecond precision but it may not have nanosecond accuracy.
+ * </p>
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond precision.
+ *
+ * @hide
+ */
+ @Override
+ @UnsupportedAppUsage
+ public final long getEventTimeNano() {
+ return nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getX(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_X
+ */
+ public final float getX() {
+ return nativeGetAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getY(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_Y
+ */
+ public final float getY() {
+ return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getPressure(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_PRESSURE
+ */
+ public final float getPressure() {
+ return nativeGetAxisValue(mNativePtr, AXIS_PRESSURE, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getSize(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_SIZE
+ */
+ public final float getSize() {
+ return nativeGetAxisValue(mNativePtr, AXIS_SIZE, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getTouchMajor(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_TOUCH_MAJOR
+ */
+ public final float getTouchMajor() {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MAJOR, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getTouchMinor(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_TOUCH_MINOR
+ */
+ public final float getTouchMinor() {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MINOR, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getToolMajor(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_TOOL_MAJOR
+ */
+ public final float getToolMajor() {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MAJOR, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getToolMinor(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_TOOL_MINOR
+ */
+ public final float getToolMinor() {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MINOR, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getOrientation(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @see #AXIS_ORIENTATION
+ */
+ public final float getOrientation() {
+ return nativeGetAxisValue(mNativePtr, AXIS_ORIENTATION, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * {@link #getAxisValue(int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param axis The axis identifier for the axis value to retrieve.
+ *
+ * @see #AXIS_X
+ * @see #AXIS_Y
+ */
+ public final float getAxisValue(int axis) {
+ return nativeGetAxisValue(mNativePtr, axis, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * The number of pointers of data contained in this event. Always
+ * >= 1.
+ */
+ public final int getPointerCount() {
+ return nativeGetPointerCount(mNativePtr);
+ }
+
+ /**
+ * Return the pointer identifier associated with a particular pointer
+ * data index in this event. The identifier tells you the actual pointer
+ * number associated with the data, accounting for individual pointers
+ * going up and down since the start of the current gesture.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ */
+ public final int getPointerId(int pointerIndex) {
+ return nativeGetPointerId(mNativePtr, pointerIndex);
+ }
+
+ /**
+ * Gets the tool type of a pointer for the given pointer index.
+ * The tool type indicates the type of tool used to make contact such
+ * as a finger or stylus, if known.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @return The tool type of the pointer.
+ *
+ * @see #TOOL_TYPE_UNKNOWN
+ * @see #TOOL_TYPE_FINGER
+ * @see #TOOL_TYPE_STYLUS
+ * @see #TOOL_TYPE_MOUSE
+ */
+ public final int getToolType(int pointerIndex) {
+ return nativeGetToolType(mNativePtr, pointerIndex);
+ }
+
+ /**
+ * Given a pointer identifier, find the index of its data in the event.
+ *
+ * @param pointerId The identifier of the pointer to be found.
+ * @return Returns either the index of the pointer (for use with
+ * {@link #getX(int)} et al.), or -1 if there is no data available for
+ * that pointer identifier.
+ */
+ public final int findPointerIndex(int pointerId) {
+ return nativeFindPointerIndex(mNativePtr, pointerId);
+ }
+
+ /**
+ * Returns the X coordinate of this event for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * Whole numbers are pixels; the
+ * value may have a fraction for input devices that are sub-pixel precise.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_X
+ */
+ public final float getX(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the Y coordinate of this event for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * Whole numbers are pixels; the
+ * value may have a fraction for input devices that are sub-pixel precise.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_Y
+ */
+ public final float getY(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_Y, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the current pressure of this event for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * The pressure generally
+ * ranges from 0 (no pressure at all) to 1 (normal pressure), however
+ * values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_PRESSURE
+ */
+ public final float getPressure(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_PRESSURE, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns a scaled value of the approximate size for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * This represents some approximation of the area of the screen being
+ * pressed; the actual value in pixels corresponding to the
+ * touch is normalized with the device specific range of values
+ * and scaled to a value between 0 and 1. The value of size can be used to
+ * determine fat touch events.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_SIZE
+ */
+ public final float getSize(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_SIZE, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the length of the major axis of an ellipse that describes the touch
+ * area at the point of contact for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_TOUCH_MAJOR
+ */
+ public final float getTouchMajor(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MAJOR, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the length of the minor axis of an ellipse that describes the touch
+ * area at the point of contact for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_TOUCH_MINOR
+ */
+ public final float getTouchMinor(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MINOR, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the length of the major axis of an ellipse that describes the size of
+ * the approaching tool for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * The tool area represents the estimated size of the finger or pen that is
+ * touching the device independent of its actual touch area at the point of contact.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_TOOL_MAJOR
+ */
+ public final float getToolMajor(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MAJOR, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the length of the minor axis of an ellipse that describes the size of
+ * the approaching tool for the given pointer
+ * <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * The tool area represents the estimated size of the finger or pen that is
+ * touching the device independent of its actual touch area at the point of contact.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_TOOL_MINOR
+ */
+ public final float getToolMinor(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MINOR, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the orientation of the touch area and tool area in radians clockwise from vertical
+ * for the given pointer <em>index</em> (use {@link #getPointerId(int)} to find the pointer
+ * identifier for this index).
+ * An angle of 0 radians indicates that the major axis of contact is oriented
+ * upwards, is perfectly circular or is of unknown orientation. A positive angle
+ * indicates that the major axis of contact is oriented to the right. A negative angle
+ * indicates that the major axis of contact is oriented to the left.
+ * The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians
+ * (finger pointing fully right).
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #AXIS_ORIENTATION
+ */
+ public final float getOrientation(int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, AXIS_ORIENTATION, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the value of the requested axis for the given pointer <em>index</em>
+ * (use {@link #getPointerId(int)} to find the pointer identifier for this index).
+ *
+ * @param axis The axis identifier for the axis value to retrieve.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @return The value of the axis, or 0 if the axis is not available.
+ *
+ * @see #AXIS_X
+ * @see #AXIS_Y
+ */
+ public final float getAxisValue(int axis, int pointerIndex) {
+ return nativeGetAxisValue(mNativePtr, axis, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Populates a {@link PointerCoords} object with pointer coordinate data for
+ * the specified pointer index.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param outPointerCoords The pointer coordinate object to populate.
+ *
+ * @see PointerCoords
+ */
+ public final void getPointerCoords(int pointerIndex, PointerCoords outPointerCoords) {
+ nativeGetPointerCoords(mNativePtr, pointerIndex, HISTORY_CURRENT, outPointerCoords);
+ }
+
+ /**
+ * Populates a {@link PointerProperties} object with pointer properties for
+ * the specified pointer index.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param outPointerProperties The pointer properties object to populate.
+ *
+ * @see PointerProperties
+ */
+ public final void getPointerProperties(int pointerIndex,
+ PointerProperties outPointerProperties) {
+ nativeGetPointerProperties(mNativePtr, pointerIndex, outPointerProperties);
+ }
+
+ /**
+ * Returns the state of any meta / modifier keys that were in effect when
+ * the event was generated. This is the same values as those
+ * returned by {@link KeyEvent#getMetaState() KeyEvent.getMetaState}.
+ *
+ * @return an integer in which each bit set to 1 represents a pressed
+ * meta key
+ *
+ * @see KeyEvent#getMetaState()
+ */
+ public final int getMetaState() {
+ return nativeGetMetaState(mNativePtr);
+ }
+
+ /**
+ * Gets the state of all buttons that are pressed such as a mouse or stylus button.
+ *
+ * @return The button state.
+ *
+ * @see #BUTTON_PRIMARY
+ * @see #BUTTON_SECONDARY
+ * @see #BUTTON_TERTIARY
+ * @see #BUTTON_FORWARD
+ * @see #BUTTON_BACK
+ * @see #BUTTON_STYLUS_PRIMARY
+ * @see #BUTTON_STYLUS_SECONDARY
+ */
+ public final int getButtonState() {
+ return nativeGetButtonState(mNativePtr);
+ }
+
+ /**
+ * Sets the bitfield indicating which buttons are pressed.
+ *
+ * @see #getButtonState()
+ * @hide
+ */
+ @TestApi
+ public final void setButtonState(int buttonState) {
+ nativeSetButtonState(mNativePtr, buttonState);
+ }
+
+ /**
+ * Returns the classification for the current gesture.
+ * The classification may change as more events become available for the same gesture.
+ *
+ * @see #CLASSIFICATION_NONE
+ * @see #CLASSIFICATION_AMBIGUOUS_GESTURE
+ * @see #CLASSIFICATION_DEEP_PRESS
+ */
+ public @Classification int getClassification() {
+ return nativeGetClassification(mNativePtr);
+ }
+
+ /**
+ * Gets which button has been modified during a press or release action.
+ *
+ * For actions other than {@link #ACTION_BUTTON_PRESS} and {@link #ACTION_BUTTON_RELEASE}
+ * the returned value is undefined.
+ *
+ * @see #getButtonState()
+ */
+ public final int getActionButton() {
+ return nativeGetActionButton(mNativePtr);
+ }
+
+ /**
+ * Sets the action button for the event.
+ *
+ * @see #getActionButton()
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public final void setActionButton(int button) {
+ nativeSetActionButton(mNativePtr, button);
+ }
+
+ /**
+ * Returns the original raw X coordinate of this event. For touch
+ * events on the screen, this is the original location of the event
+ * on the screen, before it had been adjusted for the containing window
+ * and views.
+ *
+ * @see #getX(int)
+ * @see #AXIS_X
+ */
+ public final float getRawX() {
+ return nativeGetRawAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the original raw Y coordinate of this event. For touch
+ * events on the screen, this is the original location of the event
+ * on the screen, before it had been adjusted for the containing window
+ * and views.
+ *
+ * @see #getY(int)
+ * @see #AXIS_Y
+ */
+ public final float getRawY() {
+ return nativeGetRawAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the original raw X coordinate of this event. For touch
+ * events on the screen, this is the original location of the event
+ * on the screen, before it had been adjusted for the containing window
+ * and views.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #getX(int)
+ * @see #AXIS_X
+ */
+ public float getRawX(int pointerIndex) {
+ return nativeGetRawAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Returns the original raw Y coordinate of this event. For touch
+ * events on the screen, this is the original location of the event
+ * on the screen, before it had been adjusted for the containing window
+ * and views.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ *
+ * @see #getY(int)
+ * @see #AXIS_Y
+ */
+ public float getRawY(int pointerIndex) {
+ return nativeGetRawAxisValue(mNativePtr, AXIS_Y, pointerIndex, HISTORY_CURRENT);
+ }
+
+ /**
+ * Return the precision of the X coordinates being reported. You can
+ * multiply this number with {@link #getX} to find the actual hardware
+ * value of the X coordinate.
+ * @return Returns the precision of X coordinates being reported.
+ *
+ * @see #AXIS_X
+ */
+ public final float getXPrecision() {
+ return nativeGetXPrecision(mNativePtr);
+ }
+
+ /**
+ * Return the precision of the Y coordinates being reported. You can
+ * multiply this number with {@link #getY} to find the actual hardware
+ * value of the Y coordinate.
+ * @return Returns the precision of Y coordinates being reported.
+ *
+ * @see #AXIS_Y
+ */
+ public final float getYPrecision() {
+ return nativeGetYPrecision(mNativePtr);
+ }
+
+ /**
+ * Returns the x coordinate of mouse cursor position when this event is
+ * reported. This value is only valid if {@link #getSource()} returns
+ * {@link InputDevice#SOURCE_MOUSE}.
+ *
+ * @hide
+ */
+ public float getXCursorPosition() {
+ return nativeGetXCursorPosition(mNativePtr);
+ }
+
+ /**
+ * Returns the y coordinate of mouse cursor position when this event is
+ * reported. This value is only valid if {@link #getSource()} returns
+ * {@link InputDevice#SOURCE_MOUSE}.
+ *
+ * @hide
+ */
+ public float getYCursorPosition() {
+ return nativeGetYCursorPosition(mNativePtr);
+ }
+
+ /**
+ * Sets cursor position to given coordinates. The coordinate in parameters should be after
+ * offsetting. In other words, the effect of this function is {@link #getXCursorPosition()} and
+ * {@link #getYCursorPosition()} will return the same value passed in the parameters.
+ *
+ * @hide
+ */
+ private void setCursorPosition(float x, float y) {
+ nativeSetCursorPosition(mNativePtr, x, y);
+ }
+
+ /**
+ * Returns the number of historical points in this event. These are
+ * movements that have occurred between this event and the previous event.
+ * This only applies to ACTION_MOVE events -- all other actions will have
+ * a size of 0.
+ *
+ * @return Returns the number of historical points in the event.
+ */
+ public final int getHistorySize() {
+ return nativeGetHistorySize(mNativePtr);
+ }
+
+ /**
+ * Returns the time that a historical movement occurred between this event
+ * and the previous event, in the {@link android.os.SystemClock#uptimeMillis} time base.
+ * <p>
+ * This only applies to ACTION_MOVE events.
+ * </p>
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ * @return Returns the time that a historical movement occurred between this
+ * event and the previous event,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ *
+ * @see #getHistorySize
+ * @see #getEventTime
+ */
+ public final long getHistoricalEventTime(int pos) {
+ return nativeGetEventTimeNanos(mNativePtr, pos) / NS_PER_MS;
+ }
+
+ /**
+ * Returns the time that a historical movement occurred between this event
+ * and the previous event, in the {@link android.os.SystemClock#uptimeMillis} time base
+ * but with nanosecond (instead of millisecond) precision.
+ * <p>
+ * This only applies to ACTION_MOVE events.
+ * </p><p>
+ * The value is in nanosecond precision but it may not have nanosecond accuracy.
+ * </p>
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ * @return Returns the time that a historical movement occurred between this
+ * event and the previous event,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base but with
+ * nanosecond (instead of millisecond) precision.
+ *
+ * @see #getHistorySize
+ * @see #getEventTime
+ *
+ * @hide
+ */
+ public final long getHistoricalEventTimeNano(int pos) {
+ return nativeGetEventTimeNanos(mNativePtr, pos);
+ }
+
+ /**
+ * {@link #getHistoricalX(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getX()
+ * @see #AXIS_X
+ */
+ public final float getHistoricalX(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_X, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalY(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getY()
+ * @see #AXIS_Y
+ */
+ public final float getHistoricalY(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalPressure(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getPressure()
+ * @see #AXIS_PRESSURE
+ */
+ public final float getHistoricalPressure(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_PRESSURE, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalSize(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getSize()
+ * @see #AXIS_SIZE
+ */
+ public final float getHistoricalSize(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_SIZE, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalTouchMajor(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getTouchMajor()
+ * @see #AXIS_TOUCH_MAJOR
+ */
+ public final float getHistoricalTouchMajor(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MAJOR, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalTouchMinor(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getTouchMinor()
+ * @see #AXIS_TOUCH_MINOR
+ */
+ public final float getHistoricalTouchMinor(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MINOR, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalToolMajor(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getToolMajor()
+ * @see #AXIS_TOOL_MAJOR
+ */
+ public final float getHistoricalToolMajor(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MAJOR, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalToolMinor(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getToolMinor()
+ * @see #AXIS_TOOL_MINOR
+ */
+ public final float getHistoricalToolMinor(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MINOR, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalOrientation(int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getOrientation()
+ * @see #AXIS_ORIENTATION
+ */
+ public final float getHistoricalOrientation(int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_ORIENTATION, 0, pos);
+ }
+
+ /**
+ * {@link #getHistoricalAxisValue(int, int, int)} for the first pointer index (may be an
+ * arbitrary pointer identifier).
+ *
+ * @param axis The axis identifier for the axis value to retrieve.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getAxisValue(int)
+ * @see #AXIS_X
+ * @see #AXIS_Y
+ */
+ public final float getHistoricalAxisValue(int axis, int pos) {
+ return nativeGetAxisValue(mNativePtr, axis, 0, pos);
+ }
+
+ /**
+ * Returns a historical X coordinate, as per {@link #getX(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getX(int)
+ * @see #AXIS_X
+ */
+ public final float getHistoricalX(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical Y coordinate, as per {@link #getY(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getY(int)
+ * @see #AXIS_Y
+ */
+ public final float getHistoricalY(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_Y, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical pressure coordinate, as per {@link #getPressure(int)},
+ * that occurred between this event and the previous event for the given
+ * pointer. Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getPressure(int)
+ * @see #AXIS_PRESSURE
+ */
+ public final float getHistoricalPressure(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_PRESSURE, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical size coordinate, as per {@link #getSize(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getSize(int)
+ * @see #AXIS_SIZE
+ */
+ public final float getHistoricalSize(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_SIZE, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical touch major axis coordinate, as per {@link #getTouchMajor(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getTouchMajor(int)
+ * @see #AXIS_TOUCH_MAJOR
+ */
+ public final float getHistoricalTouchMajor(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MAJOR, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical touch minor axis coordinate, as per {@link #getTouchMinor(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getTouchMinor(int)
+ * @see #AXIS_TOUCH_MINOR
+ */
+ public final float getHistoricalTouchMinor(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOUCH_MINOR, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical tool major axis coordinate, as per {@link #getToolMajor(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getToolMajor(int)
+ * @see #AXIS_TOOL_MAJOR
+ */
+ public final float getHistoricalToolMajor(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MAJOR, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical tool minor axis coordinate, as per {@link #getToolMinor(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getToolMinor(int)
+ * @see #AXIS_TOOL_MINOR
+ */
+ public final float getHistoricalToolMinor(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_TOOL_MINOR, pointerIndex, pos);
+ }
+
+ /**
+ * Returns a historical orientation coordinate, as per {@link #getOrientation(int)}, that
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getOrientation(int)
+ * @see #AXIS_ORIENTATION
+ */
+ public final float getHistoricalOrientation(int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, AXIS_ORIENTATION, pointerIndex, pos);
+ }
+
+ /**
+ * Returns the historical value of the requested axis, as per {@link #getAxisValue(int, int)},
+ * occurred between this event and the previous event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param axis The axis identifier for the axis value to retrieve.
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ * @return The value of the axis, or 0 if the axis is not available.
+ *
+ * @see #AXIS_X
+ * @see #AXIS_Y
+ */
+ public final float getHistoricalAxisValue(int axis, int pointerIndex, int pos) {
+ return nativeGetAxisValue(mNativePtr, axis, pointerIndex, pos);
+ }
+
+ /**
+ * Populates a {@link PointerCoords} object with historical pointer coordinate data,
+ * as per {@link #getPointerCoords}, that occurred between this event and the previous
+ * event for the given pointer.
+ * Only applies to ACTION_MOVE events.
+ *
+ * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0
+ * (the first pointer that is down) to {@link #getPointerCount()}-1.
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ * @param outPointerCoords The pointer coordinate object to populate.
+ *
+ * @see #getHistorySize
+ * @see #getPointerCoords
+ * @see PointerCoords
+ */
+ public final void getHistoricalPointerCoords(int pointerIndex, int pos,
+ PointerCoords outPointerCoords) {
+ nativeGetPointerCoords(mNativePtr, pointerIndex, pos, outPointerCoords);
+ }
+
+ /**
+ * Returns a bitfield indicating which edges, if any, were touched by this
+ * MotionEvent. For touch events, clients can use this to determine if the
+ * user's finger was touching the edge of the display.
+ *
+ * This property is only set for {@link #ACTION_DOWN} events.
+ *
+ * @see #EDGE_LEFT
+ * @see #EDGE_TOP
+ * @see #EDGE_RIGHT
+ * @see #EDGE_BOTTOM
+ */
+ public final int getEdgeFlags() {
+ return nativeGetEdgeFlags(mNativePtr);
+ }
+
+ /**
+ * Sets the bitfield indicating which edges, if any, were touched by this
+ * MotionEvent.
+ *
+ * @see #getEdgeFlags()
+ */
+ public final void setEdgeFlags(int flags) {
+ nativeSetEdgeFlags(mNativePtr, flags);
+ }
+
+ /**
+ * Sets this event's action.
+ */
+ public final void setAction(int action) {
+ nativeSetAction(mNativePtr, action);
+ }
+
+ /**
+ * Adjust this event's location.
+ * @param deltaX Amount to add to the current X coordinate of the event.
+ * @param deltaY Amount to add to the current Y coordinate of the event.
+ */
+ public final void offsetLocation(float deltaX, float deltaY) {
+ if (deltaX != 0.0f || deltaY != 0.0f) {
+ nativeOffsetLocation(mNativePtr, deltaX, deltaY);
+ }
+ }
+
+ /**
+ * Set this event's location. Applies {@link #offsetLocation} with a
+ * delta from the current location to the given new location.
+ *
+ * @param x New absolute X location.
+ * @param y New absolute Y location.
+ */
+ public final void setLocation(float x, float y) {
+ float oldX = getX();
+ float oldY = getY();
+ offsetLocation(x - oldX, y - oldY);
+ }
+
+ /**
+ * Applies a transformation matrix to all of the points in the event.
+ *
+ * @param matrix The transformation matrix to apply.
+ */
+ public final void transform(Matrix matrix) {
+ if (matrix == null) {
+ throw new IllegalArgumentException("matrix must not be null");
+ }
+
+ nativeTransform(mNativePtr, matrix);
+ }
+
+ /**
+ * Transforms all of the points in the event directly instead of modifying the event's
+ * internal transform.
+ *
+ * @param matrix The transformation matrix to apply.
+ * @hide
+ */
+ public void applyTransform(Matrix matrix) {
+ if (matrix == null) {
+ throw new IllegalArgumentException("matrix must not be null");
+ }
+
+ nativeApplyTransform(mNativePtr, matrix);
+ }
+
+ /**
+ * Add a new movement to the batch of movements in this event. The event's
+ * current location, position and size is updated to the new values.
+ * The current values in the event are added to a list of historical values.
+ *
+ * Only applies to {@link #ACTION_MOVE} or {@link #ACTION_HOVER_MOVE} events.
+ *
+ * @param eventTime The time stamp (in ms) for this data.
+ * @param x The new X position.
+ * @param y The new Y position.
+ * @param pressure The new pressure.
+ * @param size The new size.
+ * @param metaState Meta key state.
+ */
+ public final void addBatch(long eventTime, float x, float y,
+ float pressure, float size, int metaState) {
+ synchronized (gSharedTempLock) {
+ ensureSharedTempPointerCapacity(1);
+ final PointerCoords[] pc = gSharedTempPointerCoords;
+ pc[0].clear();
+ pc[0].x = x;
+ pc[0].y = y;
+ pc[0].pressure = pressure;
+ pc[0].size = size;
+
+ nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, pc, metaState);
+ }
+ }
+
+ /**
+ * Add a new movement to the batch of movements in this event. The event's
+ * current location, position and size is updated to the new values.
+ * The current values in the event are added to a list of historical values.
+ *
+ * Only applies to {@link #ACTION_MOVE} or {@link #ACTION_HOVER_MOVE} events.
+ *
+ * @param eventTime The time stamp (in ms) for this data.
+ * @param pointerCoords The new pointer coordinates.
+ * @param metaState Meta key state.
+ */
+ public final void addBatch(long eventTime, PointerCoords[] pointerCoords, int metaState) {
+ nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, pointerCoords, metaState);
+ }
+
+ /**
+ * Adds all of the movement samples of the specified event to this one if
+ * it is compatible. To be compatible, the event must have the same device id,
+ * source, display id, action, flags, classification, pointer count, pointer properties.
+ *
+ * Only applies to {@link #ACTION_MOVE} or {@link #ACTION_HOVER_MOVE} events.
+ *
+ * @param event The event whose movements samples should be added to this one
+ * if possible.
+ * @return True if batching was performed or false if batching was not possible.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final boolean addBatch(MotionEvent event) {
+ final int action = nativeGetAction(mNativePtr);
+ if (action != ACTION_MOVE && action != ACTION_HOVER_MOVE) {
+ return false;
+ }
+ if (action != nativeGetAction(event.mNativePtr)) {
+ return false;
+ }
+
+ if (nativeGetDeviceId(mNativePtr) != nativeGetDeviceId(event.mNativePtr)
+ || nativeGetSource(mNativePtr) != nativeGetSource(event.mNativePtr)
+ || nativeGetDisplayId(mNativePtr) != nativeGetDisplayId(event.mNativePtr)
+ || nativeGetFlags(mNativePtr) != nativeGetFlags(event.mNativePtr)
+ || nativeGetClassification(mNativePtr)
+ != nativeGetClassification(event.mNativePtr)) {
+ return false;
+ }
+
+ final int pointerCount = nativeGetPointerCount(mNativePtr);
+ if (pointerCount != nativeGetPointerCount(event.mNativePtr)) {
+ return false;
+ }
+
+ synchronized (gSharedTempLock) {
+ ensureSharedTempPointerCapacity(Math.max(pointerCount, 2));
+ final PointerProperties[] pp = gSharedTempPointerProperties;
+ final PointerCoords[] pc = gSharedTempPointerCoords;
+
+ for (int i = 0; i < pointerCount; i++) {
+ nativeGetPointerProperties(mNativePtr, i, pp[0]);
+ nativeGetPointerProperties(event.mNativePtr, i, pp[1]);
+ if (!pp[0].equals(pp[1])) {
+ return false;
+ }
+ }
+
+ final int metaState = nativeGetMetaState(event.mNativePtr);
+ final int historySize = nativeGetHistorySize(event.mNativePtr);
+ for (int h = 0; h <= historySize; h++) {
+ final int historyPos = (h == historySize ? HISTORY_CURRENT : h);
+
+ for (int i = 0; i < pointerCount; i++) {
+ nativeGetPointerCoords(event.mNativePtr, i, historyPos, pc[i]);
+ }
+
+ final long eventTimeNanos = nativeGetEventTimeNanos(event.mNativePtr, historyPos);
+ nativeAddBatch(mNativePtr, eventTimeNanos, pc, metaState);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if all points in the motion event are completely within the specified bounds.
+ * @hide
+ */
+ public final boolean isWithinBoundsNoHistory(float left, float top,
+ float right, float bottom) {
+ final int pointerCount = nativeGetPointerCount(mNativePtr);
+ for (int i = 0; i < pointerCount; i++) {
+ final float x = nativeGetAxisValue(mNativePtr, AXIS_X, i, HISTORY_CURRENT);
+ final float y = nativeGetAxisValue(mNativePtr, AXIS_Y, i, HISTORY_CURRENT);
+ if (x < left || x > right || y < top || y > bottom) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static final float clamp(float value, float low, float high) {
+ if (value < low) {
+ return low;
+ } else if (value > high) {
+ return high;
+ }
+ return value;
+ }
+
+ /**
+ * Returns a new motion events whose points have been clamped to the specified bounds.
+ * @hide
+ */
+ public final MotionEvent clampNoHistory(float left, float top, float right, float bottom) {
+ MotionEvent ev = obtain();
+ synchronized (gSharedTempLock) {
+ final int pointerCount = nativeGetPointerCount(mNativePtr);
+
+ ensureSharedTempPointerCapacity(pointerCount);
+ final PointerProperties[] pp = gSharedTempPointerProperties;
+ final PointerCoords[] pc = gSharedTempPointerCoords;
+
+ for (int i = 0; i < pointerCount; i++) {
+ nativeGetPointerProperties(mNativePtr, i, pp[i]);
+ nativeGetPointerCoords(mNativePtr, i, HISTORY_CURRENT, pc[i]);
+ pc[i].x = clamp(pc[i].x, left, right);
+ pc[i].y = clamp(pc[i].y, top, bottom);
+ }
+ ev.initialize(nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr),
+ nativeGetDisplayId(mNativePtr),
+ nativeGetAction(mNativePtr), nativeGetFlags(mNativePtr),
+ nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr),
+ nativeGetButtonState(mNativePtr), nativeGetClassification(mNativePtr),
+ nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr),
+ nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr),
+ nativeGetDownTimeNanos(mNativePtr),
+ nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT),
+ pointerCount, pp, pc);
+ return ev;
+ }
+ }
+
+ /**
+ * Gets an integer where each pointer id present in the event is marked as a bit.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final int getPointerIdBits() {
+ int idBits = 0;
+ final int pointerCount = nativeGetPointerCount(mNativePtr);
+ for (int i = 0; i < pointerCount; i++) {
+ idBits |= 1 << nativeGetPointerId(mNativePtr, i);
+ }
+ return idBits;
+ }
+
+ /**
+ * Splits a motion event such that it includes only a subset of pointer ids.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final MotionEvent split(int idBits) {
+ MotionEvent ev = obtain();
+ synchronized (gSharedTempLock) {
+ final int oldPointerCount = nativeGetPointerCount(mNativePtr);
+ ensureSharedTempPointerCapacity(oldPointerCount);
+ final PointerProperties[] pp = gSharedTempPointerProperties;
+ final PointerCoords[] pc = gSharedTempPointerCoords;
+ final int[] map = gSharedTempPointerIndexMap;
+
+ final int oldAction = nativeGetAction(mNativePtr);
+ final int oldActionMasked = oldAction & ACTION_MASK;
+ final int oldActionPointerIndex = (oldAction & ACTION_POINTER_INDEX_MASK)
+ >> ACTION_POINTER_INDEX_SHIFT;
+ int newActionPointerIndex = -1;
+ int newPointerCount = 0;
+ for (int i = 0; i < oldPointerCount; i++) {
+ nativeGetPointerProperties(mNativePtr, i, pp[newPointerCount]);
+ final int idBit = 1 << pp[newPointerCount].id;
+ if ((idBit & idBits) != 0) {
+ if (i == oldActionPointerIndex) {
+ newActionPointerIndex = newPointerCount;
+ }
+ map[newPointerCount] = i;
+ newPointerCount += 1;
+ }
+ }
+
+ if (newPointerCount == 0) {
+ throw new IllegalArgumentException("idBits did not match any ids in the event");
+ }
+
+ final int newAction;
+ if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) {
+ if (newActionPointerIndex < 0) {
+ // An unrelated pointer changed.
+ newAction = ACTION_MOVE;
+ } else if (newPointerCount == 1) {
+ // The first/last pointer went down/up.
+ newAction = oldActionMasked == ACTION_POINTER_DOWN
+ ? ACTION_DOWN
+ : (getFlags() & FLAG_CANCELED) == 0 ? ACTION_UP : ACTION_CANCEL;
+ } else {
+ // A secondary pointer went down/up.
+ newAction = oldActionMasked
+ | (newActionPointerIndex << ACTION_POINTER_INDEX_SHIFT);
+ }
+ } else {
+ // Simple up/down/cancel/move or other motion action.
+ newAction = oldAction;
+ }
+
+ final int historySize = nativeGetHistorySize(mNativePtr);
+ for (int h = 0; h <= historySize; h++) {
+ final int historyPos = h == historySize ? HISTORY_CURRENT : h;
+
+ for (int i = 0; i < newPointerCount; i++) {
+ nativeGetPointerCoords(mNativePtr, map[i], historyPos, pc[i]);
+ }
+
+ final long eventTimeNanos = nativeGetEventTimeNanos(mNativePtr, historyPos);
+ if (h == 0) {
+ ev.initialize(nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr),
+ nativeGetDisplayId(mNativePtr),
+ newAction, nativeGetFlags(mNativePtr),
+ nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr),
+ nativeGetButtonState(mNativePtr), nativeGetClassification(mNativePtr),
+ nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr),
+ nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr),
+ nativeGetDownTimeNanos(mNativePtr), eventTimeNanos,
+ newPointerCount, pp, pc);
+ } else {
+ nativeAddBatch(ev.mNativePtr, eventTimeNanos, pc, 0);
+ }
+ }
+ return ev;
+ }
+ }
+
+ /**
+ * Calculate new cursor position for events from mouse. This is used to split, clamp and inject
+ * events.
+ *
+ * <p>If the source is mouse, it sets cursor position to the centroid of all pointers because
+ * InputReader maps multiple fingers on a touchpad to locations around cursor position in screen
+ * coordinates so that the mouse cursor is at the centroid of all pointers.
+ *
+ * <p>If the source is not mouse it sets cursor position to NaN.
+ */
+ private void updateCursorPosition() {
+ if (getSource() != InputDevice.SOURCE_MOUSE) {
+ setCursorPosition(INVALID_CURSOR_POSITION, INVALID_CURSOR_POSITION);
+ return;
+ }
+
+ float x = 0;
+ float y = 0;
+
+ final int pointerCount = getPointerCount();
+ for (int i = 0; i < pointerCount; ++i) {
+ x += getX(i);
+ y += getY(i);
+ }
+
+ // If pointer count is 0, divisions below yield NaN, which is an acceptable result for this
+ // corner case.
+ x /= pointerCount;
+ y /= pointerCount;
+ setCursorPosition(x, y);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder msg = new StringBuilder();
+ msg.append("MotionEvent { action=").append(actionToString(getAction()));
+ appendUnless("0", msg, ", actionButton=", buttonStateToString(getActionButton()));
+
+ final int pointerCount = getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ appendUnless(i, msg, ", id[" + i + "]=", getPointerId(i));
+ float x = getX(i);
+ float y = getY(i);
+ if (!DEBUG_CONCISE_TOSTRING || x != 0f || y != 0f) {
+ msg.append(", x[").append(i).append("]=").append(x);
+ msg.append(", y[").append(i).append("]=").append(y);
+ }
+ appendUnless(TOOL_TYPE_SYMBOLIC_NAMES.get(TOOL_TYPE_FINGER),
+ msg, ", toolType[" + i + "]=", toolTypeToString(getToolType(i)));
+ }
+
+ appendUnless("0", msg, ", buttonState=", MotionEvent.buttonStateToString(getButtonState()));
+ appendUnless(classificationToString(CLASSIFICATION_NONE), msg, ", classification=",
+ classificationToString(getClassification()));
+ appendUnless("0", msg, ", metaState=", KeyEvent.metaStateToString(getMetaState()));
+ appendUnless("0", msg, ", flags=0x", Integer.toHexString(getFlags()));
+ appendUnless("0", msg, ", edgeFlags=0x", Integer.toHexString(getEdgeFlags()));
+ appendUnless(1, msg, ", pointerCount=", pointerCount);
+ appendUnless(0, msg, ", historySize=", getHistorySize());
+ msg.append(", eventTime=").append(getEventTime());
+ if (!DEBUG_CONCISE_TOSTRING) {
+ msg.append(", downTime=").append(getDownTime());
+ msg.append(", deviceId=").append(getDeviceId());
+ msg.append(", source=0x").append(Integer.toHexString(getSource()));
+ msg.append(", displayId=").append(getDisplayId());
+ msg.append(", eventId=").append(getId());
+ }
+ msg.append(" }");
+ return msg.toString();
+ }
+
+ private static <T> void appendUnless(T defValue, StringBuilder sb, String key, T value) {
+ if (DEBUG_CONCISE_TOSTRING && Objects.equals(defValue, value)) return;
+ sb.append(key).append(value);
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified unmasked action
+ * such as "ACTION_DOWN", "ACTION_POINTER_DOWN(3)" or an equivalent numeric constant
+ * such as "35" if unknown.
+ *
+ * @param action The unmasked action.
+ * @return The symbolic name of the specified action.
+ * @see #getAction()
+ */
+ public static String actionToString(int action) {
+ switch (action) {
+ case ACTION_DOWN:
+ return "ACTION_DOWN";
+ case ACTION_UP:
+ return "ACTION_UP";
+ case ACTION_CANCEL:
+ return "ACTION_CANCEL";
+ case ACTION_OUTSIDE:
+ return "ACTION_OUTSIDE";
+ case ACTION_MOVE:
+ return "ACTION_MOVE";
+ case ACTION_HOVER_MOVE:
+ return "ACTION_HOVER_MOVE";
+ case ACTION_SCROLL:
+ return "ACTION_SCROLL";
+ case ACTION_HOVER_ENTER:
+ return "ACTION_HOVER_ENTER";
+ case ACTION_HOVER_EXIT:
+ return "ACTION_HOVER_EXIT";
+ case ACTION_BUTTON_PRESS:
+ return "ACTION_BUTTON_PRESS";
+ case ACTION_BUTTON_RELEASE:
+ return "ACTION_BUTTON_RELEASE";
+ }
+ int index = (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
+ switch (action & ACTION_MASK) {
+ case ACTION_POINTER_DOWN:
+ return "ACTION_POINTER_DOWN(" + index + ")";
+ case ACTION_POINTER_UP:
+ return "ACTION_POINTER_UP(" + index + ")";
+ default:
+ return Integer.toString(action);
+ }
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified axis
+ * such as "AXIS_X" or an equivalent numeric constant such as "42" if unknown.
+ *
+ * @param axis The axis.
+ * @return The symbolic name of the specified axis.
+ */
+ public static String axisToString(int axis) {
+ String symbolicName = nativeAxisToString(axis);
+ return symbolicName != null ? LABEL_PREFIX + symbolicName : Integer.toString(axis);
+ }
+
+ /**
+ * Gets an axis by its symbolic name such as "AXIS_X" or an
+ * equivalent numeric constant such as "42".
+ *
+ * @param symbolicName The symbolic name of the axis.
+ * @return The axis or -1 if not found.
+ * @see KeyEvent#keyCodeToString(int)
+ */
+ public static int axisFromString(String symbolicName) {
+ if (symbolicName.startsWith(LABEL_PREFIX)) {
+ symbolicName = symbolicName.substring(LABEL_PREFIX.length());
+ int axis = nativeAxisFromString(symbolicName);
+ if (axis >= 0) {
+ return axis;
+ }
+ }
+ try {
+ return Integer.parseInt(symbolicName, 10);
+ } catch (NumberFormatException ex) {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified combined
+ * button state flags such as "0", "BUTTON_PRIMARY",
+ * "BUTTON_PRIMARY|BUTTON_SECONDARY" or an equivalent numeric constant such as "0x10000000"
+ * if unknown.
+ *
+ * @param buttonState The button state.
+ * @return The symbolic name of the specified combined button state flags.
+ * @hide
+ */
+ public static String buttonStateToString(int buttonState) {
+ if (buttonState == 0) {
+ return "0";
+ }
+ StringBuilder result = null;
+ int i = 0;
+ while (buttonState != 0) {
+ final boolean isSet = (buttonState & 1) != 0;
+ buttonState >>>= 1; // unsigned shift!
+ if (isSet) {
+ final String name = BUTTON_SYMBOLIC_NAMES[i];
+ if (result == null) {
+ if (buttonState == 0) {
+ return name;
+ }
+ result = new StringBuilder(name);
+ } else {
+ result.append('|');
+ result.append(name);
+ }
+ }
+ i += 1;
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified classification.
+ *
+ * @param classification The classification type.
+ * @return The symbolic name of this classification.
+ * @hide
+ */
+ public static String classificationToString(@Classification int classification) {
+ switch (classification) {
+ case CLASSIFICATION_NONE:
+ return "NONE";
+ case CLASSIFICATION_AMBIGUOUS_GESTURE:
+ return "AMBIGUOUS_GESTURE";
+ case CLASSIFICATION_DEEP_PRESS:
+ return "DEEP_PRESS";
+
+ }
+ return "NONE";
+ }
+
+ /**
+ * Returns a string that represents the symbolic name of the specified tool type
+ * such as "TOOL_TYPE_FINGER" or an equivalent numeric constant such as "42" if unknown.
+ *
+ * @param toolType The tool type.
+ * @return The symbolic name of the specified tool type.
+ * @hide
+ */
+ public static String toolTypeToString(int toolType) {
+ String symbolicName = TOOL_TYPE_SYMBOLIC_NAMES.get(toolType);
+ return symbolicName != null ? symbolicName : Integer.toString(toolType);
+ }
+
+ /**
+ * Checks if a mouse or stylus button (or combination of buttons) is pressed.
+ * @param button Button (or combination of buttons).
+ * @return True if specified buttons are pressed.
+ *
+ * @see #BUTTON_PRIMARY
+ * @see #BUTTON_SECONDARY
+ * @see #BUTTON_TERTIARY
+ * @see #BUTTON_FORWARD
+ * @see #BUTTON_BACK
+ * @see #BUTTON_STYLUS_PRIMARY
+ * @see #BUTTON_STYLUS_SECONDARY
+ */
+ public final boolean isButtonPressed(int button) {
+ if (button == 0) {
+ return false;
+ }
+ return (getButtonState() & button) == button;
+ }
+
+ /**
+ * Gets a rotation matrix that (when applied to a motionevent) will rotate that motion event
+ * such that the result coordinates end up in the same physical location on a display whose
+ * coordinates are rotated by `rotation`.
+ *
+ * For example, rotating 0,0 by 90 degrees will move a point from the physical top-left to
+ * the bottom-left of the 90-degree-rotated display.
+ *
+ * @hide
+ */
+ public static Matrix createRotateMatrix(
+ @Surface.Rotation int rotation, int displayW, int displayH) {
+ if (rotation == Surface.ROTATION_0) {
+ return new Matrix(Matrix.IDENTITY_MATRIX);
+ }
+ // values is row-major
+ float[] values = null;
+ if (rotation == Surface.ROTATION_90) {
+ values = new float[]{0, 1, 0,
+ -1, 0, displayH,
+ 0, 0, 1};
+ } else if (rotation == Surface.ROTATION_180) {
+ values = new float[]{-1, 0, displayW,
+ 0, -1, displayH,
+ 0, 0, 1};
+ } else if (rotation == Surface.ROTATION_270) {
+ values = new float[]{0, -1, displayW,
+ 1, 0, 0,
+ 0, 0, 1};
+ }
+ Matrix toOrient = new Matrix();
+ toOrient.setValues(values);
+ return toOrient;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<MotionEvent> CREATOR
+ = new Parcelable.Creator<MotionEvent>() {
+ public MotionEvent createFromParcel(Parcel in) {
+ in.readInt(); // skip token, we already know this is a MotionEvent
+ return MotionEvent.createFromParcelBody(in);
+ }
+
+ public MotionEvent[] newArray(int size) {
+ return new MotionEvent[size];
+ }
+ };
+
+ /** @hide */
+ public static MotionEvent createFromParcelBody(Parcel in) {
+ MotionEvent ev = obtain();
+ ev.mNativePtr = nativeReadFromParcel(ev.mNativePtr, in);
+ return ev;
+ }
+
+ /** @hide */
+ @Override
+ public final void cancel() {
+ setAction(ACTION_CANCEL);
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(PARCEL_TOKEN_MOTION_EVENT);
+ nativeWriteToParcel(mNativePtr, out);
+ }
+
+ /**
+ * Transfer object for pointer coordinates.
+ *
+ * Objects of this type can be used to specify the pointer coordinates when
+ * creating new {@link MotionEvent} objects and to query pointer coordinates
+ * in bulk.
+ *
+ * Refer to {@link InputDevice} for information about how different kinds of
+ * input devices and sources represent pointer coordinates.
+ */
+ public static final class PointerCoords {
+ private static final int INITIAL_PACKED_AXIS_VALUES = 8;
+ @UnsupportedAppUsage
+ private long mPackedAxisBits;
+ @UnsupportedAppUsage
+ private float[] mPackedAxisValues;
+
+ /**
+ * Creates a pointer coords object with all axes initialized to zero.
+ */
+ public PointerCoords() {
+ }
+
+ /**
+ * Creates a pointer coords object as a copy of the
+ * contents of another pointer coords object.
+ *
+ * @param other The pointer coords object to copy.
+ */
+ public PointerCoords(PointerCoords other) {
+ copyFrom(other);
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public static PointerCoords[] createArray(int size) {
+ PointerCoords[] array = new PointerCoords[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = new PointerCoords();
+ }
+ return array;
+ }
+
+ /**
+ * The X component of the pointer movement.
+ *
+ * @see MotionEvent#AXIS_X
+ */
+ public float x;
+
+ /**
+ * The Y component of the pointer movement.
+ *
+ * @see MotionEvent#AXIS_Y
+ */
+ public float y;
+
+ /**
+ * A normalized value that describes the pressure applied to the device
+ * by a finger or other tool.
+ * The pressure generally ranges from 0 (no pressure at all) to 1 (normal pressure),
+ * although values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ *
+ * @see MotionEvent#AXIS_PRESSURE
+ */
+ public float pressure;
+
+ /**
+ * A normalized value that describes the approximate size of the pointer touch area
+ * in relation to the maximum detectable size of the device.
+ * It represents some approximation of the area of the screen being
+ * pressed; the actual value in pixels corresponding to the
+ * touch is normalized with the device specific range of values
+ * and scaled to a value between 0 and 1. The value of size can be used to
+ * determine fat touch events.
+ *
+ * @see MotionEvent#AXIS_SIZE
+ */
+ public float size;
+
+ /**
+ * The length of the major axis of an ellipse that describes the touch area at
+ * the point of contact.
+ * If the device is a touch screen, the length is reported in pixels, otherwise it is
+ * reported in device-specific units.
+ *
+ * @see MotionEvent#AXIS_TOUCH_MAJOR
+ */
+ public float touchMajor;
+
+ /**
+ * The length of the minor axis of an ellipse that describes the touch area at
+ * the point of contact.
+ * If the device is a touch screen, the length is reported in pixels, otherwise it is
+ * reported in device-specific units.
+ *
+ * @see MotionEvent#AXIS_TOUCH_MINOR
+ */
+ public float touchMinor;
+
+ /**
+ * The length of the major axis of an ellipse that describes the size of
+ * the approaching tool.
+ * The tool area represents the estimated size of the finger or pen that is
+ * touching the device independent of its actual touch area at the point of contact.
+ * If the device is a touch screen, the length is reported in pixels, otherwise it is
+ * reported in device-specific units.
+ *
+ * @see MotionEvent#AXIS_TOOL_MAJOR
+ */
+ public float toolMajor;
+
+ /**
+ * The length of the minor axis of an ellipse that describes the size of
+ * the approaching tool.
+ * The tool area represents the estimated size of the finger or pen that is
+ * touching the device independent of its actual touch area at the point of contact.
+ * If the device is a touch screen, the length is reported in pixels, otherwise it is
+ * reported in device-specific units.
+ *
+ * @see MotionEvent#AXIS_TOOL_MINOR
+ */
+ public float toolMinor;
+
+ /**
+ * The orientation of the touch area and tool area in radians clockwise from vertical.
+ * An angle of 0 radians indicates that the major axis of contact is oriented
+ * upwards, is perfectly circular or is of unknown orientation. A positive angle
+ * indicates that the major axis of contact is oriented to the right. A negative angle
+ * indicates that the major axis of contact is oriented to the left.
+ * The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians
+ * (finger pointing fully right).
+ *
+ * @see MotionEvent#AXIS_ORIENTATION
+ */
+ public float orientation;
+
+ /**
+ * Clears the contents of this object.
+ * Resets all axes to zero.
+ */
+ public void clear() {
+ mPackedAxisBits = 0;
+
+ x = 0;
+ y = 0;
+ pressure = 0;
+ size = 0;
+ touchMajor = 0;
+ touchMinor = 0;
+ toolMajor = 0;
+ toolMinor = 0;
+ orientation = 0;
+ }
+
+ /**
+ * Copies the contents of another pointer coords object.
+ *
+ * @param other The pointer coords object to copy.
+ */
+ public void copyFrom(PointerCoords other) {
+ final long bits = other.mPackedAxisBits;
+ mPackedAxisBits = bits;
+ if (bits != 0) {
+ final float[] otherValues = other.mPackedAxisValues;
+ final int count = Long.bitCount(bits);
+ float[] values = mPackedAxisValues;
+ if (values == null || count > values.length) {
+ values = new float[otherValues.length];
+ mPackedAxisValues = values;
+ }
+ System.arraycopy(otherValues, 0, values, 0, count);
+ }
+
+ x = other.x;
+ y = other.y;
+ pressure = other.pressure;
+ size = other.size;
+ touchMajor = other.touchMajor;
+ touchMinor = other.touchMinor;
+ toolMajor = other.toolMajor;
+ toolMinor = other.toolMinor;
+ orientation = other.orientation;
+ }
+
+ /**
+ * Gets the value associated with the specified axis.
+ *
+ * @param axis The axis identifier for the axis value to retrieve.
+ * @return The value associated with the axis, or 0 if none.
+ *
+ * @see MotionEvent#AXIS_X
+ * @see MotionEvent#AXIS_Y
+ */
+ public float getAxisValue(int axis) {
+ switch (axis) {
+ case AXIS_X:
+ return x;
+ case AXIS_Y:
+ return y;
+ case AXIS_PRESSURE:
+ return pressure;
+ case AXIS_SIZE:
+ return size;
+ case AXIS_TOUCH_MAJOR:
+ return touchMajor;
+ case AXIS_TOUCH_MINOR:
+ return touchMinor;
+ case AXIS_TOOL_MAJOR:
+ return toolMajor;
+ case AXIS_TOOL_MINOR:
+ return toolMinor;
+ case AXIS_ORIENTATION:
+ return orientation;
+ default: {
+ if (axis < 0 || axis > 63) {
+ throw new IllegalArgumentException("Axis out of range.");
+ }
+ final long bits = mPackedAxisBits;
+ final long axisBit = 0x8000000000000000L >>> axis;
+ if ((bits & axisBit) == 0) {
+ return 0;
+ }
+ final int index = Long.bitCount(bits & ~(0xFFFFFFFFFFFFFFFFL >>> axis));
+ return mPackedAxisValues[index];
+ }
+ }
+ }
+
+ /**
+ * Sets the value associated with the specified axis.
+ *
+ * @param axis The axis identifier for the axis value to assign.
+ * @param value The value to set.
+ *
+ * @see MotionEvent#AXIS_X
+ * @see MotionEvent#AXIS_Y
+ */
+ public void setAxisValue(int axis, float value) {
+ switch (axis) {
+ case AXIS_X:
+ x = value;
+ break;
+ case AXIS_Y:
+ y = value;
+ break;
+ case AXIS_PRESSURE:
+ pressure = value;
+ break;
+ case AXIS_SIZE:
+ size = value;
+ break;
+ case AXIS_TOUCH_MAJOR:
+ touchMajor = value;
+ break;
+ case AXIS_TOUCH_MINOR:
+ touchMinor = value;
+ break;
+ case AXIS_TOOL_MAJOR:
+ toolMajor = value;
+ break;
+ case AXIS_TOOL_MINOR:
+ toolMinor = value;
+ break;
+ case AXIS_ORIENTATION:
+ orientation = value;
+ break;
+ default: {
+ if (axis < 0 || axis > 63) {
+ throw new IllegalArgumentException("Axis out of range.");
+ }
+ final long bits = mPackedAxisBits;
+ final long axisBit = 0x8000000000000000L >>> axis;
+ final int index = Long.bitCount(bits & ~(0xFFFFFFFFFFFFFFFFL >>> axis));
+ float[] values = mPackedAxisValues;
+ if ((bits & axisBit) == 0) {
+ if (values == null) {
+ values = new float[INITIAL_PACKED_AXIS_VALUES];
+ mPackedAxisValues = values;
+ } else {
+ final int count = Long.bitCount(bits);
+ if (count < values.length) {
+ if (index != count) {
+ System.arraycopy(values, index, values, index + 1,
+ count - index);
+ }
+ } else {
+ float[] newValues = new float[count * 2];
+ System.arraycopy(values, 0, newValues, 0, index);
+ System.arraycopy(values, index, newValues, index + 1,
+ count - index);
+ values = newValues;
+ mPackedAxisValues = values;
+ }
+ }
+ mPackedAxisBits = bits | axisBit;
+ }
+ values[index] = value;
+ }
+ }
+ }
+ }
+
+ /**
+ * Transfer object for pointer properties.
+ *
+ * Objects of this type can be used to specify the pointer id and tool type
+ * when creating new {@link MotionEvent} objects and to query pointer properties in bulk.
+ */
+ public static final class PointerProperties {
+ /**
+ * Creates a pointer properties object with an invalid pointer id.
+ */
+ public PointerProperties() {
+ clear();
+ }
+
+ /**
+ * Creates a pointer properties object as a copy of the contents of
+ * another pointer properties object.
+ * @param other
+ */
+ public PointerProperties(PointerProperties other) {
+ copyFrom(other);
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public static PointerProperties[] createArray(int size) {
+ PointerProperties[] array = new PointerProperties[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = new PointerProperties();
+ }
+ return array;
+ }
+
+ /**
+ * The pointer id.
+ * Initially set to {@link #INVALID_POINTER_ID} (-1).
+ *
+ * @see MotionEvent#getPointerId(int)
+ */
+ public int id;
+
+ /**
+ * The pointer tool type.
+ * Initially set to 0.
+ *
+ * @see MotionEvent#getToolType(int)
+ */
+ public int toolType;
+
+ /**
+ * Resets the pointer properties to their initial values.
+ */
+ public void clear() {
+ id = INVALID_POINTER_ID;
+ toolType = TOOL_TYPE_UNKNOWN;
+ }
+
+ /**
+ * Copies the contents of another pointer properties object.
+ *
+ * @param other The pointer properties object to copy.
+ */
+ public void copyFrom(PointerProperties other) {
+ id = other.id;
+ toolType = other.toolType;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (other instanceof PointerProperties) {
+ return equals((PointerProperties)other);
+ }
+ return false;
+ }
+
+ private boolean equals(PointerProperties other) {
+ return other != null && id == other.id && toolType == other.toolType;
+ }
+
+ @Override
+ public int hashCode() {
+ return id | (toolType << 8);
+ }
+ }
+}
diff --git a/android/view/NativeVectorDrawableAnimator.java b/android/view/NativeVectorDrawableAnimator.java
new file mode 100644
index 0000000..b0556a3
--- /dev/null
+++ b/android/view/NativeVectorDrawableAnimator.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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;
+
+/**
+ * Exists just to allow for android.graphics & android.view package separation
+ *
+ * TODO: Get off of this coupling more cleanly somehow
+ *
+ * @hide
+ */
+public interface NativeVectorDrawableAnimator {
+ /** @hide */
+ long getAnimatorNativePtr();
+}
diff --git a/android/view/NotificationHeaderView.java b/android/view/NotificationHeaderView.java
new file mode 100644
index 0000000..42b798c
--- /dev/null
+++ b/android/view/NotificationHeaderView.java
@@ -0,0 +1,351 @@
+/*
+ * 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.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.widget.CachingIconView;
+import com.android.internal.widget.NotificationExpandButton;
+
+import java.util.ArrayList;
+
+/**
+ * A header of a notification view
+ *
+ * @hide
+ */
[email protected]
+public class NotificationHeaderView extends RelativeLayout {
+ private final int mTouchableHeight;
+ private OnClickListener mExpandClickListener;
+ private HeaderTouchListener mTouchListener = new HeaderTouchListener();
+ private NotificationTopLineView mTopLineView;
+ private NotificationExpandButton mExpandButton;
+ private View mAltExpandTarget;
+ private CachingIconView mIcon;
+ private Drawable mBackground;
+ private boolean mEntireHeaderClickable;
+ private boolean mExpandOnlyOnButton;
+ private boolean mAcceptAllTouches;
+
+ ViewOutlineProvider mProvider = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (mBackground != null) {
+ outline.setRect(0, 0, getWidth(), getHeight());
+ outline.setAlpha(1f);
+ }
+ }
+ };
+
+ public NotificationHeaderView(Context context) {
+ this(context, null);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ Resources res = getResources();
+ mTouchableHeight = res.getDimensionPixelSize(R.dimen.notification_header_touchable_height);
+ mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mIcon = findViewById(R.id.icon);
+ mTopLineView = findViewById(R.id.notification_top_line);
+ mExpandButton = findViewById(R.id.expand_button);
+ mAltExpandTarget = findViewById(R.id.alternate_expand_target);
+ setClipToPadding(false);
+ }
+
+ /**
+ * Set a {@link Drawable} to be displayed as a background on the header.
+ */
+ public void setHeaderBackgroundDrawable(Drawable drawable) {
+ if (drawable != null) {
+ setWillNotDraw(false);
+ mBackground = drawable;
+ mBackground.setCallback(this);
+ setOutlineProvider(mProvider);
+ } else {
+ setWillNotDraw(true);
+ mBackground = null;
+ setOutlineProvider(null);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mBackground != null) {
+ mBackground.setBounds(0, 0, getWidth(), getHeight());
+ mBackground.draw(canvas);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || who == mBackground;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ if (mBackground != null && mBackground.isStateful()) {
+ mBackground.setState(getDrawableState());
+ }
+ }
+
+ private void updateTouchListener() {
+ if (mExpandClickListener == null) {
+ setOnTouchListener(null);
+ return;
+ }
+ setOnTouchListener(mTouchListener);
+ mTouchListener.bindTouchRects();
+ }
+
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ mExpandClickListener = l;
+ mExpandButton.setOnClickListener(mExpandClickListener);
+ mAltExpandTarget.setOnClickListener(mExpandClickListener);
+ updateTouchListener();
+ }
+
+ /**
+ * Sets the extra margin at the end of the top line of left-aligned text + icons.
+ * This value will have the margin required to accommodate the expand button added to it.
+ *
+ * @param extraMarginEnd extra margin in px
+ */
+ public void setTopLineExtraMarginEnd(int extraMarginEnd) {
+ mTopLineView.setHeaderTextMarginEnd(extraMarginEnd);
+ }
+
+ /**
+ * Sets the extra margin at the end of the top line of left-aligned text + icons.
+ * This value will have the margin required to accommodate the expand button added to it.
+ *
+ * @param extraMarginEndDp extra margin in dp
+ */
+ @RemotableViewMethod
+ public void setTopLineExtraMarginEndDp(float extraMarginEndDp) {
+ setTopLineExtraMarginEnd(
+ (int) (extraMarginEndDp * getResources().getDisplayMetrics().density));
+ }
+
+ /**
+ * This is used to make the low-priority header show the bolded text of a title.
+ *
+ * @param styleTextAsTitle true if this header's text is to have the style of a title
+ */
+ @RemotableViewMethod
+ public void styleTextAsTitle(boolean styleTextAsTitle) {
+ int styleResId = styleTextAsTitle
+ ? R.style.TextAppearance_DeviceDefault_Notification_Title
+ : R.style.TextAppearance_DeviceDefault_Notification_Info;
+ // Most of the time, we're showing text in the minimized state
+ View headerText = findViewById(R.id.header_text);
+ if (headerText instanceof TextView) {
+ ((TextView) headerText).setTextAppearance(styleResId);
+ }
+ // If there's no summary or text, we show the app name instead of nothing
+ View appNameText = findViewById(R.id.app_name_text);
+ if (appNameText instanceof TextView) {
+ ((TextView) appNameText).setTextAppearance(styleResId);
+ }
+ }
+
+ /**
+ * Handles clicks on the header based on the region tapped.
+ */
+ public class HeaderTouchListener implements OnTouchListener {
+
+ private final ArrayList<Rect> mTouchRects = new ArrayList<>();
+ private Rect mExpandButtonRect;
+ private Rect mAltExpandTargetRect;
+ private int mTouchSlop;
+ private boolean mTrackGesture;
+ private float mDownX;
+ private float mDownY;
+
+ public HeaderTouchListener() {
+ }
+
+ public void bindTouchRects() {
+ mTouchRects.clear();
+ addRectAroundView(mIcon);
+ mExpandButtonRect = addRectAroundView(mExpandButton);
+ mAltExpandTargetRect = addRectAroundView(mAltExpandTarget);
+ addWidthRect();
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ }
+
+ private void addWidthRect() {
+ Rect r = new Rect();
+ r.top = 0;
+ r.bottom = mTouchableHeight;
+ r.left = 0;
+ r.right = getWidth();
+ mTouchRects.add(r);
+ }
+
+ private Rect addRectAroundView(View view) {
+ final Rect r = getRectAroundView(view);
+ mTouchRects.add(r);
+ return r;
+ }
+
+ private Rect getRectAroundView(View view) {
+ float size = 48 * getResources().getDisplayMetrics().density;
+ float width = Math.max(size, view.getWidth());
+ float height = Math.max(size, view.getHeight());
+ final Rect r = new Rect();
+ if (view.getVisibility() == GONE) {
+ view = getFirstChildNotGone();
+ r.left = (int) (view.getLeft() - width / 2.0f);
+ } else {
+ r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
+ }
+ r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
+ r.bottom = (int) (r.top + height);
+ r.right = (int) (r.left + width);
+ return r;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ mTrackGesture = false;
+ if (isInside(x, y)) {
+ mDownX = x;
+ mDownY = y;
+ mTrackGesture = true;
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mTrackGesture) {
+ if (Math.abs(mDownX - x) > mTouchSlop
+ || Math.abs(mDownY - y) > mTouchSlop) {
+ mTrackGesture = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mTrackGesture) {
+ float topLineX = mTopLineView.getX();
+ float topLineY = mTopLineView.getY();
+ if (mTopLineView.onTouchUp(x - topLineX, y - topLineY,
+ mDownX - topLineX, mDownY - topLineY)) {
+ break;
+ }
+ mExpandButton.performClick();
+ }
+ break;
+ }
+ return mTrackGesture;
+ }
+
+ private boolean isInside(float x, float y) {
+ if (mAcceptAllTouches) {
+ return true;
+ }
+ if (mExpandOnlyOnButton) {
+ return mExpandButtonRect.contains((int) x, (int) y)
+ || mAltExpandTargetRect.contains((int) x, (int) y);
+ }
+ for (int i = 0; i < mTouchRects.size(); i++) {
+ Rect r = mTouchRects.get(i);
+ if (r.contains((int) x, (int) y)) {
+ return true;
+ }
+ }
+ float topLineX = x - mTopLineView.getX();
+ float topLineY = y - mTopLineView.getY();
+ return mTopLineView.isInTouchRect(topLineX, topLineY);
+ }
+ }
+
+ private View getFirstChildNotGone() {
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ return child;
+ }
+ }
+ return this;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public boolean isInTouchRect(float x, float y) {
+ if (mExpandClickListener == null) {
+ return false;
+ }
+ return mTouchListener.isInside(x, y);
+ }
+
+ /**
+ * Sets whether or not all touches to this header view will register as a click. Note that
+ * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true},
+ * then calling this method with {@code false} will not override that configuration.
+ */
+ @RemotableViewMethod
+ public void setAcceptAllTouches(boolean acceptAllTouches) {
+ mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches;
+ }
+
+ /**
+ * Sets whether only the expand icon itself should serve as the expand target.
+ */
+ @RemotableViewMethod
+ public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
+ mExpandOnlyOnButton = expandOnlyOnButton;
+ }
+}
diff --git a/android/view/NotificationTopLineView.java b/android/view/NotificationTopLineView.java
new file mode 100644
index 0000000..bd20f5b
--- /dev/null
+++ b/android/view/NotificationTopLineView.java
@@ -0,0 +1,478 @@
+/*
+ * 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.view;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The top line of content in a notification view.
+ * This includes the text views and badges but excludes the icon and the expander.
+ *
+ * @hide
+ */
[email protected]
+public class NotificationTopLineView extends ViewGroup {
+ private final OverflowAdjuster mOverflowAdjuster = new OverflowAdjuster();
+ private final int mGravityY;
+ private final int mChildMinWidth;
+ private final int mChildHideWidth;
+ @Nullable private View mAppName;
+ @Nullable private View mTitle;
+ private View mHeaderText;
+ private View mHeaderTextDivider;
+ private View mSecondaryHeaderText;
+ private View mSecondaryHeaderTextDivider;
+ private OnClickListener mFeedbackListener;
+ private HeaderTouchListener mTouchListener = new HeaderTouchListener();
+ private View mFeedbackIcon;
+ private int mHeaderTextMarginEnd;
+
+ private Set<View> mViewsToDisappear = new HashSet<>();
+
+ private int mMaxAscent;
+ private int mMaxDescent;
+
+ public NotificationTopLineView(Context context) {
+ this(context, null);
+ }
+
+ public NotificationTopLineView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NotificationTopLineView(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationTopLineView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ Resources res = getResources();
+ mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
+ mChildHideWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_hide_width);
+
+ // NOTE: Implementation only supports TOP, BOTTOM, and CENTER_VERTICAL gravities,
+ // with CENTER_VERTICAL being the default.
+ int[] attrIds = {android.R.attr.gravity};
+ TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
+ int gravity = ta.getInt(0, 0);
+ ta.recycle();
+ if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
+ mGravityY = Gravity.BOTTOM;
+ } else if ((gravity & Gravity.TOP) == Gravity.TOP) {
+ mGravityY = Gravity.TOP;
+ } else {
+ mGravityY = Gravity.CENTER_VERTICAL;
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mAppName = findViewById(R.id.app_name_text);
+ mTitle = findViewById(R.id.title);
+ mHeaderText = findViewById(R.id.header_text);
+ mHeaderTextDivider = findViewById(R.id.header_text_divider);
+ mSecondaryHeaderText = findViewById(R.id.header_text_secondary);
+ mSecondaryHeaderTextDivider = findViewById(R.id.header_text_secondary_divider);
+ mFeedbackIcon = findViewById(R.id.feedback);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
+ final boolean wrapHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST;
+ int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth, MeasureSpec.AT_MOST);
+ int heightSpec = MeasureSpec.makeMeasureSpec(givenHeight, MeasureSpec.AT_MOST);
+ int totalWidth = getPaddingStart();
+ int maxChildHeight = -1;
+ mMaxAscent = -1;
+ mMaxDescent = -1;
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ // We'll give it the rest of the space in the end
+ continue;
+ }
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec,
+ lp.leftMargin + lp.rightMargin, lp.width);
+ int childHeightSpec = getChildMeasureSpec(heightSpec,
+ lp.topMargin + lp.bottomMargin, lp.height);
+ child.measure(childWidthSpec, childHeightSpec);
+ totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
+ int childBaseline = child.getBaseline();
+ int childHeight = child.getMeasuredHeight();
+ if (childBaseline != -1) {
+ mMaxAscent = Math.max(mMaxAscent, childBaseline);
+ mMaxDescent = Math.max(mMaxDescent, childHeight - childBaseline);
+ }
+ maxChildHeight = Math.max(maxChildHeight, childHeight);
+ }
+
+ mViewsToDisappear.clear();
+ // Ensure that there is at least enough space for the icons
+ int endMargin = Math.max(mHeaderTextMarginEnd, getPaddingEnd());
+ if (totalWidth > givenWidth - endMargin) {
+ int overFlow = totalWidth - givenWidth + endMargin;
+
+ mOverflowAdjuster.resetForOverflow(overFlow, heightSpec)
+ // First shrink the app name, down to a minimum size
+ .adjust(mAppName, null, mChildMinWidth)
+ // Next, shrink the header text (this usually has subText)
+ // This shrinks the subtext first, but not all the way (yet!)
+ .adjust(mHeaderText, mHeaderTextDivider, mChildMinWidth)
+ // Next, shrink the secondary header text (this rarely has conversationTitle)
+ .adjust(mSecondaryHeaderText, mSecondaryHeaderTextDivider, 0)
+ // Next, shrink the title text (this has contentTitle; only in headerless views)
+ .adjust(mTitle, null, mChildMinWidth)
+ // Next, shrink the header down to 0 if still necessary.
+ .adjust(mHeaderText, mHeaderTextDivider, 0)
+ // Finally, shrink the title to 0 if necessary (media is super cramped)
+ .adjust(mTitle, null, 0)
+ // Clean up
+ .finish();
+ }
+ setMeasuredDimension(givenWidth, wrapHeight ? maxChildHeight : givenHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ final int width = getWidth();
+ int start = getPaddingStart();
+ int childCount = getChildCount();
+ int ownHeight = b - t;
+ int childSpace = ownHeight - mPaddingTop - mPaddingBottom;
+
+ // Instead of centering the baseline, pick a baseline that centers views which align to it.
+ // Only used when mGravityY is CENTER_VERTICAL
+ int baselineY = mPaddingTop + ((childSpace - (mMaxAscent + mMaxDescent)) / 2) + mMaxAscent;
+
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ int childHeight = child.getMeasuredHeight();
+ MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
+
+ // Calculate vertical alignment of the views, accounting for the view baselines
+ int childTop;
+ int childBaseline = child.getBaseline();
+ switch (mGravityY) {
+ case Gravity.TOP:
+ childTop = mPaddingTop + params.topMargin;
+ if (childBaseline != -1) {
+ childTop += mMaxAscent - childBaseline;
+ }
+ break;
+ case Gravity.CENTER_VERTICAL:
+ if (childBaseline != -1) {
+ // Align baselines vertically only if the child is smaller than us
+ if (childSpace - childHeight > 0) {
+ childTop = baselineY - childBaseline;
+ } else {
+ childTop = mPaddingTop + (childSpace - childHeight) / 2;
+ }
+ } else {
+ childTop = mPaddingTop + ((childSpace - childHeight) / 2)
+ + params.topMargin - params.bottomMargin;
+ }
+ break;
+ case Gravity.BOTTOM:
+ int childBottom = ownHeight - mPaddingBottom;
+ childTop = childBottom - childHeight - params.bottomMargin;
+ if (childBaseline != -1) {
+ int descent = childHeight - childBaseline;
+ childTop -= (mMaxDescent - descent);
+ }
+ break;
+ default:
+ childTop = mPaddingTop;
+ }
+ if (mViewsToDisappear.contains(child)) {
+ child.layout(start, childTop, start, childTop + childHeight);
+ } else {
+ start += params.getMarginStart();
+ int end = start + child.getMeasuredWidth();
+ int layoutLeft = isRtl ? width - end : start;
+ int layoutRight = isRtl ? width - start : end;
+ start = end + params.getMarginEnd();
+ child.layout(layoutLeft, childTop, layoutRight, childTop + childHeight);
+ }
+ }
+ updateTouchListener();
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ private void updateTouchListener() {
+ if (mFeedbackListener == null) {
+ setOnTouchListener(null);
+ return;
+ }
+ setOnTouchListener(mTouchListener);
+ mTouchListener.bindTouchRects();
+ }
+
+ /**
+ * Sets onclick listener for feedback icon.
+ */
+ public void setFeedbackOnClickListener(OnClickListener l) {
+ mFeedbackListener = l;
+ mFeedbackIcon.setOnClickListener(mFeedbackListener);
+ updateTouchListener();
+ }
+
+ /**
+ * Sets the margin end for the text portion of the header, excluding right-aligned elements
+ *
+ * @param headerTextMarginEnd margin size
+ */
+ public void setHeaderTextMarginEnd(int headerTextMarginEnd) {
+ if (mHeaderTextMarginEnd != headerTextMarginEnd) {
+ mHeaderTextMarginEnd = headerTextMarginEnd;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Get the current margin end value for the header text
+ *
+ * @return margin size
+ */
+ public int getHeaderTextMarginEnd() {
+ return mHeaderTextMarginEnd;
+ }
+
+ /**
+ * Set padding at the start of the view.
+ */
+ public void setPaddingStart(int paddingStart) {
+ setPaddingRelative(paddingStart, getPaddingTop(), getPaddingEnd(), getPaddingBottom());
+ }
+
+ private class HeaderTouchListener implements OnTouchListener {
+
+ private Rect mFeedbackRect;
+ private int mTouchSlop;
+ private boolean mTrackGesture;
+ private float mDownX;
+ private float mDownY;
+
+ HeaderTouchListener() {
+ }
+
+ public void bindTouchRects() {
+ mFeedbackRect = getRectAroundView(mFeedbackIcon);
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ }
+
+ private Rect getRectAroundView(View view) {
+ float size = 48 * getResources().getDisplayMetrics().density;
+ float width = Math.max(size, view.getWidth());
+ float height = Math.max(size, view.getHeight());
+ final Rect r = new Rect();
+ if (view.getVisibility() == GONE) {
+ view = getFirstChildNotGone();
+ r.left = (int) (view.getLeft() - width / 2.0f);
+ } else {
+ r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
+ }
+ r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
+ r.bottom = (int) (r.top + height);
+ r.right = (int) (r.left + width);
+ return r;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ mTrackGesture = false;
+ if (isInside(x, y)) {
+ mDownX = x;
+ mDownY = y;
+ mTrackGesture = true;
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mTrackGesture) {
+ if (Math.abs(mDownX - x) > mTouchSlop
+ || Math.abs(mDownY - y) > mTouchSlop) {
+ mTrackGesture = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mTrackGesture && onTouchUp(x, y, mDownX, mDownY)) {
+ return true;
+ }
+ break;
+ }
+ return mTrackGesture;
+ }
+
+ private boolean onTouchUp(float upX, float upY, float downX, float downY) {
+ if (mFeedbackIcon.isVisibleToUser()
+ && (mFeedbackRect.contains((int) upX, (int) upY)
+ || mFeedbackRect.contains((int) downX, (int) downY))) {
+ mFeedbackIcon.performClick();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isInside(float x, float y) {
+ return mFeedbackRect.contains((int) x, (int) y);
+ }
+ }
+
+ private View getFirstChildNotGone() {
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ return child;
+ }
+ }
+ return this;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ /**
+ * Determine if the given point is touching an active part of the top line.
+ */
+ public boolean isInTouchRect(float x, float y) {
+ if (mFeedbackListener == null) {
+ return false;
+ }
+ return mTouchListener.isInside(x, y);
+ }
+
+ /**
+ * Perform a click on an active part of the top line, if touching.
+ */
+ public boolean onTouchUp(float upX, float upY, float downX, float downY) {
+ if (mFeedbackListener == null) {
+ return false;
+ }
+ return mTouchListener.onTouchUp(upX, upY, downX, downY);
+ }
+
+ private final class OverflowAdjuster {
+ private int mOverflow;
+ private int mHeightSpec;
+ private View mRegrowView;
+
+ OverflowAdjuster resetForOverflow(int overflow, int heightSpec) {
+ mOverflow = overflow;
+ mHeightSpec = heightSpec;
+ mRegrowView = null;
+ return this;
+ }
+
+ /**
+ * Shrink the targetView's width by up to overFlow, down to minimumWidth.
+ * @param targetView the view to shrink the width of
+ * @param targetDivider a divider view which should be set to 0 width if the targetView is
+ * @param minimumWidth the minimum width allowed for the targetView
+ * @return this object
+ */
+ OverflowAdjuster adjust(View targetView, View targetDivider, int minimumWidth) {
+ if (mOverflow <= 0 || targetView == null || targetView.getVisibility() == View.GONE) {
+ return this;
+ }
+ final int oldWidth = targetView.getMeasuredWidth();
+ if (oldWidth <= minimumWidth) {
+ return this;
+ }
+ // we're too big
+ int newSize = Math.max(minimumWidth, oldWidth - mOverflow);
+ if (minimumWidth == 0 && newSize < mChildHideWidth
+ && mRegrowView != null && mRegrowView != targetView) {
+ // View is so small it's better to hide it entirely (and its divider and margins)
+ // so we can give that space back to another previously shrunken view.
+ newSize = 0;
+ }
+
+ int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
+ targetView.measure(childWidthSpec, mHeightSpec);
+ mOverflow -= oldWidth - newSize;
+
+ if (newSize == 0) {
+ mViewsToDisappear.add(targetView);
+ mOverflow -= getHorizontalMargins(targetView);
+ if (targetDivider != null && targetDivider.getVisibility() != GONE) {
+ mViewsToDisappear.add(targetDivider);
+ int oldDividerWidth = targetDivider.getMeasuredWidth();
+ int dividerWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST);
+ targetDivider.measure(dividerWidthSpec, mHeightSpec);
+ mOverflow -= (oldDividerWidth + getHorizontalMargins(targetDivider));
+ }
+ }
+ if (mOverflow < 0 && mRegrowView != null) {
+ // We're now under-flowing, so regrow the last view.
+ final int regrowCurrentSize = mRegrowView.getMeasuredWidth();
+ final int maxSize = regrowCurrentSize - mOverflow;
+ int regrowWidthSpec = MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST);
+ mRegrowView.measure(regrowWidthSpec, mHeightSpec);
+ finish();
+ return this;
+ }
+
+ if (newSize != 0) {
+ // if we shrunk this view (but did not completely hide it) store it for potential
+ // re-growth if we proactively shorten a future view.
+ mRegrowView = targetView;
+ }
+ return this;
+ }
+
+ void finish() {
+ resetForOverflow(0, 0);
+ }
+
+ private int getHorizontalMargins(View view) {
+ MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
+ return params.getMarginStart() + params.getMarginEnd();
+ }
+ }
+}
diff --git a/android/view/OnReceiveContentListener.java b/android/view/OnReceiveContentListener.java
new file mode 100644
index 0000000..f2a763a
--- /dev/null
+++ b/android/view/OnReceiveContentListener.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Listener for apps to implement handling for insertion of content. Content may be both text and
+ * non-text (plain/styled text, HTML, images, videos, audio files, etc).
+ *
+ * <p>This listener can be attached to different types of UI components using
+ * {@link View#setOnReceiveContentListener}.
+ *
+ * <p>Here is a sample implementation that handles content URIs and delegates the processing for
+ * text and everything else to the platform:<br>
+ * <pre class="prettyprint">
+ * // (1) Define the listener
+ * public class MyReceiver implements OnReceiveContentListener {
+ * public static final String[] MIME_TYPES = new String[] {"image/*", "video/*"};
+ *
+ * @Override
+ * public ContentInfo onReceiveContent(View view, ContentInfo payload) {
+ * Pair<ContentInfo, ContentInfo> split =
+ * ContentInfoCompat.partition(payload, item -> item.getUri() != null);
+ * ContentInfo uriContent = split.first;
+ * ContentInfo remaining = split.second;
+ * if (uriContent != null) {
+ * ClipData clip = uriContent.getClip();
+ * for (int i = 0; i < clip.getItemCount(); i++) {
+ * Uri uri = clip.getItemAt(i).getUri();
+ * // ... app-specific logic to handle the URI ...
+ * }
+ * }
+ * // Return anything that we didn't handle ourselves. This preserves the default platform
+ * // behavior for text and anything else for which we are not implementing custom handling.
+ * return remaining;
+ * }
+ * }
+ *
+ * // (2) Register the listener
+ * public class MyActivity extends Activity {
+ * @Override
+ * public void onCreate(Bundle savedInstanceState) {
+ * // ...
+ *
+ * EditText myInput = findViewById(R.id.my_input);
+ * myInput.setOnReceiveContentListener(MyReceiver.MIME_TYPES, new MyReceiver());
+ * }
+ * </pre>
+ */
+public interface OnReceiveContentListener {
+ /**
+ * Receive the given content.
+ *
+ * <p>Implementations should handle any content items of interest and return all unhandled
+ * items to preserve the default platform behavior for content that does not have app-specific
+ * handling. For example, an implementation may provide handling for content URIs (to provide
+ * support for inserting images, etc) and delegate the processing of text to the platform to
+ * preserve the common behavior for inserting text. See the class javadoc for a sample
+ * implementation.
+ *
+ * <h3>Handling different content</h3>
+ * <ul>
+ * <li>Text. If the {@link ContentInfo#getSource() source} is
+ * {@link ContentInfo#SOURCE_AUTOFILL autofill}, the view's content should be fully
+ * replaced by the passed-in text. For sources other than autofill, the passed-in text
+ * should overwrite the current selection or be inserted at the current cursor position
+ * if there is no selection.
+ * <li>Non-text content (e.g. images). The content may be inserted inline if the widget
+ * supports this, or it may be added as an attachment (could potentially be shown in a
+ * completely separate view).
+ * </ul>
+ *
+ * <h3>URI permissions</h3>
+ * <p>{@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION Read permissions} are
+ * granted automatically by the platform for any
+ * {@link android.content.ContentResolver#SCHEME_CONTENT content URIs} in the payload passed
+ * to this listener. Permissions are transient and will be released automatically by the
+ * platform.
+ * <p>Processing of content should normally be done in a service or activity.
+ * For long-running processing, using {@code androidx.work.WorkManager} is recommended.
+ * When implementing this, permissions should be extended to the target service or activity
+ * by passing the content using {@link android.content.Intent#setClipData Intent.setClipData}
+ * and {@link android.content.Intent#addFlags(int) setting} the flag
+ * {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION FLAG_GRANT_READ_URI_PERMISSION}.
+ * <p>Alternatively, if using a background thread within the current context to process the
+ * content, a reference to the {@code payload} object should be maintained to ensure that
+ * permissions are not revoked prematurely.
+ *
+ * @param view The view where the content insertion was requested.
+ * @param payload The content to insert and related metadata. The payload may contain multiple
+ * items and their MIME types may be different (e.g. an image item and a text
+ * item). The payload may also contain items whose MIME type is not in the list
+ * of MIME types specified when
+ * {@link View#setOnReceiveContentListener setting} the listener. For
+ * those items, the listener may reject the content (defer to the default
+ * platform behavior) or execute some other fallback logic (e.g. show an
+ * appropriate message to the user).
+ *
+ * @return The portion of the passed-in content whose processing should be delegated to
+ * the platform. Return null if all content was handled in some way. Actual insertion of
+ * the content may be processed asynchronously in the background and may or may not
+ * succeed even if this method returns null. For example, an app may end up not inserting
+ * an item if it exceeds the app's size limit for that type of content.
+ */
+ @Nullable
+ ContentInfo onReceiveContent(@NonNull View view, @NonNull ContentInfo payload);
+}
diff --git a/android/view/OrientationEventListener.java b/android/view/OrientationEventListener.java
new file mode 100644
index 0000000..cd48a4f
--- /dev/null
+++ b/android/view/OrientationEventListener.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.util.Log;
+
+/**
+ * Helper class for receiving notifications from the SensorManager when
+ * the orientation of the device has changed.
+ */
+public abstract class OrientationEventListener {
+ private static final String TAG = "OrientationEventListener";
+ private static final boolean DEBUG = false;
+ private static final boolean localLOGV = false;
+ private int mOrientation = ORIENTATION_UNKNOWN;
+ private SensorManager mSensorManager;
+ private boolean mEnabled = false;
+ private int mRate;
+ private Sensor mSensor;
+ private SensorEventListener mSensorEventListener;
+ private OrientationListener mOldListener;
+
+ /**
+ * Returned from onOrientationChanged when the device orientation cannot be determined
+ * (typically when the device is in a close to flat position).
+ *
+ * @see #onOrientationChanged
+ */
+ public static final int ORIENTATION_UNKNOWN = -1;
+
+ /**
+ * Creates a new OrientationEventListener.
+ *
+ * @param context for the OrientationEventListener.
+ */
+ public OrientationEventListener(Context context) {
+ this(context, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+
+ /**
+ * Creates a new OrientationEventListener.
+ *
+ * @param context for the OrientationEventListener.
+ * @param rate at which sensor events are processed (see also
+ * {@link android.hardware.SensorManager SensorManager}). Use the default
+ * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
+ * SENSOR_DELAY_NORMAL} for simple screen orientation change detection.
+ */
+ public OrientationEventListener(Context context, int rate) {
+ mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
+ mRate = rate;
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ if (mSensor != null) {
+ // Create listener only if sensors do exist
+ mSensorEventListener = new SensorEventListenerImpl();
+ }
+ }
+
+ void registerListener(OrientationListener lis) {
+ mOldListener = lis;
+ }
+
+ /**
+ * Enables the OrientationEventListener so it will monitor the sensor and call
+ * {@link #onOrientationChanged} when the device orientation changes.
+ */
+ public void enable() {
+ if (mSensor == null) {
+ Log.w(TAG, "Cannot detect sensors. Not enabled");
+ return;
+ }
+ if (mEnabled == false) {
+ if (localLOGV) Log.d(TAG, "OrientationEventListener enabled");
+ mSensorManager.registerListener(mSensorEventListener, mSensor, mRate);
+ mEnabled = true;
+ }
+ }
+
+ /**
+ * Disables the OrientationEventListener.
+ */
+ public void disable() {
+ if (mSensor == null) {
+ Log.w(TAG, "Cannot detect sensors. Invalid disable");
+ return;
+ }
+ if (mEnabled == true) {
+ if (localLOGV) Log.d(TAG, "OrientationEventListener disabled");
+ mSensorManager.unregisterListener(mSensorEventListener);
+ mEnabled = false;
+ }
+ }
+
+ class SensorEventListenerImpl implements SensorEventListener {
+ private static final int _DATA_X = 0;
+ private static final int _DATA_Y = 1;
+ private static final int _DATA_Z = 2;
+
+ public void onSensorChanged(SensorEvent event) {
+ float[] values = event.values;
+ int orientation = ORIENTATION_UNKNOWN;
+ float X = -values[_DATA_X];
+ float Y = -values[_DATA_Y];
+ float Z = -values[_DATA_Z];
+ float magnitude = X*X + Y*Y;
+ // Don't trust the angle if the magnitude is small compared to the y value
+ if (magnitude * 4 >= Z*Z) {
+ float OneEightyOverPi = 57.29577957855f;
+ float angle = (float)Math.atan2(-Y, X) * OneEightyOverPi;
+ orientation = 90 - (int)Math.round(angle);
+ // normalize to 0 - 359 range
+ while (orientation >= 360) {
+ orientation -= 360;
+ }
+ while (orientation < 0) {
+ orientation += 360;
+ }
+ }
+ if (mOldListener != null) {
+ mOldListener.onSensorChanged(Sensor.TYPE_ACCELEROMETER, event.values);
+ }
+ if (orientation != mOrientation) {
+ mOrientation = orientation;
+ onOrientationChanged(orientation);
+ }
+ }
+
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+
+ }
+ }
+
+ /*
+ * Returns true if sensor is enabled and false otherwise
+ */
+ public boolean canDetectOrientation() {
+ return mSensor != null;
+ }
+
+ /**
+ * Called when the orientation of the device has changed.
+ * orientation parameter is in degrees, ranging from 0 to 359.
+ * orientation is 0 degrees when the device is oriented in its natural position,
+ * 90 degrees when its left side is at the top, 180 degrees when it is upside down,
+ * and 270 degrees when its right side is to the top.
+ * {@link #ORIENTATION_UNKNOWN} is returned when the device is close to flat
+ * and the orientation cannot be determined.
+ *
+ * @param orientation The new orientation of the device.
+ *
+ * @see #ORIENTATION_UNKNOWN
+ */
+ abstract public void onOrientationChanged(int orientation);
+}
diff --git a/android/view/OrientationListener.java b/android/view/OrientationListener.java
new file mode 100644
index 0000000..ce8074e
--- /dev/null
+++ b/android/view/OrientationListener.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.hardware.SensorListener;
+
+/**
+ * Helper class for receiving notifications from the SensorManager when
+ * the orientation of the device has changed.
+ * @deprecated use {@link android.view.OrientationEventListener} instead.
+ * This class internally uses the OrientationEventListener.
+ */
+@Deprecated
+public abstract class OrientationListener implements SensorListener {
+ private OrientationEventListener mOrientationEventLis;
+
+ /**
+ * Returned from onOrientationChanged when the device orientation cannot be determined
+ * (typically when the device is in a close to flat position).
+ *
+ * @see #onOrientationChanged
+ */
+ public static final int ORIENTATION_UNKNOWN = OrientationEventListener.ORIENTATION_UNKNOWN;
+
+ /**
+ * Creates a new OrientationListener.
+ *
+ * @param context for the OrientationListener.
+ */
+ public OrientationListener(Context context) {
+ mOrientationEventLis = new OrientationEventListenerInternal(context);
+ }
+
+ /**
+ * Creates a new OrientationListener.
+ *
+ * @param context for the OrientationListener.
+ * @param rate at which sensor events are processed (see also
+ * {@link android.hardware.SensorManager SensorManager}). Use the default
+ * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
+ * SENSOR_DELAY_NORMAL} for simple screen orientation change detection.
+ */
+ public OrientationListener(Context context, int rate) {
+ mOrientationEventLis = new OrientationEventListenerInternal(context, rate);
+ }
+
+ class OrientationEventListenerInternal extends OrientationEventListener {
+ OrientationEventListenerInternal(Context context) {
+ super(context);
+ }
+
+ OrientationEventListenerInternal(Context context, int rate) {
+ super(context, rate);
+ // register so that onSensorChanged gets invoked
+ registerListener(OrientationListener.this);
+ }
+
+ public void onOrientationChanged(int orientation) {
+ OrientationListener.this.onOrientationChanged(orientation);
+ }
+ }
+
+ /**
+ * Enables the OrientationListener so it will monitor the sensor and call
+ * {@link #onOrientationChanged} when the device orientation changes.
+ */
+ public void enable() {
+ mOrientationEventLis.enable();
+ }
+
+ /**
+ * Disables the OrientationListener.
+ */
+ public void disable() {
+ mOrientationEventLis.disable();
+ }
+
+ public void onAccuracyChanged(int sensor, int accuracy) {
+ }
+
+ public void onSensorChanged(int sensor, float[] values) {
+ // just ignore the call here onOrientationChanged is invoked anyway
+ }
+
+
+ /**
+ * Look at {@link android.view.OrientationEventListener#onOrientationChanged}
+ * for method description and usage
+ * @param orientation The new orientation of the device.
+ *
+ * @see #ORIENTATION_UNKNOWN
+ */
+ abstract public void onOrientationChanged(int orientation);
+
+}
diff --git a/android/view/PendingInsetsController.java b/android/view/PendingInsetsController.java
new file mode 100644
index 0000000..c61baf6
--- /dev/null
+++ b/android/view/PendingInsetsController.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CancellationSignal;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.animation.Interpolator;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * An insets controller that keeps track of pending requests. This is such that an app can freely
+ * use {@link WindowInsetsController} before the view root is attached during activity startup.
+ * @hide
+ */
+public class PendingInsetsController implements WindowInsetsController {
+
+ private static final int KEEP_BEHAVIOR = -1;
+ private final ArrayList<PendingRequest> mRequests = new ArrayList<>();
+ private @Appearance int mAppearance;
+ private @Appearance int mAppearanceMask;
+ private @Behavior int mBehavior = KEEP_BEHAVIOR;
+ private boolean mAnimationsDisabled;
+ private final InsetsState mDummyState = new InsetsState();
+ private InsetsController mReplayedInsetsController;
+ private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
+ = new ArrayList<>();
+ private int mCaptionInsetsHeight = 0;
+
+ @Override
+ public void show(int types) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.show(types);
+ } else {
+ mRequests.add(new ShowRequest(types));
+ }
+ }
+
+ @Override
+ public void hide(int types) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.hide(types);
+ } else {
+ mRequests.add(new HideRequest(types));
+ }
+ }
+
+ @Override
+ public void setSystemBarsAppearance(int appearance, int mask) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.setSystemBarsAppearance(appearance, mask);
+ } else {
+ mAppearance = (mAppearance & ~mask) | (appearance & mask);
+ mAppearanceMask |= mask;
+ }
+ }
+
+ @Override
+ public int getSystemBarsAppearance() {
+ if (mReplayedInsetsController != null) {
+ return mReplayedInsetsController.getSystemBarsAppearance();
+ }
+ return mAppearance;
+ }
+
+ @Override
+ public void setCaptionInsetsHeight(int height) {
+ mCaptionInsetsHeight = height;
+ }
+
+ @Override
+ public void setSystemBarsBehavior(int behavior) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.setSystemBarsBehavior(behavior);
+ } else {
+ mBehavior = behavior;
+ }
+ }
+
+ @Override
+ public int getSystemBarsBehavior() {
+ if (mReplayedInsetsController != null) {
+ return mReplayedInsetsController.getSystemBarsBehavior();
+ }
+ if (mBehavior == KEEP_BEHAVIOR) {
+ return BEHAVIOR_DEFAULT;
+ }
+ return mBehavior;
+ }
+
+ @Override
+ public void setAnimationsDisabled(boolean disable) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.setAnimationsDisabled(disable);
+ } else {
+ mAnimationsDisabled = disable;
+ }
+ }
+
+ @Override
+ public InsetsState getState() {
+ return mDummyState;
+ }
+
+ @Override
+ public boolean isRequestedVisible(int type) {
+
+ // Method is only used once real insets controller is attached, so no need to traverse
+ // requests here.
+ return InsetsState.getDefaultVisibility(type);
+ }
+
+ @Override
+ public void addOnControllableInsetsChangedListener(
+ OnControllableInsetsChangedListener listener) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.addOnControllableInsetsChangedListener(listener);
+ } else {
+ mControllableInsetsChangedListeners.add(listener);
+ listener.onControllableInsetsChanged(this, 0);
+ }
+ }
+
+ @Override
+ public void removeOnControllableInsetsChangedListener(
+ OnControllableInsetsChangedListener listener) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.removeOnControllableInsetsChangedListener(listener);
+ } else {
+ mControllableInsetsChangedListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Replays the commands on {@code controller} and attaches it to this instance such that any
+ * calls will be forwarded to the real instance in the future.
+ */
+ @VisibleForTesting
+ public void replayAndAttach(InsetsController controller) {
+ if (mBehavior != KEEP_BEHAVIOR) {
+ controller.setSystemBarsBehavior(mBehavior);
+ }
+ if (mAppearanceMask != 0) {
+ controller.setSystemBarsAppearance(mAppearance, mAppearanceMask);
+ }
+ if (mCaptionInsetsHeight != 0) {
+ controller.setCaptionInsetsHeight(mCaptionInsetsHeight);
+ }
+ if (mAnimationsDisabled) {
+ controller.setAnimationsDisabled(true);
+ }
+ int size = mRequests.size();
+ for (int i = 0; i < size; i++) {
+ mRequests.get(i).replay(controller);
+ }
+ size = mControllableInsetsChangedListeners.size();
+ for (int i = 0; i < size; i++) {
+ controller.addOnControllableInsetsChangedListener(
+ mControllableInsetsChangedListeners.get(i));
+ }
+
+ // Reset all state so it doesn't get applied twice just in case
+ mRequests.clear();
+ mControllableInsetsChangedListeners.clear();
+ mBehavior = KEEP_BEHAVIOR;
+ mAppearance = 0;
+ mAppearanceMask = 0;
+ mAnimationsDisabled = false;
+
+ // After replaying, we forward everything directly to the replayed instance.
+ mReplayedInsetsController = controller;
+ }
+
+ /**
+ * Detaches the controller to no longer forward calls to the real instance.
+ */
+ @VisibleForTesting
+ public void detach() {
+ mReplayedInsetsController = null;
+ }
+
+ @Override
+ public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
+ @Nullable Interpolator interpolator,
+ CancellationSignal cancellationSignal,
+ @NonNull WindowInsetsAnimationControlListener listener) {
+ if (mReplayedInsetsController != null) {
+ mReplayedInsetsController.controlWindowInsetsAnimation(types, durationMillis,
+ interpolator, cancellationSignal, listener);
+ } else {
+ listener.onCancelled(null);
+ }
+ }
+
+ private interface PendingRequest {
+ void replay(InsetsController controller);
+ }
+
+ private static class ShowRequest implements PendingRequest {
+
+ private final @InsetsType int mTypes;
+
+ public ShowRequest(int types) {
+ mTypes = types;
+ }
+
+ @Override
+ public void replay(InsetsController controller) {
+ controller.show(mTypes);
+ }
+ }
+
+ private static class HideRequest implements PendingRequest {
+
+ private final @InsetsType int mTypes;
+
+ public HideRequest(int types) {
+ mTypes = types;
+ }
+
+ @Override
+ public void replay(InsetsController controller) {
+ controller.hide(mTypes);
+ }
+ }
+}
diff --git a/android/view/PixelCopy.java b/android/view/PixelCopy.java
new file mode 100644
index 0000000..2797a4d
--- /dev/null
+++ b/android/view/PixelCopy.java
@@ -0,0 +1,298 @@
+/*
+ * 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.view;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides a mechanisms to issue pixel copy requests to allow for copy
+ * operations from {@link Surface} to {@link Bitmap}
+ */
+public final class PixelCopy {
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SUCCESS, ERROR_UNKNOWN, ERROR_TIMEOUT, ERROR_SOURCE_NO_DATA,
+ ERROR_SOURCE_INVALID, ERROR_DESTINATION_INVALID})
+ public @interface CopyResultStatus {}
+
+ /** The pixel copy request succeeded */
+ public static final int SUCCESS = 0;
+
+ /** The pixel copy request failed with an unknown error. */
+ public static final int ERROR_UNKNOWN = 1;
+
+ /**
+ * A timeout occurred while trying to acquire a buffer from the source to
+ * copy from.
+ */
+ public static final int ERROR_TIMEOUT = 2;
+
+ /**
+ * The source has nothing to copy from. When the source is a {@link Surface}
+ * this means that no buffers have been queued yet. Wait for the source
+ * to produce a frame and try again.
+ */
+ public static final int ERROR_SOURCE_NO_DATA = 3;
+
+ /**
+ * It is not possible to copy from the source. This can happen if the source
+ * is hardware-protected or destroyed.
+ */
+ public static final int ERROR_SOURCE_INVALID = 4;
+
+ /**
+ * The destination isn't a valid copy target. If the destination is a bitmap
+ * this can occur if the bitmap is too large for the hardware to copy to.
+ * It can also occur if the destination has been destroyed.
+ */
+ public static final int ERROR_DESTINATION_INVALID = 5;
+
+ /**
+ * Listener for observing the completion of a PixelCopy request.
+ */
+ public interface OnPixelCopyFinishedListener {
+ /**
+ * Callback for when a pixel copy request has completed. This will be called
+ * regardless of whether the copy succeeded or failed.
+ *
+ * @param copyResult Contains the resulting status of the copy request.
+ * This will either be {@link PixelCopy#SUCCESS} or one of the
+ * <code>PixelCopy.ERROR_*</code> values.
+ */
+ void onPixelCopyFinished(@CopyResultStatus int copyResult);
+ }
+
+ /**
+ * Requests for the display content of a {@link SurfaceView} to be copied
+ * into a provided {@link Bitmap}.
+ *
+ * The contents of the source will be scaled to fit exactly inside the bitmap.
+ * The pixel format of the source buffer will be converted, as part of the copy,
+ * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer
+ * in the SurfaceView's Surface will be used as the source of the copy.
+ *
+ * @param source The source from which to copy
+ * @param dest The destination of the copy. The source will be scaled to
+ * match the width, height, and format of this bitmap.
+ * @param listener Callback for when the pixel copy request completes
+ * @param listenerThread The callback will be invoked on this Handler when
+ * the copy is finished.
+ */
+ public static void request(@NonNull SurfaceView source, @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) {
+ request(source.getHolder().getSurface(), dest, listener, listenerThread);
+ }
+
+ /**
+ * Requests for the display content of a {@link SurfaceView} to be copied
+ * into a provided {@link Bitmap}.
+ *
+ * The contents of the source will be scaled to fit exactly inside the bitmap.
+ * The pixel format of the source buffer will be converted, as part of the copy,
+ * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer
+ * in the SurfaceView's Surface will be used as the source of the copy.
+ *
+ * @param source The source from which to copy
+ * @param srcRect The area of the source to copy from. If this is null
+ * the copy area will be the entire surface. The rect will be clamped to
+ * the bounds of the Surface.
+ * @param dest The destination of the copy. The source will be scaled to
+ * match the width, height, and format of this bitmap.
+ * @param listener Callback for when the pixel copy request completes
+ * @param listenerThread The callback will be invoked on this Handler when
+ * the copy is finished.
+ */
+ public static void request(@NonNull SurfaceView source, @Nullable Rect srcRect,
+ @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ request(source.getHolder().getSurface(), srcRect,
+ dest, listener, listenerThread);
+ }
+
+ /**
+ * Requests a copy of the pixels from a {@link Surface} to be copied into
+ * a provided {@link Bitmap}.
+ *
+ * The contents of the source will be scaled to fit exactly inside the bitmap.
+ * The pixel format of the source buffer will be converted, as part of the copy,
+ * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer
+ * in the Surface will be used as the source of the copy.
+ *
+ * @param source The source from which to copy
+ * @param dest The destination of the copy. The source will be scaled to
+ * match the width, height, and format of this bitmap.
+ * @param listener Callback for when the pixel copy request completes
+ * @param listenerThread The callback will be invoked on this Handler when
+ * the copy is finished.
+ */
+ public static void request(@NonNull Surface source, @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) {
+ request(source, null, dest, listener, listenerThread);
+ }
+
+ /**
+ * Requests a copy of the pixels at the provided {@link Rect} from
+ * a {@link Surface} to be copied into a provided {@link Bitmap}.
+ *
+ * The contents of the source rect will be scaled to fit exactly inside the bitmap.
+ * The pixel format of the source buffer will be converted, as part of the copy,
+ * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer
+ * in the Surface will be used as the source of the copy.
+ *
+ * @param source The source from which to copy
+ * @param srcRect The area of the source to copy from. If this is null
+ * the copy area will be the entire surface. The rect will be clamped to
+ * the bounds of the Surface.
+ * @param dest The destination of the copy. The source will be scaled to
+ * match the width, height, and format of this bitmap.
+ * @param listener Callback for when the pixel copy request completes
+ * @param listenerThread The callback will be invoked on this Handler when
+ * the copy is finished.
+ */
+ public static void request(@NonNull Surface source, @Nullable Rect srcRect,
+ @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ validateBitmapDest(dest);
+ if (!source.isValid()) {
+ throw new IllegalArgumentException("Surface isn't valid, source.isValid() == false");
+ }
+ if (srcRect != null && srcRect.isEmpty()) {
+ throw new IllegalArgumentException("sourceRect is empty");
+ }
+ // TODO: Make this actually async and fast and cool and stuff
+ int result = ThreadedRenderer.copySurfaceInto(source, srcRect, dest);
+ listenerThread.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onPixelCopyFinished(result);
+ }
+ });
+ }
+
+ /**
+ * Requests a copy of the pixels from a {@link Window} to be copied into
+ * a provided {@link Bitmap}.
+ *
+ * The contents of the source will be scaled to fit exactly inside the bitmap.
+ * The pixel format of the source buffer will be converted, as part of the copy,
+ * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer
+ * in the Window's Surface will be used as the source of the copy.
+ *
+ * Note: This is limited to being able to copy from Window's with a non-null
+ * DecorView. If {@link Window#peekDecorView()} is null this throws an
+ * {@link IllegalArgumentException}. It will similarly throw an exception
+ * if the DecorView has not yet acquired a backing surface. It is recommended
+ * that {@link OnDrawListener} is used to ensure that at least one draw
+ * has happened before trying to copy from the window, otherwise either
+ * an {@link IllegalArgumentException} will be thrown or an error will
+ * be returned to the {@link OnPixelCopyFinishedListener}.
+ *
+ * @param source The source from which to copy
+ * @param dest The destination of the copy. The source will be scaled to
+ * match the width, height, and format of this bitmap.
+ * @param listener Callback for when the pixel copy request completes
+ * @param listenerThread The callback will be invoked on this Handler when
+ * the copy is finished.
+ */
+ public static void request(@NonNull Window source, @NonNull Bitmap dest,
+ @NonNull OnPixelCopyFinishedListener listener, @NonNull Handler listenerThread) {
+ request(source, null, dest, listener, listenerThread);
+ }
+
+ /**
+ * Requests a copy of the pixels at the provided {@link Rect} from
+ * a {@link Window} to be copied into a provided {@link Bitmap}.
+ *
+ * The contents of the source rect will be scaled to fit exactly inside the bitmap.
+ * The pixel format of the source buffer will be converted, as part of the copy,
+ * to fit the the bitmap's {@link Bitmap.Config}. The most recently queued buffer
+ * in the Window's Surface will be used as the source of the copy.
+ *
+ * Note: This is limited to being able to copy from Window's with a non-null
+ * DecorView. If {@link Window#peekDecorView()} is null this throws an
+ * {@link IllegalArgumentException}. It will similarly throw an exception
+ * if the DecorView has not yet acquired a backing surface. It is recommended
+ * that {@link OnDrawListener} is used to ensure that at least one draw
+ * has happened before trying to copy from the window, otherwise either
+ * an {@link IllegalArgumentException} will be thrown or an error will
+ * be returned to the {@link OnPixelCopyFinishedListener}.
+ *
+ * @param source The source from which to copy
+ * @param srcRect The area of the source to copy from. If this is null
+ * the copy area will be the entire surface. The rect will be clamped to
+ * the bounds of the Surface.
+ * @param dest The destination of the copy. The source will be scaled to
+ * match the width, height, and format of this bitmap.
+ * @param listener Callback for when the pixel copy request completes
+ * @param listenerThread The callback will be invoked on this Handler when
+ * the copy is finished.
+ */
+ public static void request(@NonNull Window source, @Nullable Rect srcRect,
+ @NonNull Bitmap dest, @NonNull OnPixelCopyFinishedListener listener,
+ @NonNull Handler listenerThread) {
+ validateBitmapDest(dest);
+ if (source == null) {
+ throw new IllegalArgumentException("source is null");
+ }
+ if (source.peekDecorView() == null) {
+ throw new IllegalArgumentException(
+ "Only able to copy windows with decor views");
+ }
+ Surface surface = null;
+ final ViewRootImpl root = source.peekDecorView().getViewRootImpl();
+ if (root != null) {
+ surface = root.mSurface;
+ final Rect surfaceInsets = root.mWindowAttributes.surfaceInsets;
+ if (srcRect == null) {
+ srcRect = new Rect(surfaceInsets.left, surfaceInsets.top,
+ root.mWidth + surfaceInsets.left, root.mHeight + surfaceInsets.top);
+ } else {
+ srcRect.offset(surfaceInsets.left, surfaceInsets.top);
+ }
+ }
+ if (surface == null || !surface.isValid()) {
+ throw new IllegalArgumentException(
+ "Window doesn't have a backing surface!");
+ }
+ request(surface, srcRect, dest, listener, listenerThread);
+ }
+
+ private static void validateBitmapDest(Bitmap bitmap) {
+ // TODO: Pre-check max texture dimens if we can
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Bitmap cannot be null");
+ }
+ if (bitmap.isRecycled()) {
+ throw new IllegalArgumentException("Bitmap is recycled");
+ }
+ if (!bitmap.isMutable()) {
+ throw new IllegalArgumentException("Bitmap is immutable");
+ }
+ }
+
+ private PixelCopy() {}
+}
diff --git a/android/view/PointerIcon.java b/android/view/PointerIcon.java
new file mode 100644
index 0000000..38ae03d
--- /dev/null
+++ b/android/view/PointerIcon.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.XmlRes;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.XmlUtils;
+
+/**
+ * Represents an icon that can be used as a mouse pointer.
+ * <p>
+ * Pointer icons can be provided either by the system using system types,
+ * or by applications using bitmaps or application resources.
+ * </p>
+ */
+public final class PointerIcon implements Parcelable {
+ private static final String TAG = "PointerIcon";
+
+ /** {@hide} Type constant: Custom icon with a user-supplied bitmap. */
+ public static final int TYPE_CUSTOM = -1;
+
+ /** Type constant: Null icon. It has no bitmap. */
+ public static final int TYPE_NULL = 0;
+
+ /** Type constant: no icons are specified. If all views uses this, then falls back
+ * to the default type, but this is helpful to distinguish a view explicitly want
+ * to have the default icon.
+ * @hide
+ */
+ public static final int TYPE_NOT_SPECIFIED = 1;
+
+ /** Type constant: Arrow icon. (Default mouse pointer) */
+ public static final int TYPE_ARROW = 1000;
+
+ /** {@hide} Type constant: Spot hover icon for touchpads. */
+ public static final int TYPE_SPOT_HOVER = 2000;
+
+ /** {@hide} Type constant: Spot touch icon for touchpads. */
+ public static final int TYPE_SPOT_TOUCH = 2001;
+
+ /** {@hide} Type constant: Spot anchor icon for touchpads. */
+ public static final int TYPE_SPOT_ANCHOR = 2002;
+
+ // Type constants for additional predefined icons for mice.
+ /** Type constant: context-menu. */
+ public static final int TYPE_CONTEXT_MENU = 1001;
+
+ /** Type constant: hand. */
+ public static final int TYPE_HAND = 1002;
+
+ /** Type constant: help. */
+ public static final int TYPE_HELP = 1003;
+
+ /** Type constant: wait. */
+ public static final int TYPE_WAIT = 1004;
+
+ /** Type constant: cell. */
+ public static final int TYPE_CELL = 1006;
+
+ /** Type constant: crosshair. */
+ public static final int TYPE_CROSSHAIR = 1007;
+
+ /** Type constant: text. */
+ public static final int TYPE_TEXT = 1008;
+
+ /** Type constant: vertical-text. */
+ public static final int TYPE_VERTICAL_TEXT = 1009;
+
+ /** Type constant: alias (indicating an alias of/shortcut to something is
+ * to be created. */
+ public static final int TYPE_ALIAS = 1010;
+
+ /** Type constant: copy. */
+ public static final int TYPE_COPY = 1011;
+
+ /** Type constant: no-drop. */
+ public static final int TYPE_NO_DROP = 1012;
+
+ /** Type constant: all-scroll. */
+ public static final int TYPE_ALL_SCROLL = 1013;
+
+ /** Type constant: horizontal double arrow mainly for resizing. */
+ public static final int TYPE_HORIZONTAL_DOUBLE_ARROW = 1014;
+
+ /** Type constant: vertical double arrow mainly for resizing. */
+ public static final int TYPE_VERTICAL_DOUBLE_ARROW = 1015;
+
+ /** Type constant: diagonal double arrow -- top-right to bottom-left. */
+ public static final int TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW = 1016;
+
+ /** Type constant: diagonal double arrow -- top-left to bottom-right. */
+ public static final int TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW = 1017;
+
+ /** Type constant: zoom-in. */
+ public static final int TYPE_ZOOM_IN = 1018;
+
+ /** Type constant: zoom-out. */
+ public static final int TYPE_ZOOM_OUT = 1019;
+
+ /** Type constant: grab. */
+ public static final int TYPE_GRAB = 1020;
+
+ /** Type constant: grabbing. */
+ public static final int TYPE_GRABBING = 1021;
+
+ // OEM private types should be defined starting at this range to avoid
+ // conflicts with any system types that may be defined in the future.
+ private static final int TYPE_OEM_FIRST = 10000;
+
+ /** The default pointer icon. */
+ public static final int TYPE_DEFAULT = TYPE_ARROW;
+
+ private static final PointerIcon gNullIcon = new PointerIcon(TYPE_NULL);
+ private static final SparseArray<SparseArray<PointerIcon>> gSystemIconsByDisplay =
+ new SparseArray<SparseArray<PointerIcon>>();
+ private static boolean sUseLargeIcons = false;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final int mType;
+ private int mSystemIconResourceId;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Bitmap mBitmap;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private float mHotSpotX;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private float mHotSpotY;
+ // The bitmaps for the additional frame of animated pointer icon. Note that the first frame
+ // will be stored in mBitmap.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Bitmap mBitmapFrames[];
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mDurationPerFrame;
+
+ /**
+ * Listener for displays lifecycle.
+ * @hide
+ */
+ private static DisplayManager.DisplayListener sDisplayListener;
+
+ private PointerIcon(int type) {
+ mType = type;
+ }
+
+ /**
+ * Gets a special pointer icon that has no bitmap.
+ *
+ * @return The null pointer icon.
+ *
+ * @see #TYPE_NULL
+ * @hide
+ */
+ public static PointerIcon getNullIcon() {
+ return gNullIcon;
+ }
+
+ /**
+ * Gets the default pointer icon.
+ *
+ * @param context The context.
+ * @return The default pointer icon.
+ *
+ * @throws IllegalArgumentException if context is null.
+ * @hide
+ */
+ public static PointerIcon getDefaultIcon(@NonNull Context context) {
+ return getSystemIcon(context, TYPE_DEFAULT);
+ }
+
+ /**
+ * Gets a system pointer icon for the given type.
+ * If typeis not recognized, returns the default pointer icon.
+ *
+ * @param context The context.
+ * @param type The pointer icon type.
+ * @return The pointer icon.
+ *
+ * @throws IllegalArgumentException if context is null.
+ */
+ public static PointerIcon getSystemIcon(@NonNull Context context, int type) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+
+ if (type == TYPE_NULL) {
+ return gNullIcon;
+ }
+
+ if (sDisplayListener == null) {
+ registerDisplayListener(context);
+ }
+
+ final int displayId = context.getDisplayId();
+ SparseArray<PointerIcon> systemIcons = gSystemIconsByDisplay.get(displayId);
+ if (systemIcons == null) {
+ systemIcons = new SparseArray<>();
+ gSystemIconsByDisplay.put(displayId, systemIcons);
+ }
+
+ PointerIcon icon = systemIcons.get(type);
+ // Reload if not in the same display.
+ if (icon != null) {
+ return icon;
+ }
+
+ int typeIndex = getSystemIconTypeIndex(type);
+ if (typeIndex == 0) {
+ typeIndex = getSystemIconTypeIndex(TYPE_DEFAULT);
+ }
+
+ int defStyle = sUseLargeIcons ?
+ com.android.internal.R.style.LargePointer : com.android.internal.R.style.Pointer;
+ TypedArray a = context.obtainStyledAttributes(null,
+ com.android.internal.R.styleable.Pointer,
+ 0, defStyle);
+ int resourceId = a.getResourceId(typeIndex, -1);
+ a.recycle();
+
+ if (resourceId == -1) {
+ Log.w(TAG, "Missing theme resources for pointer icon type " + type);
+ return type == TYPE_DEFAULT ? gNullIcon : getSystemIcon(context, TYPE_DEFAULT);
+ }
+
+ icon = new PointerIcon(type);
+ if ((resourceId & 0xff000000) == 0x01000000) {
+ icon.mSystemIconResourceId = resourceId;
+ } else {
+ icon.loadResource(context, context.getResources(), resourceId);
+ }
+ systemIcons.append(type, icon);
+ return icon;
+ }
+
+ /**
+ * Updates wheter accessibility large icons are used or not.
+ * @hide
+ */
+ public static void setUseLargeIcons(boolean use) {
+ sUseLargeIcons = use;
+ gSystemIconsByDisplay.clear();
+ }
+
+ /**
+ * Creates a custom pointer icon from the given bitmap and hotspot information.
+ *
+ * @param bitmap The bitmap for the icon.
+ * @param hotSpotX The X offset of the pointer icon hotspot in the bitmap.
+ * Must be within the [0, bitmap.getWidth()) range.
+ * @param hotSpotY The Y offset of the pointer icon hotspot in the bitmap.
+ * Must be within the [0, bitmap.getHeight()) range.
+ * @return A pointer icon for this bitmap.
+ *
+ * @throws IllegalArgumentException if bitmap is null, or if the x/y hotspot
+ * parameters are invalid.
+ */
+ public static PointerIcon create(@NonNull Bitmap bitmap, float hotSpotX, float hotSpotY) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("bitmap must not be null");
+ }
+ validateHotSpot(bitmap, hotSpotX, hotSpotY);
+
+ PointerIcon icon = new PointerIcon(TYPE_CUSTOM);
+ icon.mBitmap = bitmap;
+ icon.mHotSpotX = hotSpotX;
+ icon.mHotSpotY = hotSpotY;
+ return icon;
+ }
+
+ /**
+ * Loads a custom pointer icon from an XML resource.
+ * <p>
+ * The XML resource should have the following form:
+ * <code>
+ * <?xml version="1.0" encoding="utf-8"?>
+ * <pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:bitmap="@drawable/my_pointer_bitmap"
+ * android:hotSpotX="24"
+ * android:hotSpotY="24" />
+ * </code>
+ * </p>
+ *
+ * @param resources The resources object.
+ * @param resourceId The resource id.
+ * @return The pointer icon.
+ *
+ * @throws IllegalArgumentException if resources is null.
+ * @throws Resources.NotFoundException if the resource was not found or the drawable
+ * linked in the resource was not found.
+ */
+ public static PointerIcon load(@NonNull Resources resources, @XmlRes int resourceId) {
+ if (resources == null) {
+ throw new IllegalArgumentException("resources must not be null");
+ }
+
+ PointerIcon icon = new PointerIcon(TYPE_CUSTOM);
+ icon.loadResource(null, resources, resourceId);
+ return icon;
+ }
+
+ /**
+ * Loads the bitmap and hotspot information for a pointer icon, if it is not already loaded.
+ * Returns a pointer icon (not necessarily the same instance) with the information filled in.
+ *
+ * @param context The context.
+ * @return The loaded pointer icon.
+ *
+ * @throws IllegalArgumentException if context is null.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public PointerIcon load(@NonNull Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+
+ if (mSystemIconResourceId == 0 || mBitmap != null) {
+ return this;
+ }
+
+ PointerIcon result = new PointerIcon(mType);
+ result.mSystemIconResourceId = mSystemIconResourceId;
+ result.loadResource(context, context.getResources(), mSystemIconResourceId);
+ return result;
+ }
+
+ /** @hide */
+ public int getType() {
+ return mType;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<PointerIcon> CREATOR
+ = new Parcelable.Creator<PointerIcon>() {
+ public PointerIcon createFromParcel(Parcel in) {
+ int type = in.readInt();
+ if (type == TYPE_NULL) {
+ return getNullIcon();
+ }
+
+ int systemIconResourceId = in.readInt();
+ if (systemIconResourceId != 0) {
+ PointerIcon icon = new PointerIcon(type);
+ icon.mSystemIconResourceId = systemIconResourceId;
+ return icon;
+ }
+
+ Bitmap bitmap = Bitmap.CREATOR.createFromParcel(in);
+ float hotSpotX = in.readFloat();
+ float hotSpotY = in.readFloat();
+ return PointerIcon.create(bitmap, hotSpotX, hotSpotY);
+ }
+
+ public PointerIcon[] newArray(int size) {
+ return new PointerIcon[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mType);
+
+ if (mType != TYPE_NULL) {
+ out.writeInt(mSystemIconResourceId);
+ if (mSystemIconResourceId == 0) {
+ mBitmap.writeToParcel(out, flags);
+ out.writeFloat(mHotSpotX);
+ out.writeFloat(mHotSpotY);
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other == null || !(other instanceof PointerIcon)) {
+ return false;
+ }
+
+ PointerIcon otherIcon = (PointerIcon) other;
+ if (mType != otherIcon.mType
+ || mSystemIconResourceId != otherIcon.mSystemIconResourceId) {
+ return false;
+ }
+
+ if (mSystemIconResourceId == 0 && (mBitmap != otherIcon.mBitmap
+ || mHotSpotX != otherIcon.mHotSpotX
+ || mHotSpotY != otherIcon.mHotSpotY)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the Bitmap from the Drawable.
+ *
+ * If the Bitmap needed to be scaled up to account for density, BitmapDrawable
+ * handles this at draw time. But this class doesn't actually draw the Bitmap;
+ * it is just a holder for native code to access its SkBitmap. So this needs to
+ * get a version that is scaled to account for density.
+ */
+ private Bitmap getBitmapFromDrawable(BitmapDrawable bitmapDrawable) {
+ Bitmap bitmap = bitmapDrawable.getBitmap();
+ final int scaledWidth = bitmapDrawable.getIntrinsicWidth();
+ final int scaledHeight = bitmapDrawable.getIntrinsicHeight();
+ if (scaledWidth == bitmap.getWidth() && scaledHeight == bitmap.getHeight()) {
+ return bitmap;
+ }
+
+ Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ RectF dst = new RectF(0, 0, scaledWidth, scaledHeight);
+
+ Bitmap scaled = Bitmap.createBitmap(scaledWidth, scaledHeight, bitmap.getConfig());
+ Canvas canvas = new Canvas(scaled);
+ Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ canvas.drawBitmap(bitmap, src, dst, paint);
+ return scaled;
+ }
+
+ private void loadResource(Context context, Resources resources, @XmlRes int resourceId) {
+ final XmlResourceParser parser = resources.getXml(resourceId);
+ final int bitmapRes;
+ final float hotSpotX;
+ final float hotSpotY;
+ try {
+ XmlUtils.beginDocument(parser, "pointer-icon");
+
+ final TypedArray a = resources.obtainAttributes(
+ parser, com.android.internal.R.styleable.PointerIcon);
+ bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
+ hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
+ hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+ a.recycle();
+ } catch (Exception ex) {
+ throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
+ } finally {
+ parser.close();
+ }
+
+ if (bitmapRes == 0) {
+ throw new IllegalArgumentException("<pointer-icon> is missing bitmap attribute.");
+ }
+
+ Drawable drawable;
+ if (context == null) {
+ drawable = resources.getDrawable(bitmapRes);
+ } else {
+ drawable = context.getDrawable(bitmapRes);
+ }
+ if (drawable instanceof AnimationDrawable) {
+ // Extract animation frame bitmaps.
+ final AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
+ final int frames = animationDrawable.getNumberOfFrames();
+ drawable = animationDrawable.getFrame(0);
+ if (frames == 1) {
+ Log.w(TAG, "Animation icon with single frame -- simply treating the first "
+ + "frame as a normal bitmap icon.");
+ } else {
+ // Assumes they have the exact duration.
+ mDurationPerFrame = animationDrawable.getDuration(0);
+ mBitmapFrames = new Bitmap[frames - 1];
+ final int width = drawable.getIntrinsicWidth();
+ final int height = drawable.getIntrinsicHeight();
+ for (int i = 1; i < frames; ++i) {
+ Drawable drawableFrame = animationDrawable.getFrame(i);
+ if (!(drawableFrame instanceof BitmapDrawable)) {
+ throw new IllegalArgumentException("Frame of an animated pointer icon "
+ + "must refer to a bitmap drawable.");
+ }
+ if (drawableFrame.getIntrinsicWidth() != width ||
+ drawableFrame.getIntrinsicHeight() != height) {
+ throw new IllegalArgumentException("The bitmap size of " + i + "-th frame "
+ + "is different. All frames should have the exact same size and "
+ + "share the same hotspot.");
+ }
+ BitmapDrawable bitmapDrawableFrame = (BitmapDrawable) drawableFrame;
+ mBitmapFrames[i - 1] = getBitmapFromDrawable(bitmapDrawableFrame);
+ }
+ }
+ }
+ if (!(drawable instanceof BitmapDrawable)) {
+ throw new IllegalArgumentException("<pointer-icon> bitmap attribute must "
+ + "refer to a bitmap drawable.");
+ }
+
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ final Bitmap bitmap = getBitmapFromDrawable(bitmapDrawable);
+ validateHotSpot(bitmap, hotSpotX, hotSpotY);
+ // Set the properties now that we have successfully loaded the icon.
+ mBitmap = bitmap;
+ mHotSpotX = hotSpotX;
+ mHotSpotY = hotSpotY;
+ }
+
+ private static void validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY) {
+ if (hotSpotX < 0 || hotSpotX >= bitmap.getWidth()) {
+ throw new IllegalArgumentException("x hotspot lies outside of the bitmap area");
+ }
+ if (hotSpotY < 0 || hotSpotY >= bitmap.getHeight()) {
+ throw new IllegalArgumentException("y hotspot lies outside of the bitmap area");
+ }
+ }
+
+ private static int getSystemIconTypeIndex(int type) {
+ switch (type) {
+ case TYPE_ARROW:
+ return com.android.internal.R.styleable.Pointer_pointerIconArrow;
+ case TYPE_SPOT_HOVER:
+ return com.android.internal.R.styleable.Pointer_pointerIconSpotHover;
+ case TYPE_SPOT_TOUCH:
+ return com.android.internal.R.styleable.Pointer_pointerIconSpotTouch;
+ case TYPE_SPOT_ANCHOR:
+ return com.android.internal.R.styleable.Pointer_pointerIconSpotAnchor;
+ case TYPE_HAND:
+ return com.android.internal.R.styleable.Pointer_pointerIconHand;
+ case TYPE_CONTEXT_MENU:
+ return com.android.internal.R.styleable.Pointer_pointerIconContextMenu;
+ case TYPE_HELP:
+ return com.android.internal.R.styleable.Pointer_pointerIconHelp;
+ case TYPE_WAIT:
+ return com.android.internal.R.styleable.Pointer_pointerIconWait;
+ case TYPE_CELL:
+ return com.android.internal.R.styleable.Pointer_pointerIconCell;
+ case TYPE_CROSSHAIR:
+ return com.android.internal.R.styleable.Pointer_pointerIconCrosshair;
+ case TYPE_TEXT:
+ return com.android.internal.R.styleable.Pointer_pointerIconText;
+ case TYPE_VERTICAL_TEXT:
+ return com.android.internal.R.styleable.Pointer_pointerIconVerticalText;
+ case TYPE_ALIAS:
+ return com.android.internal.R.styleable.Pointer_pointerIconAlias;
+ case TYPE_COPY:
+ return com.android.internal.R.styleable.Pointer_pointerIconCopy;
+ case TYPE_ALL_SCROLL:
+ return com.android.internal.R.styleable.Pointer_pointerIconAllScroll;
+ case TYPE_NO_DROP:
+ return com.android.internal.R.styleable.Pointer_pointerIconNodrop;
+ case TYPE_HORIZONTAL_DOUBLE_ARROW:
+ return com.android.internal.R.styleable.Pointer_pointerIconHorizontalDoubleArrow;
+ case TYPE_VERTICAL_DOUBLE_ARROW:
+ return com.android.internal.R.styleable.Pointer_pointerIconVerticalDoubleArrow;
+ case TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW:
+ return com.android.internal.R.styleable.
+ Pointer_pointerIconTopRightDiagonalDoubleArrow;
+ case TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW:
+ return com.android.internal.R.styleable.
+ Pointer_pointerIconTopLeftDiagonalDoubleArrow;
+ case TYPE_ZOOM_IN:
+ return com.android.internal.R.styleable.Pointer_pointerIconZoomIn;
+ case TYPE_ZOOM_OUT:
+ return com.android.internal.R.styleable.Pointer_pointerIconZoomOut;
+ case TYPE_GRAB:
+ return com.android.internal.R.styleable.Pointer_pointerIconGrab;
+ case TYPE_GRABBING:
+ return com.android.internal.R.styleable.Pointer_pointerIconGrabbing;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Manage system icon cache handled by display lifecycle.
+ * @param context The context.
+ */
+ private static void registerDisplayListener(@NonNull Context context) {
+ sDisplayListener = new DisplayManager.DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ gSystemIconsByDisplay.remove(displayId);
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ gSystemIconsByDisplay.remove(displayId);
+ }
+ };
+
+ DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ displayManager.registerDisplayListener(sDisplayListener, null /* handler */);
+ }
+
+}
diff --git a/android/view/PointerIcon_Delegate.java b/android/view/PointerIcon_Delegate.java
new file mode 100644
index 0000000..84c75f7
--- /dev/null
+++ b/android/view/PointerIcon_Delegate.java
@@ -0,0 +1,38 @@
+/*
+ * 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.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+public class PointerIcon_Delegate {
+
+ @LayoutlibDelegate
+ /*package*/ static void loadResource(PointerIcon icon, Context context, Resources resources,
+ int resourceId) {
+ // HACK: This bypasses the problem of having an enum resolved as a resourceId.
+ // PointerIcon would not be displayed by layoutlib anyway, so we always return the null
+ // icon.
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void registerDisplayListener(Context context) {
+ // Ignore this as we do not have a DisplayManager
+ }
+}
diff --git a/android/view/PrivacyIndicatorBounds.java b/android/view/PrivacyIndicatorBounds.java
new file mode 100644
index 0000000..4cf5eda
--- /dev/null
+++ b/android/view/PrivacyIndicatorBounds.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.Surface.ROTATION_0;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcelable;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DataClass;
+
+/**
+ * A class which manages the bounds of the privacy indicator
+ *
+ * @hide
+ */
+@DataClass(
+ genEqualsHashCode = true,
+ genConstructor = false,
+ genAidl = false,
+ genGetters = false
+)
+public class PrivacyIndicatorBounds implements Parcelable {
+
+ private final @NonNull Rect[] mStaticBounds;
+ private final int mRotation;
+
+ public PrivacyIndicatorBounds() {
+ mStaticBounds = new Rect[4];
+ mRotation = ROTATION_0;
+ }
+
+ public PrivacyIndicatorBounds(@NonNull Rect[] staticBounds, @Surface.Rotation int rotation) {
+ mStaticBounds = staticBounds;
+ mRotation = rotation;
+ }
+
+ /**
+ * Return a PrivacyIndicatorBounds with updated static bounds
+ */
+ public PrivacyIndicatorBounds updateStaticBounds(@NonNull Rect[] staticPositions) {
+ return new PrivacyIndicatorBounds(staticPositions, mRotation);
+ }
+
+ /**
+ * Update the bounds of the indicator for a specific rotation, or ROTATION_0, if the provided
+ * rotation integer isn't found
+ */
+ public PrivacyIndicatorBounds updateBoundsForRotation(@Nullable Rect bounds,
+ @Surface.Rotation int rotation) {
+ if (rotation >= mStaticBounds.length || rotation < 0) {
+ return this;
+ }
+ Rect[] newBounds = ArrayUtils.cloneOrNull(mStaticBounds);
+ newBounds[rotation] = bounds;
+ return updateStaticBounds(newBounds);
+ }
+
+ /**
+ * Return an inset PrivacyIndicatorBounds
+ */
+ public PrivacyIndicatorBounds inset(int insetLeft, int insetTop, int insetRight,
+ int insetBottom) {
+ if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) {
+ return this;
+ }
+ Rect[] insetStaticBounds = new Rect[mStaticBounds.length];
+ for (int i = 0; i < mStaticBounds.length; i++) {
+ insetStaticBounds[i] =
+ insetRect(mStaticBounds[i], insetLeft, insetTop, insetRight, insetBottom);
+ }
+ return updateStaticBounds(insetStaticBounds);
+ }
+
+ private static Rect insetRect(Rect orig, int insetLeft, int insetTop, int insetRight,
+ int insetBottom) {
+ if (orig == null) {
+ return null;
+ }
+ int left = Math.max(0, orig.left - insetLeft);
+ int top = Math.max(0, orig.top - insetTop);
+ int right = Math.max(left, orig.right - insetRight);
+ int bottom = Math.max(top, orig.bottom - insetBottom);
+ return new Rect(left, top, right, bottom);
+ }
+
+ /**
+ * Return a PrivacyIndicatorBounds with the static position rotated.
+ */
+ public PrivacyIndicatorBounds rotate(@Surface.Rotation int rotation) {
+ if (rotation == ROTATION_0) {
+ return this;
+ }
+ return new PrivacyIndicatorBounds(mStaticBounds, rotation);
+ }
+
+ /**
+ * Returns a scaled PrivacyIndicatorBounds
+ */
+ public PrivacyIndicatorBounds scale(float scale) {
+ if (scale == 1f) {
+ return this;
+ }
+
+ Rect[] scaledStaticPos = new Rect[mStaticBounds.length];
+ for (int i = 0; i < mStaticBounds.length; i++) {
+ scaledStaticPos[i] = scaleRect(mStaticBounds[i], scale);
+ }
+ return new PrivacyIndicatorBounds(scaledStaticPos, mRotation);
+ }
+
+ private static Rect scaleRect(Rect orig, float scale) {
+ if (orig == null) {
+ return null;
+ }
+
+ Rect newRect = new Rect(orig);
+ newRect.scale(scale);
+ return newRect;
+ }
+
+ public Rect getStaticPrivacyIndicatorBounds() {
+ return mStaticBounds[mRotation];
+ }
+
+ @Override
+ public String toString() {
+ return "PrivacyIndicatorBounds {static bounds=" + getStaticPrivacyIndicatorBounds()
+ + " rotation=" + mRotation + "}";
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/PrivacyIndicatorBounds.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(PrivacyIndicatorBounds other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ PrivacyIndicatorBounds that = (PrivacyIndicatorBounds) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Arrays.equals(mStaticBounds, that.mStaticBounds)
+ && mRotation == that.mRotation;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Arrays.hashCode(mStaticBounds);
+ _hash = 31 * _hash + mRotation;
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedArray(mStaticBounds, flags);
+ dest.writeInt(mRotation);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ protected PrivacyIndicatorBounds(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ Rect[] staticBounds = (Rect[]) in.createTypedArray(Rect.CREATOR);
+ int rotation = in.readInt();
+
+ this.mStaticBounds = staticBounds;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mStaticBounds);
+ this.mRotation = rotation;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<PrivacyIndicatorBounds> CREATOR
+ = new Parcelable.Creator<PrivacyIndicatorBounds>() {
+ @Override
+ public PrivacyIndicatorBounds[] newArray(int size) {
+ return new PrivacyIndicatorBounds[size];
+ }
+
+ @Override
+ public PrivacyIndicatorBounds createFromParcel(@NonNull android.os.Parcel in) {
+ return new PrivacyIndicatorBounds(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1621526273838L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/PrivacyIndicatorBounds.java",
+ inputSignatures = "private final @android.annotation.NonNull android.graphics.Rect[] mStaticBounds\nprivate final int mRotation\npublic android.view.PrivacyIndicatorBounds updateStaticBounds(android.graphics.Rect[])\npublic android.view.PrivacyIndicatorBounds updateBoundsForRotation(android.graphics.Rect,int)\npublic android.view.PrivacyIndicatorBounds inset(int,int,int,int)\nprivate static android.graphics.Rect insetRect(android.graphics.Rect,int,int,int,int)\npublic android.view.PrivacyIndicatorBounds rotate(int)\npublic android.view.PrivacyIndicatorBounds scale(float)\nprivate static android.graphics.Rect scaleRect(android.graphics.Rect,float)\npublic android.graphics.Rect getStaticPrivacyIndicatorBounds()\npublic @java.lang.Override java.lang.String toString()\nclass PrivacyIndicatorBounds extends java.lang.Object implements [android.os.Parcelable]\[email protected](genEqualsHashCode=true, genConstructor=false, genAidl=false, genGetters=false)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/RectShadowPainter.java b/android/view/RectShadowPainter.java
new file mode 100644
index 0000000..88771a7
--- /dev/null
+++ b/android/view/RectShadowPainter.java
@@ -0,0 +1,205 @@
+/*
+ * 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.view;
+
+import com.android.layoutlib.bridge.impl.GcSnapshot;
+import com.android.layoutlib.bridge.impl.ResourceHelper;
+
+import android.graphics.BaseCanvas_Delegate;
+import android.graphics.Canvas;
+import android.graphics.Canvas_Delegate;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.Path.FillType;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region.Op;
+import android.graphics.Shader.TileMode;
+
+import java.awt.Rectangle;
+
+/**
+ * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly,
+ * since it modifies the size of the content, that we can't do.
+ */
+public class RectShadowPainter {
+
+
+ private static final int START_COLOR = ResourceHelper.getColor("#37000000");
+ private static final int END_COLOR = ResourceHelper.getColor("#03000000");
+ private static final float PERPENDICULAR_ANGLE = 90f;
+
+ public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas,
+ float alpha) {
+ Rect outline = new Rect();
+ if (!viewOutline.getRect(outline)) {
+ assert false : "Outline is not a rect shadow";
+ return;
+ }
+
+ // TODO replacing the algorithm here to create better shadow
+
+ float shadowSize = elevationToShadow(elevation);
+ int saved = modifyCanvas(canvas, shadowSize);
+ if (saved == -1) {
+ return;
+ }
+
+ float radius = viewOutline.getRadius();
+ if (radius <= 0) {
+ // We can not paint a shadow with radius 0
+ return;
+ }
+
+ try {
+ Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
+ cornerPaint.setStyle(Style.FILL);
+ Paint edgePaint = new Paint(cornerPaint);
+ edgePaint.setAntiAlias(false);
+ float outerArcRadius = radius + shadowSize;
+ int[] colors = {START_COLOR, START_COLOR, END_COLOR};
+ if (alpha != 1f) {
+ // Correct colors using the given component alpha
+ for (int i = 0; i < colors.length; i++) {
+ colors[i] = Color.argb((int) (Color.alpha(colors[i]) * alpha), Color.red(colors[i]),
+ Color.green(colors[i]), Color.blue(colors[i]));
+ }
+ }
+ cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors,
+ new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP));
+ edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, colors[0], colors[2],
+ TileMode.CLAMP));
+ Path path = new Path();
+ path.setFillType(FillType.EVEN_ODD);
+ // A rectangle bounding the complete shadow.
+ RectF shadowRect = new RectF(outline);
+ shadowRect.inset(-shadowSize, -shadowSize);
+ // A rectangle with edges corresponding to the straight edges of the outline.
+ RectF inset = new RectF(outline);
+ inset.inset(radius, radius);
+ // A rectangle used to represent the edge shadow.
+ RectF edgeShadowRect = new RectF();
+
+
+ // left and right sides.
+ edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height());
+ // Left shadow
+ sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0);
+ // Right shadow
+ sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2);
+ // Top shadow
+ edgeShadowRect.set(-shadowSize, 0, 0, inset.width());
+ sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1);
+ // bottom shadow. This needs an inset so that blank doesn't appear when the content is
+ // moved up.
+ edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width());
+ edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0,
+ colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP));
+ sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3);
+
+ // Draw corners.
+ drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0);
+ drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1);
+ drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2);
+ drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3);
+ } finally {
+ canvas.restoreToCount(saved);
+ }
+ }
+
+ private static float elevationToShadow(float elevation) {
+ // The factor is chosen by eyeballing the shadow size on device and preview.
+ return elevation * 0.5f;
+ }
+
+ /**
+ * Translate canvas by half of shadow size up, so that it appears that light is coming
+ * slightly from above. Also, remove clipping, so that shadow is not clipped.
+ */
+ private static int modifyCanvas(Canvas canvas, float shadowSize) {
+ Rect clipBounds = canvas.getClipBounds();
+ if (clipBounds.isEmpty()) {
+ return -1;
+ }
+ int saved = canvas.save();
+ // Usually canvas has been translated to the top left corner of the view when this is
+ // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow.
+ // Thus, we just expand in each direction by width and height of the canvas, while staying
+ // inside the original drawing region.
+ GcSnapshot snapshot = Canvas_Delegate.getDelegate(canvas).getSnapshot();
+ Rectangle originalClip = snapshot.getOriginalClip();
+ if (originalClip != null) {
+ canvas.clipRect(originalClip.x, originalClip.y, originalClip.x + originalClip.width,
+ originalClip.y + originalClip.height, Op.REPLACE);
+ canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(),
+ canvas.getHeight(), Op.INTERSECT);
+ }
+ canvas.translate(0, shadowSize / 2f);
+ return saved;
+ }
+
+ private static void sideShadow(Canvas canvas, Paint edgePaint,
+ RectF edgeShadowRect, float dx, float dy, int rotations) {
+ if (isRectEmpty(edgeShadowRect)) {
+ return;
+ }
+ int saved = canvas.save();
+ canvas.translate(dx, dy);
+ canvas.rotate(rotations * PERPENDICULAR_ANGLE);
+ canvas.drawRect(edgeShadowRect, edgePaint);
+ canvas.restoreToCount(saved);
+ }
+
+ /**
+ * @param canvas Canvas to draw the rectangle on.
+ * @param paint Paint to use when drawing the corner.
+ * @param path A path to reuse. Prevents allocating memory for each path.
+ * @param x Center of circle, which this corner is a part of.
+ * @param y Center of circle, which this corner is a part of.
+ * @param radius radius of the arc
+ * @param rotations number of quarter rotations before starting to paint the arc.
+ */
+ private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y,
+ float radius, int rotations) {
+ int saved = canvas.save();
+ canvas.translate(x, y);
+ path.reset();
+ path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE,
+ PERPENDICULAR_ANGLE, false);
+ path.lineTo(0, 0);
+ path.close();
+ canvas.drawPath(path, paint);
+ canvas.restoreToCount(saved);
+ }
+
+ /**
+ * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks.
+ * <p/>
+ * This is required because {@link BaseCanvas_Delegate#native_drawRect(long, float, float,
+ * float,
+ * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up
+ * drawing empty rectangles, which results in IllegalArgumentException.
+ */
+ private static boolean isRectEmpty(RectF rect) {
+ return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom;
+ }
+}
diff --git a/android/view/RemotableViewMethod.java b/android/view/RemotableViewMethod.java
new file mode 100644
index 0000000..5eff848
--- /dev/null
+++ b/android/view/RemotableViewMethod.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.TestApi;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @hide
+ * This annotation indicates that a method on a subclass of View
+ * is alllowed to be used with the {@link android.widget.RemoteViews} mechanism.
+ */
+@TestApi
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RemotableViewMethod {
+ /**
+ * @return Method name which can be called on a background thread. It should have the
+ * same arguments as the original method and should return a {@link Runnable} (or null)
+ * which will be called on the UI thread.
+ */
+ String asyncImpl() default "";
+}
diff --git a/android/view/RemoteAccessibilityController.java b/android/view/RemoteAccessibilityController.java
new file mode 100644
index 0000000..bc0fab1
--- /dev/null
+++ b/android/view/RemoteAccessibilityController.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Matrix;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.accessibility.IAccessibilityEmbeddedConnection;
+
+class RemoteAccessibilityController {
+ private static final String TAG = "RemoteAccessibilityController";
+ private int mHostId;
+ private RemoteAccessibilityEmbeddedConnection mConnectionWrapper;
+ private Matrix mScreenMatrixForEmbeddedHierarchy = new Matrix();
+ private final float[] mMatrixValues = new float[9];
+ private View mHostView;
+
+ RemoteAccessibilityController(View v) {
+ mHostView = v;
+ }
+
+ private void runOnUiThread(Runnable runnable) {
+ final Handler h = mHostView.getHandler();
+ if (h != null && h.getLooper() != Looper.myLooper()) {
+ h.post(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
+ void assosciateHierarchy(IAccessibilityEmbeddedConnection connection,
+ IBinder leashToken, int hostId) {
+ mHostId = hostId;
+
+ try {
+ leashToken = connection.associateEmbeddedHierarchy(
+ leashToken, mHostId);
+ setRemoteAccessibilityEmbeddedConnection(connection, leashToken);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Error in associateEmbeddedHierarchy " + e);
+ }
+ }
+
+ void disassosciateHierarchy() {
+ setRemoteAccessibilityEmbeddedConnection(null, null);
+ }
+
+ boolean alreadyAssociated(IAccessibilityEmbeddedConnection connection) {
+ if (mConnectionWrapper == null) {
+ return false;
+ }
+ return mConnectionWrapper.mConnection.equals(connection);
+ }
+
+ boolean connected() {
+ return mConnectionWrapper != null;
+ }
+
+ IBinder getLeashToken() {
+ return mConnectionWrapper.getLeashToken();
+ }
+
+ /**
+ * Wrapper of accessibility embedded connection for embedded view hierarchy.
+ */
+ private final class RemoteAccessibilityEmbeddedConnection implements IBinder.DeathRecipient {
+ private final IAccessibilityEmbeddedConnection mConnection;
+ private final IBinder mLeashToken;
+
+ RemoteAccessibilityEmbeddedConnection(IAccessibilityEmbeddedConnection connection,
+ IBinder leashToken) {
+ mConnection = connection;
+ mLeashToken = leashToken;
+ }
+
+ IAccessibilityEmbeddedConnection getConnection() {
+ return mConnection;
+ }
+
+ IBinder getLeashToken() {
+ return mLeashToken;
+ }
+
+ void linkToDeath() throws RemoteException {
+ mConnection.asBinder().linkToDeath(this, 0);
+ }
+
+ void unlinkToDeath() {
+ mConnection.asBinder().unlinkToDeath(this, 0);
+ }
+
+ @Override
+ public void binderDied() {
+ unlinkToDeath();
+ runOnUiThread(() -> {
+ if (mConnectionWrapper == this) {
+ mConnectionWrapper = null;
+ }
+ });
+ }
+ }
+
+ private void setRemoteAccessibilityEmbeddedConnection(
+ IAccessibilityEmbeddedConnection connection, IBinder leashToken) {
+ try {
+ if (mConnectionWrapper != null) {
+ mConnectionWrapper.getConnection()
+ .disassociateEmbeddedHierarchy();
+ mConnectionWrapper.unlinkToDeath();
+ mConnectionWrapper = null;
+ }
+ if (connection != null && leashToken != null) {
+ mConnectionWrapper =
+ new RemoteAccessibilityEmbeddedConnection(connection, leashToken);
+ mConnectionWrapper.linkToDeath();
+ }
+ } catch (RemoteException e) {
+ Log.d(TAG, "Error while setRemoteEmbeddedConnection " + e);
+ }
+ }
+
+ private RemoteAccessibilityEmbeddedConnection getRemoteAccessibilityEmbeddedConnection() {
+ return mConnectionWrapper;
+ }
+
+ void setScreenMatrix(Matrix m) {
+ // If the screen matrix is identity or doesn't change, do nothing.
+ if (m.isIdentity() || m.equals(mScreenMatrixForEmbeddedHierarchy)) {
+ return;
+ }
+
+ try {
+ final RemoteAccessibilityEmbeddedConnection wrapper =
+ getRemoteAccessibilityEmbeddedConnection();
+ if (wrapper == null) {
+ return;
+ }
+ m.getValues(mMatrixValues);
+ wrapper.getConnection().setScreenMatrix(mMatrixValues);
+ mScreenMatrixForEmbeddedHierarchy.set(m);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Error while setScreenMatrix " + e);
+ }
+ }
+
+
+
+
+
+
+}
diff --git a/android/view/RemoteAnimationAdapter.java b/android/view/RemoteAnimationAdapter.java
new file mode 100644
index 0000000..a78036f
--- /dev/null
+++ b/android/view/RemoteAnimationAdapter.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import android.app.ActivityOptions;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Object that describes how to run a remote animation.
+ * <p>
+ * A remote animation lets another app control the entire app transition. It does so by
+ * <ul>
+ * <li>using {@link ActivityOptions#makeRemoteAnimation}</li>
+ * <li>using {@link IWindowManager#overridePendingAppTransitionRemote}</li>
+ * </ul>
+ * to register a {@link RemoteAnimationAdapter} that describes how the animation should be run:
+ * Along some meta-data, this object contains a callback that gets invoked from window manager when
+ * the transition is ready to be started.
+ * <p>
+ * Window manager supplies a list of {@link RemoteAnimationTarget}s into the callback. Each target
+ * contains information about the activity that is animating as well as
+ * {@link RemoteAnimationTarget#leash}. The controlling app can modify the leash like any other
+ * {@link SurfaceControl}, including the possibility to synchronize updating the leash's surface
+ * properties with a frame to be drawn using
+ * {@link SurfaceControl.Transaction#deferTransactionUntil}.
+ * <p>
+ * When the animation is done, the controlling app can invoke
+ * {@link IRemoteAnimationFinishedCallback} that gets supplied into
+ * {@link IRemoteAnimationRunner#onStartAnimation}
+ *
+ * @hide
+ */
+public class RemoteAnimationAdapter implements Parcelable {
+
+ private final IRemoteAnimationRunner mRunner;
+ private final long mDuration;
+ private final long mStatusBarTransitionDelay;
+ private final boolean mChangeNeedsSnapshot;
+
+ /** @see #getCallingPid */
+ private int mCallingPid;
+ private int mCallingUid;
+
+ /**
+ * @param runner The interface that gets notified when we actually need to start the animation.
+ * @param duration The duration of the animation.
+ * @param changeNeedsSnapshot For change transitions, whether this should create a snapshot by
+ * screenshotting the task.
+ * @param statusBarTransitionDelay The desired delay for all visual animations in the
+ * status bar caused by this app animation in millis.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public RemoteAnimationAdapter(IRemoteAnimationRunner runner, long duration,
+ long statusBarTransitionDelay, boolean changeNeedsSnapshot) {
+ mRunner = runner;
+ mDuration = duration;
+ mChangeNeedsSnapshot = changeNeedsSnapshot;
+ mStatusBarTransitionDelay = statusBarTransitionDelay;
+ }
+
+ @UnsupportedAppUsage
+ public RemoteAnimationAdapter(IRemoteAnimationRunner runner, long duration,
+ long statusBarTransitionDelay) {
+ this(runner, duration, statusBarTransitionDelay, false /* changeNeedsSnapshot */);
+ }
+
+ public RemoteAnimationAdapter(Parcel in) {
+ mRunner = IRemoteAnimationRunner.Stub.asInterface(in.readStrongBinder());
+ mDuration = in.readLong();
+ mStatusBarTransitionDelay = in.readLong();
+ mChangeNeedsSnapshot = in.readBoolean();
+ }
+
+ public IRemoteAnimationRunner getRunner() {
+ return mRunner;
+ }
+
+ public long getDuration() {
+ return mDuration;
+ }
+
+ public long getStatusBarTransitionDelay() {
+ return mStatusBarTransitionDelay;
+ }
+
+ public boolean getChangeNeedsSnapshot() {
+ return mChangeNeedsSnapshot;
+ }
+
+ /**
+ * To be called by system_server to keep track which pid and uid is running this animation.
+ */
+ public void setCallingPidUid(int pid, int uid) {
+ mCallingPid = pid;
+ mCallingUid = uid;
+ }
+
+ /**
+ * @return The pid of the process running the animation.
+ */
+ public int getCallingPid() {
+ return mCallingPid;
+ }
+
+ /**
+ * @return The uid of the process running the animation.
+ */
+ public int getCallingUid() {
+ return mCallingUid;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongInterface(mRunner);
+ dest.writeLong(mDuration);
+ dest.writeLong(mStatusBarTransitionDelay);
+ dest.writeBoolean(mChangeNeedsSnapshot);
+ }
+
+ public static final @android.annotation.NonNull Creator<RemoteAnimationAdapter> CREATOR
+ = new Creator<RemoteAnimationAdapter>() {
+ public RemoteAnimationAdapter createFromParcel(Parcel in) {
+ return new RemoteAnimationAdapter(in);
+ }
+
+ public RemoteAnimationAdapter[] newArray(int size) {
+ return new RemoteAnimationAdapter[size];
+ }
+ };
+}
diff --git a/android/view/RemoteAnimationDefinition.java b/android/view/RemoteAnimationDefinition.java
new file mode 100644
index 0000000..ea97995
--- /dev/null
+++ b/android/view/RemoteAnimationDefinition.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+
+import android.annotation.Nullable;
+import android.app.WindowConfiguration.ActivityType;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.WindowManager.TransitionOldType;
+
+/**
+ * Defines which animation types should be overridden by which remote animation.
+ *
+ * @hide
+ */
+public class RemoteAnimationDefinition implements Parcelable {
+
+ private final SparseArray<RemoteAnimationAdapterEntry> mTransitionAnimationMap;
+
+ @UnsupportedAppUsage
+ public RemoteAnimationDefinition() {
+ mTransitionAnimationMap = new SparseArray<>();
+ }
+
+ /**
+ * Registers a remote animation for a specific transition.
+ *
+ * @param transition The old transition type. Must be one of WindowManager.TRANSIT_OLD_* values.
+ * @param activityTypeFilter The remote animation only runs if an activity with type of this
+ * parameter is involved in the transition.
+ * @param adapter The adapter that described how to run the remote animation.
+ */
+ @UnsupportedAppUsage
+ public void addRemoteAnimation(@TransitionOldType int transition,
+ @ActivityType int activityTypeFilter, RemoteAnimationAdapter adapter) {
+ mTransitionAnimationMap.put(transition,
+ new RemoteAnimationAdapterEntry(adapter, activityTypeFilter));
+ }
+
+ /**
+ * Registers a remote animation for a specific transition without defining an activity type
+ * filter.
+ *
+ * @param transition The old transition type. Must be one of WindowManager.TRANSIT_OLD_* values.
+ * @param adapter The adapter that described how to run the remote animation.
+ */
+ @UnsupportedAppUsage
+ public void addRemoteAnimation(@TransitionOldType int transition,
+ RemoteAnimationAdapter adapter) {
+ addRemoteAnimation(transition, ACTIVITY_TYPE_UNDEFINED, adapter);
+ }
+
+ /**
+ * Checks whether a remote animation for specific transition is defined.
+ *
+ * @param transition The old transition type. Must be one of WindowManager.TRANSIT_OLD_* values.
+ * @param activityTypes The set of activity types of activities that are involved in the
+ * transition. Will be used for filtering.
+ * @return Whether this definition has defined a remote animation for the specified transition.
+ */
+ public boolean hasTransition(@TransitionOldType int transition,
+ ArraySet<Integer> activityTypes) {
+ return getAdapter(transition, activityTypes) != null;
+ }
+
+ /**
+ * Retrieves the remote animation for a specific transition.
+ *
+ * @param transition The old transition type. Must be one of WindowManager.TRANSIT_OLD_* values.
+ * @param activityTypes The set of activity types of activities that are involved in the
+ * transition. Will be used for filtering.
+ * @return The remote animation adapter for the specified transition.
+ */
+ public @Nullable RemoteAnimationAdapter getAdapter(@TransitionOldType int transition,
+ ArraySet<Integer> activityTypes) {
+ final RemoteAnimationAdapterEntry entry = mTransitionAnimationMap.get(transition);
+ if (entry == null) {
+ return null;
+ }
+ if (entry.activityTypeFilter == ACTIVITY_TYPE_UNDEFINED
+ || activityTypes.contains(entry.activityTypeFilter)) {
+ return entry.adapter;
+ } else {
+ return null;
+ }
+ }
+
+ public RemoteAnimationDefinition(Parcel in) {
+ final int size = in.readInt();
+ mTransitionAnimationMap = new SparseArray<>(size);
+ for (int i = 0; i < size; i++) {
+ final int transition = in.readInt();
+ final RemoteAnimationAdapterEntry entry = in.readTypedObject(
+ RemoteAnimationAdapterEntry.CREATOR);
+ mTransitionAnimationMap.put(transition, entry);
+ }
+ }
+
+ /**
+ * To be called by system_server to keep track which pid is running the remote animations inside
+ * this definition.
+ */
+ public void setCallingPidUid(int pid, int uid) {
+ for (int i = mTransitionAnimationMap.size() - 1; i >= 0; i--) {
+ mTransitionAnimationMap.valueAt(i).adapter.setCallingPidUid(pid, uid);
+ }
+ }
+
+ /**
+ * Links the death of the runner to the provided death recipient.
+ */
+ public void linkToDeath(IBinder.DeathRecipient deathRecipient) {
+ try {
+ for (int i = 0; i < mTransitionAnimationMap.size(); i++) {
+ mTransitionAnimationMap.valueAt(i).adapter.getRunner().asBinder()
+ .linkToDeath(deathRecipient, 0 /* flags */);
+ }
+ } catch (RemoteException e) {
+ Slog.e("RemoteAnimationDefinition", "Failed to link to death recipient");
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ final int size = mTransitionAnimationMap.size();
+ dest.writeInt(size);
+ for (int i = 0; i < size; i++) {
+ dest.writeInt(mTransitionAnimationMap.keyAt(i));
+ dest.writeTypedObject(mTransitionAnimationMap.valueAt(i), flags);
+ }
+ }
+
+ public static final @android.annotation.NonNull Creator<RemoteAnimationDefinition> CREATOR =
+ new Creator<RemoteAnimationDefinition>() {
+ public RemoteAnimationDefinition createFromParcel(Parcel in) {
+ return new RemoteAnimationDefinition(in);
+ }
+
+ public RemoteAnimationDefinition[] newArray(int size) {
+ return new RemoteAnimationDefinition[size];
+ }
+ };
+
+ private static class RemoteAnimationAdapterEntry implements Parcelable {
+
+ final RemoteAnimationAdapter adapter;
+
+ /**
+ * Only run the transition if one of the activities matches the filter.
+ * {@link WindowConfiguration.ACTIVITY_TYPE_UNDEFINED} means no filter
+ */
+ @ActivityType final int activityTypeFilter;
+
+ RemoteAnimationAdapterEntry(RemoteAnimationAdapter adapter, int activityTypeFilter) {
+ this.adapter = adapter;
+ this.activityTypeFilter = activityTypeFilter;
+ }
+
+ private RemoteAnimationAdapterEntry(Parcel in) {
+ adapter = in.readTypedObject(RemoteAnimationAdapter.CREATOR);
+ activityTypeFilter = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedObject(adapter, flags);
+ dest.writeInt(activityTypeFilter);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private static final @android.annotation.NonNull Creator<RemoteAnimationAdapterEntry> CREATOR
+ = new Creator<RemoteAnimationAdapterEntry>() {
+
+ @Override
+ public RemoteAnimationAdapterEntry createFromParcel(Parcel in) {
+ return new RemoteAnimationAdapterEntry(in);
+ }
+
+ @Override
+ public RemoteAnimationAdapterEntry[] newArray(int size) {
+ return new RemoteAnimationAdapterEntry[size];
+ }
+ };
+ }
+}
diff --git a/android/view/RemoteAnimationTarget.java b/android/view/RemoteAnimationTarget.java
new file mode 100644
index 0000000..cdc099b
--- /dev/null
+++ b/android/view/RemoteAnimationTarget.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import static android.graphics.GraphicsProtos.dumpPointProto;
+import static android.view.RemoteAnimationTargetProto.CLIP_RECT;
+import static android.view.RemoteAnimationTargetProto.CONTENT_INSETS;
+import static android.view.RemoteAnimationTargetProto.IS_TRANSLUCENT;
+import static android.view.RemoteAnimationTargetProto.LEASH;
+import static android.view.RemoteAnimationTargetProto.LOCAL_BOUNDS;
+import static android.view.RemoteAnimationTargetProto.MODE;
+import static android.view.RemoteAnimationTargetProto.POSITION;
+import static android.view.RemoteAnimationTargetProto.PREFIX_ORDER_INDEX;
+import static android.view.RemoteAnimationTargetProto.SCREEN_SPACE_BOUNDS;
+import static android.view.RemoteAnimationTargetProto.SOURCE_CONTAINER_BOUNDS;
+import static android.view.RemoteAnimationTargetProto.START_BOUNDS;
+import static android.view.RemoteAnimationTargetProto.START_LEASH;
+import static android.view.RemoteAnimationTargetProto.TASK_ID;
+import static android.view.RemoteAnimationTargetProto.WINDOW_CONFIGURATION;
+import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+
+import android.annotation.IntDef;
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.app.WindowConfiguration;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Describes an activity to be animated as part of a remote animation.
+ *
+ * @hide
+ */
+public class RemoteAnimationTarget implements Parcelable {
+
+ /**
+ * The app is in the set of opening apps of this transition.
+ */
+ public static final int MODE_OPENING = 0;
+
+ /**
+ * The app is in the set of closing apps of this transition.
+ */
+ public static final int MODE_CLOSING = 1;
+
+ /**
+ * The app is in the set of resizing apps (eg. mode change) of this transition.
+ */
+ public static final int MODE_CHANGING = 2;
+
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_OPENING,
+ MODE_CLOSING,
+ MODE_CHANGING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {}
+
+ /**
+ * The {@link Mode} to describe whether this app is opening or closing.
+ */
+ @UnsupportedAppUsage
+ public final @Mode int mode;
+
+ /**
+ * The id of the task this app belongs to.
+ */
+ @UnsupportedAppUsage
+ public final int taskId;
+
+ /**
+ * The {@link SurfaceControl} object to actually control the transform of the app.
+ */
+ @UnsupportedAppUsage
+ public final SurfaceControl leash;
+
+ /**
+ * The {@link SurfaceControl} for the starting state of a target if this transition is
+ * MODE_CHANGING, {@code null)} otherwise. This is relative to the app window.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final SurfaceControl startLeash;
+
+ /**
+ * Whether the app is translucent and may reveal apps behind.
+ */
+ @UnsupportedAppUsage
+ public final boolean isTranslucent;
+
+ /**
+ * The clip rect window manager applies when clipping the app's main surface in screen space
+ * coordinates. This is just a hint to the animation runner: If running a clip-rect animation,
+ * anything that extends beyond these bounds will not have any effect. This implies that any
+ * clip-rect animation should likely stop at these bounds.
+ */
+ @UnsupportedAppUsage
+ public final Rect clipRect;
+
+ /**
+ * The insets of the main app window.
+ */
+ @UnsupportedAppUsage
+ public final Rect contentInsets;
+
+ /**
+ * The index of the element in the tree in prefix order. This should be used for z-layering
+ * to preserve original z-layer order in the hierarchy tree assuming no "boosting" needs to
+ * happen.
+ */
+ @UnsupportedAppUsage
+ public final int prefixOrderIndex;
+
+ /**
+ * The source position of the app, in screen spaces coordinates. If the position of the leash
+ * is modified from the controlling app, any animation transform needs to be offset by this
+ * amount.
+ * @deprecated Use {@link #localBounds} instead.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public final Point position;
+
+ /**
+ * Bounds of the target relative to its parent.
+ * When the app target animating on its parent, we need to use the local coordinates relative to
+ * its parent with {@code localBounds.left} & {@code localBounds.top} rather than using
+ * {@code position} in screen coordinates.
+ */
+ public final Rect localBounds;
+
+ /**
+ * The bounds of the source container the app lives in, in screen space coordinates. If the crop
+ * of the leash is modified from the controlling app, it needs to take the source container
+ * bounds into account when calculating the crop.
+ * @deprecated Renamed to {@link #screenSpaceBounds}
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public final Rect sourceContainerBounds;
+
+ /**
+ * Bounds of the target relative to the screen. If the crop of the leash is modified from the
+ * controlling app, it needs to take the screen space bounds into account when calculating the
+ * crop.
+ */
+ public final Rect screenSpaceBounds;
+
+ /**
+ * The starting bounds of the source container in screen space coordinates. This is {@code null}
+ * if the animation target isn't MODE_CHANGING. Since this is the starting bounds, it's size
+ * should be equivalent to the size of the starting thumbnail. Note that sourceContainerBounds
+ * is the end bounds of a change transition.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final Rect startBounds;
+
+ /**
+ * The window configuration for the target.
+ */
+ @UnsupportedAppUsage
+ public final WindowConfiguration windowConfiguration;
+
+ /**
+ * Whether the task is not presented in Recents UI.
+ */
+ @UnsupportedAppUsage
+ public boolean isNotInRecents;
+
+ /**
+ * {@link TaskInfo} to allow the controller to identify information about the task.
+ *
+ * TODO: add this to proto dump
+ */
+ public ActivityManager.RunningTaskInfo taskInfo;
+
+ /**
+ * The {@link android.view.WindowManager.LayoutParams.WindowType} of this window. It's only used
+ * for non-app window.
+ */
+ public final @WindowManager.LayoutParams.WindowType int windowType;
+
+ public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
+ Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position,
+ Rect localBounds, Rect screenSpaceBounds,
+ WindowConfiguration windowConfig, boolean isNotInRecents,
+ SurfaceControl startLeash, Rect startBounds, ActivityManager.RunningTaskInfo taskInfo) {
+ this(taskId, mode, leash, isTranslucent, clipRect, contentInsets, prefixOrderIndex,
+ position, localBounds, screenSpaceBounds, windowConfig, isNotInRecents, startLeash,
+ startBounds, taskInfo, INVALID_WINDOW_TYPE);
+ }
+
+ public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
+ Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position,
+ Rect localBounds, Rect screenSpaceBounds,
+ WindowConfiguration windowConfig, boolean isNotInRecents,
+ SurfaceControl startLeash, Rect startBounds,
+ ActivityManager.RunningTaskInfo taskInfo,
+ @WindowManager.LayoutParams.WindowType int windowType) {
+ this.mode = mode;
+ this.taskId = taskId;
+ this.leash = leash;
+ this.isTranslucent = isTranslucent;
+ this.clipRect = new Rect(clipRect);
+ this.contentInsets = new Rect(contentInsets);
+ this.prefixOrderIndex = prefixOrderIndex;
+ this.position = new Point(position);
+ this.localBounds = new Rect(localBounds);
+ this.sourceContainerBounds = new Rect(screenSpaceBounds);
+ this.screenSpaceBounds = new Rect(screenSpaceBounds);
+ this.windowConfiguration = windowConfig;
+ this.isNotInRecents = isNotInRecents;
+ this.startLeash = startLeash;
+ this.startBounds = startBounds == null ? null : new Rect(startBounds);
+ this.taskInfo = taskInfo;
+ this.windowType = windowType;
+ }
+
+ public RemoteAnimationTarget(Parcel in) {
+ taskId = in.readInt();
+ mode = in.readInt();
+ leash = in.readTypedObject(SurfaceControl.CREATOR);
+ isTranslucent = in.readBoolean();
+ clipRect = in.readTypedObject(Rect.CREATOR);
+ contentInsets = in.readTypedObject(Rect.CREATOR);
+ prefixOrderIndex = in.readInt();
+ position = in.readTypedObject(Point.CREATOR);
+ localBounds = in.readTypedObject(Rect.CREATOR);
+ sourceContainerBounds = in.readTypedObject(Rect.CREATOR);
+ screenSpaceBounds = in.readTypedObject(Rect.CREATOR);
+ windowConfiguration = in.readTypedObject(WindowConfiguration.CREATOR);
+ isNotInRecents = in.readBoolean();
+ startLeash = in.readTypedObject(SurfaceControl.CREATOR);
+ startBounds = in.readTypedObject(Rect.CREATOR);
+ taskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR);
+ windowType = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(taskId);
+ dest.writeInt(mode);
+ dest.writeTypedObject(leash, 0 /* flags */);
+ dest.writeBoolean(isTranslucent);
+ dest.writeTypedObject(clipRect, 0 /* flags */);
+ dest.writeTypedObject(contentInsets, 0 /* flags */);
+ dest.writeInt(prefixOrderIndex);
+ dest.writeTypedObject(position, 0 /* flags */);
+ dest.writeTypedObject(localBounds, 0 /* flags */);
+ dest.writeTypedObject(sourceContainerBounds, 0 /* flags */);
+ dest.writeTypedObject(screenSpaceBounds, 0 /* flags */);
+ dest.writeTypedObject(windowConfiguration, 0 /* flags */);
+ dest.writeBoolean(isNotInRecents);
+ dest.writeTypedObject(startLeash, 0 /* flags */);
+ dest.writeTypedObject(startBounds, 0 /* flags */);
+ dest.writeTypedObject(taskInfo, 0 /* flags */);
+ dest.writeInt(windowType);
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix); pw.print("mode="); pw.print(mode);
+ pw.print(" taskId="); pw.print(taskId);
+ pw.print(" isTranslucent="); pw.print(isTranslucent);
+ pw.print(" clipRect="); clipRect.printShortString(pw);
+ pw.print(" contentInsets="); contentInsets.printShortString(pw);
+ pw.print(" prefixOrderIndex="); pw.print(prefixOrderIndex);
+ pw.print(" position="); printPoint(position, pw);
+ pw.print(" sourceContainerBounds="); sourceContainerBounds.printShortString(pw);
+ pw.print(" screenSpaceBounds="); screenSpaceBounds.printShortString(pw);
+ pw.print(" localBounds="); localBounds.printShortString(pw);
+ pw.println();
+ pw.print(prefix); pw.print("windowConfiguration="); pw.println(windowConfiguration);
+ pw.print(prefix); pw.print("leash="); pw.println(leash);
+ pw.print(prefix); pw.print("taskInfo="); pw.println(taskInfo);
+ pw.print(prefix); pw.print("windowType="); pw.print(windowType);
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(TASK_ID, taskId);
+ proto.write(MODE, mode);
+ leash.dumpDebug(proto, LEASH);
+ proto.write(IS_TRANSLUCENT, isTranslucent);
+ clipRect.dumpDebug(proto, CLIP_RECT);
+ contentInsets.dumpDebug(proto, CONTENT_INSETS);
+ proto.write(PREFIX_ORDER_INDEX, prefixOrderIndex);
+ dumpPointProto(position, proto, POSITION);
+ sourceContainerBounds.dumpDebug(proto, SOURCE_CONTAINER_BOUNDS);
+ screenSpaceBounds.dumpDebug(proto, SCREEN_SPACE_BOUNDS);
+ localBounds.dumpDebug(proto, LOCAL_BOUNDS);
+ windowConfiguration.dumpDebug(proto, WINDOW_CONFIGURATION);
+ if (startLeash != null) {
+ startLeash.dumpDebug(proto, START_LEASH);
+ }
+ if (startBounds != null) {
+ startBounds.dumpDebug(proto, START_BOUNDS);
+ }
+ proto.end(token);
+ }
+
+ private static void printPoint(Point p, PrintWriter pw) {
+ pw.print("["); pw.print(p.x); pw.print(","); pw.print(p.y); pw.print("]");
+ }
+
+ public static final @android.annotation.NonNull Creator<RemoteAnimationTarget> CREATOR
+ = new Creator<RemoteAnimationTarget>() {
+ public RemoteAnimationTarget createFromParcel(Parcel in) {
+ return new RemoteAnimationTarget(in);
+ }
+
+ public RemoteAnimationTarget[] newArray(int size) {
+ return new RemoteAnimationTarget[size];
+ }
+ };
+}
diff --git a/android/view/RenderNodeAnimator.java b/android/view/RenderNodeAnimator.java
new file mode 100644
index 0000000..31fdfb7
--- /dev/null
+++ b/android/view/RenderNodeAnimator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+
+/**
+ * @hide
+ */
+public class RenderNodeAnimator extends android.graphics.animation.RenderNodeAnimator
+ implements android.graphics.animation.RenderNodeAnimator.ViewListener {
+
+ private View mViewTarget;
+
+ public RenderNodeAnimator(int property, float finalValue) {
+ super(property, finalValue);
+ }
+
+ public RenderNodeAnimator(CanvasProperty<Float> property, float finalValue) {
+ super(property, finalValue);
+ }
+
+ public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, float finalValue) {
+ super(property, paintField, finalValue);
+ }
+
+ public RenderNodeAnimator(int x, int y, float startRadius, float endRadius) {
+ super(x, y, startRadius, endRadius);
+ }
+
+ @Override
+ public void onAlphaAnimationStart(float finalAlpha) {
+ // Alpha is a special snowflake that has the canonical value stored
+ // in mTransformationInfo instead of in RenderNode, so we need to update
+ // it with the final value here.
+ mViewTarget.ensureTransformationInfo();
+ mViewTarget.setAlphaInternal(finalAlpha);
+ }
+
+ @Override
+ public void invalidateParent(boolean forceRedraw) {
+ mViewTarget.invalidateViewProperty(true, false);
+ }
+
+ /** @hide */
+ public void setTarget(@NonNull View view) {
+ mViewTarget = view;
+ setViewListener(this);
+ setTarget(mViewTarget.mRenderNode);
+ }
+}
diff --git a/android/view/RoundScrollbarRenderer.java b/android/view/RoundScrollbarRenderer.java
new file mode 100644
index 0000000..df9e23e
--- /dev/null
+++ b/android/view/RoundScrollbarRenderer.java
@@ -0,0 +1,130 @@
+/*
+ * 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.view;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+/**
+ * Helper class for drawing round scroll bars on round Wear devices.
+ */
+class RoundScrollbarRenderer {
+ // The range of the scrollbar position represented as an angle in degrees.
+ private static final int SCROLLBAR_ANGLE_RANGE = 90;
+ private static final int MAX_SCROLLBAR_ANGLE_SWIPE = 16;
+ private static final int MIN_SCROLLBAR_ANGLE_SWIPE = 6;
+ private static final float WIDTH_PERCENTAGE = 0.02f;
+ private static final int DEFAULT_THUMB_COLOR = 0xFFE8EAED;
+ private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF;
+
+ private final Paint mThumbPaint = new Paint();
+ private final Paint mTrackPaint = new Paint();
+ private final RectF mRect = new RectF();
+ private final View mParent;
+ private final int mMaskThickness;
+
+ public RoundScrollbarRenderer(View parent) {
+ // Paints for the round scrollbar.
+ // Set up the thumb paint
+ mThumbPaint.setAntiAlias(true);
+ mThumbPaint.setStrokeCap(Paint.Cap.ROUND);
+ mThumbPaint.setStyle(Paint.Style.STROKE);
+
+ // Set up the track paint
+ mTrackPaint.setAntiAlias(true);
+ mTrackPaint.setStrokeCap(Paint.Cap.ROUND);
+ mTrackPaint.setStyle(Paint.Style.STROKE);
+
+ mParent = parent;
+
+ // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
+ // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
+ // that it doesn't get clipped.
+ mMaskThickness = parent.getContext().getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.circular_display_mask_thickness);
+ }
+
+ public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds) {
+ if (alpha == 0) {
+ return;
+ }
+ // Get information about the current scroll state of the parent view.
+ float maxScroll = mParent.computeVerticalScrollRange();
+ float scrollExtent = mParent.computeVerticalScrollExtent();
+ if (scrollExtent <= 0 || maxScroll <= scrollExtent) {
+ return;
+ }
+ float currentScroll = Math.max(0, mParent.computeVerticalScrollOffset());
+ float linearThumbLength = mParent.computeVerticalScrollExtent();
+ float thumbWidth = mParent.getWidth() * WIDTH_PERCENTAGE;
+ mThumbPaint.setStrokeWidth(thumbWidth);
+ mTrackPaint.setStrokeWidth(thumbWidth);
+
+ setThumbColor(applyAlpha(DEFAULT_THUMB_COLOR, alpha));
+ setTrackColor(applyAlpha(DEFAULT_TRACK_COLOR, alpha));
+
+ // Normalize the sweep angle for the scroll bar.
+ float sweepAngle = (linearThumbLength / maxScroll) * SCROLLBAR_ANGLE_RANGE;
+ sweepAngle = clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE);
+ // Normalize the start angle so that it falls on the track.
+ float startAngle = (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle))
+ / (maxScroll - linearThumbLength) - SCROLLBAR_ANGLE_RANGE / 2;
+ startAngle = clamp(startAngle, -SCROLLBAR_ANGLE_RANGE / 2,
+ SCROLLBAR_ANGLE_RANGE / 2 - sweepAngle);
+
+ // Draw the track and the thumb.
+ float inset = thumbWidth / 2 + mMaskThickness;
+ mRect.set(
+ bounds.left + inset,
+ bounds.top + inset,
+ bounds.right - inset,
+ bounds.bottom - inset);
+ canvas.drawArc(mRect, -SCROLLBAR_ANGLE_RANGE / 2, SCROLLBAR_ANGLE_RANGE, false,
+ mTrackPaint);
+ canvas.drawArc(mRect, startAngle, sweepAngle, false, mThumbPaint);
+ }
+
+ private static float clamp(float val, float min, float max) {
+ if (val < min) {
+ return min;
+ } else if (val > max) {
+ return max;
+ } else {
+ return val;
+ }
+ }
+
+ private static int applyAlpha(int color, float alpha) {
+ int alphaByte = (int) (Color.alpha(color) * alpha);
+ return Color.argb(alphaByte, Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ private void setThumbColor(int thumbColor) {
+ if (mThumbPaint.getColor() != thumbColor) {
+ mThumbPaint.setColor(thumbColor);
+ }
+ }
+
+ private void setTrackColor(int trackColor) {
+ if (mTrackPaint.getColor() != trackColor) {
+ mTrackPaint.setColor(trackColor);
+ }
+ }
+}
diff --git a/android/view/RoundedCorner.java b/android/view/RoundedCorner.java
new file mode 100644
index 0000000..d10fc44
--- /dev/null
+++ b/android/view/RoundedCorner.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a rounded corner of the display.
+ * <p>
+ * <img src="{@docRoot}reference/android/images/rounded_corner/rounded-corner-info.png" height="120"
+ * alt="A figure to describe what the rounded corner radius and the center point are. "/>
+ * </p>
+ *
+ * <p>Note: The rounded corner formed by the radius and the center is an approximation.</p>
+ *
+ * <p>{@link RoundedCorner} is immutable.</p>
+ */
+public final class RoundedCorner implements Parcelable {
+
+ /**
+ * The rounded corner is at the top-left of the screen.
+ */
+ public static final int POSITION_TOP_LEFT = 0;
+ /**
+ * The rounded corner is at the top-right of the screen.
+ */
+ public static final int POSITION_TOP_RIGHT = 1;
+ /**
+ * The rounded corner is at the bottom-right of the screen.
+ */
+ public static final int POSITION_BOTTOM_RIGHT = 2;
+ /**
+ * The rounded corner is at the bottom-left of the screen.
+ */
+ public static final int POSITION_BOTTOM_LEFT = 3;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "POSITION_" }, value = {
+ POSITION_TOP_LEFT,
+ POSITION_TOP_RIGHT,
+ POSITION_BOTTOM_RIGHT,
+ POSITION_BOTTOM_LEFT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Position {}
+
+ private final @Position int mPosition;
+ private final int mRadius;
+ @NonNull
+ private final Point mCenter;
+
+ /**
+ * Creates an empty {@link RoundedCorner} on the given position.
+ * @hide
+ */
+ @VisibleForTesting
+ public RoundedCorner(@Position int position) {
+ mPosition = position;
+ mRadius = 0;
+ mCenter = new Point(0, 0);
+ }
+
+ /**
+ * Creates a {@link RoundedCorner}.
+ *
+ * <p>Note that this is only useful for tests. For production code, developers should always
+ * use a {@link RoundedCorner} obtained from the system via
+ * {@link WindowInsets#getRoundedCorner} or {@link Display#getRoundedCorner}.</p>
+ *
+ * @param position the position of the rounded corner.
+ * @param radius the radius of the rounded corner.
+ * @param centerX the x of center point of the rounded corner.
+ * @param centerY the y of center point of the rounded corner.
+ *
+ */
+ public RoundedCorner(@Position int position, int radius, int centerX,
+ int centerY) {
+ mPosition = position;
+ mRadius = radius;
+ mCenter = new Point(centerX, centerY);
+ }
+
+ /**
+ * Creates a {@link RoundedCorner} from a passed in {@link RoundedCorner}.
+ *
+ * @hide
+ */
+ RoundedCorner(RoundedCorner rc) {
+ mPosition = rc.getPosition();
+ mRadius = rc.getRadius();
+ mCenter = new Point(rc.getCenter());
+ }
+
+ /**
+ * Get the position of this {@link RoundedCorner}.
+ *
+ * @see #POSITION_TOP_LEFT
+ * @see #POSITION_TOP_RIGHT
+ * @see #POSITION_BOTTOM_RIGHT
+ * @see #POSITION_BOTTOM_LEFT
+ */
+ public @Position int getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Returns the radius of a quarter circle approximation of this {@link RoundedCorner}.
+ *
+ * @return the rounded corner radius of this {@link RoundedCorner}. Returns 0 if there is no
+ * rounded corner.
+ */
+ public int getRadius() {
+ return mRadius;
+ }
+
+ /**
+ * Returns the circle center of a quarter circle approximation of this {@link RoundedCorner}.
+ *
+ * @return the center point of this {@link RoundedCorner} in the application's coordinate.
+ */
+ @NonNull
+ public Point getCenter() {
+ return new Point(mCenter);
+ }
+
+ /**
+ * Checks whether this {@link RoundedCorner} exists and is inside the application's bounds.
+ *
+ * @return {@code false} if there is a rounded corner and is contained in the application's
+ * bounds. Otherwise return {@code true}.
+ *
+ * @hide
+ */
+ public boolean isEmpty() {
+ return mRadius == 0 || mCenter.x <= 0 || mCenter.y <= 0;
+ }
+
+ private String getPositionString(@Position int position) {
+ switch (position) {
+ case POSITION_TOP_LEFT:
+ return "TopLeft";
+ case POSITION_TOP_RIGHT:
+ return "TopRight";
+ case POSITION_BOTTOM_RIGHT:
+ return "BottomRight";
+ case POSITION_BOTTOM_LEFT:
+ return "BottomLeft";
+ default:
+ return "Invalid";
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof RoundedCorner) {
+ RoundedCorner r = (RoundedCorner) o;
+ return mPosition == r.mPosition && mRadius == r.mRadius
+ && mCenter.equals(r.mCenter);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ result = 31 * result + mPosition;
+ result = 31 * result + mRadius;
+ result = 31 * result + mCenter.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RoundedCorner{"
+ + "position=" + getPositionString(mPosition)
+ + ", radius=" + mRadius
+ + ", center=" + mCenter
+ + '}';
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mPosition);
+ out.writeInt(mRadius);
+ out.writeInt(mCenter.x);
+ out.writeInt(mCenter.y);
+ }
+
+ public static final @NonNull Creator<RoundedCorner> CREATOR = new Creator<RoundedCorner>() {
+ @Override
+ public RoundedCorner createFromParcel(Parcel in) {
+ return new RoundedCorner(in.readInt(), in.readInt(), in.readInt(), in.readInt());
+ }
+
+ @Override
+ public RoundedCorner[] newArray(int size) {
+ return new RoundedCorner[size];
+ }
+ };
+}
diff --git a/android/view/RoundedCorners.java b/android/view/RoundedCorners.java
new file mode 100644
index 0000000..623d969
--- /dev/null
+++ b/android/view/RoundedCorners.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
+import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
+import static android.view.RoundedCorner.POSITION_TOP_LEFT;
+import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import android.view.RoundedCorner.Position;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+
+/**
+ * A class to create & manage all the {@link RoundedCorner} on the display.
+ *
+ * @hide
+ */
+public class RoundedCorners implements Parcelable {
+
+ public static final RoundedCorners NO_ROUNDED_CORNERS = new RoundedCorners(
+ new RoundedCorner(POSITION_TOP_LEFT), new RoundedCorner(POSITION_TOP_RIGHT),
+ new RoundedCorner(POSITION_BOTTOM_RIGHT), new RoundedCorner(POSITION_BOTTOM_LEFT));
+
+ /**
+ * The number of possible positions at which rounded corners can be located.
+ */
+ public static final int ROUNDED_CORNER_POSITION_LENGTH = 4;
+
+ private static final Object CACHE_LOCK = new Object();
+
+ @GuardedBy("CACHE_LOCK")
+ private static int sCachedDisplayWidth;
+ @GuardedBy("CACHE_LOCK")
+ private static int sCachedDisplayHeight;
+ @GuardedBy("CACHE_LOCK")
+ private static Pair<Integer, Integer> sCachedRadii;
+ @GuardedBy("CACHE_LOCK")
+ private static RoundedCorners sCachedRoundedCorners;
+
+ @VisibleForTesting
+ public final RoundedCorner[] mRoundedCorners;
+
+ public RoundedCorners(RoundedCorner[] roundedCorners) {
+ mRoundedCorners = roundedCorners;
+ }
+
+ public RoundedCorners(RoundedCorner topLeft, RoundedCorner topRight, RoundedCorner bottomRight,
+ RoundedCorner bottomLeft) {
+ mRoundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ mRoundedCorners[POSITION_TOP_LEFT] = topLeft;
+ mRoundedCorners[POSITION_TOP_RIGHT] = topRight;
+ mRoundedCorners[POSITION_BOTTOM_RIGHT] = bottomRight;
+ mRoundedCorners[POSITION_BOTTOM_LEFT] = bottomLeft;
+ }
+
+ public RoundedCorners(RoundedCorners roundedCorners) {
+ mRoundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; ++i) {
+ mRoundedCorners[i] = new RoundedCorner(roundedCorners.mRoundedCorners[i]);
+ }
+ }
+
+ /**
+ * Creates the rounded corners according to @android:dimen/rounded_corner_radius,
+ * @android:dimen/rounded_corner_radius_top and @android:dimen/rounded_corner_radius_bottom
+ */
+ public static RoundedCorners fromResources(
+ Resources res, int displayWidth, int displayHeight) {
+ return fromRadii(loadRoundedCornerRadii(res), displayWidth, displayHeight);
+ }
+
+ /**
+ * Creates the rounded corners from radius
+ */
+ @VisibleForTesting
+ public static RoundedCorners fromRadii(Pair<Integer, Integer> radii, int displayWidth,
+ int displayHeight) {
+ if (radii == null) {
+ return null;
+ }
+
+ synchronized (CACHE_LOCK) {
+ if (radii.equals(sCachedRadii) && sCachedDisplayWidth == displayWidth
+ && sCachedDisplayHeight == displayHeight) {
+ return sCachedRoundedCorners;
+ }
+ }
+
+ final RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ final int topRadius = radii.first > 0 ? radii.first : 0;
+ final int bottomRadius = radii.second > 0 ? radii.second : 0;
+ for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; i++) {
+ roundedCorners[i] = createRoundedCorner(
+ i,
+ i <= POSITION_TOP_RIGHT ? topRadius : bottomRadius,
+ displayWidth,
+ displayHeight);
+ }
+
+ final RoundedCorners result = new RoundedCorners(roundedCorners);
+ synchronized (CACHE_LOCK) {
+ sCachedDisplayWidth = displayWidth;
+ sCachedDisplayHeight = displayHeight;
+ sCachedRadii = radii;
+ sCachedRoundedCorners = result;
+ }
+ return result;
+ }
+
+ /**
+ * Loads the rounded corner radii from resources.
+ *
+ * @param res
+ * @return a Pair of radius. The first is the top rounded corner radius and second is the
+ * bottom corner radius.
+ */
+ @Nullable
+ private static Pair<Integer, Integer> loadRoundedCornerRadii(Resources res) {
+ final int radiusDefault = res.getDimensionPixelSize(R.dimen.rounded_corner_radius);
+ final int radiusTop = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_top);
+ final int radiusBottom = res.getDimensionPixelSize(R.dimen.rounded_corner_radius_bottom);
+ if (radiusDefault == 0 && radiusTop == 0 && radiusBottom == 0) {
+ return null;
+ }
+ final Pair<Integer, Integer> radii = new Pair<>(
+ radiusTop > 0 ? radiusTop : radiusDefault,
+ radiusBottom > 0 ? radiusBottom : radiusDefault);
+ return radii;
+ }
+
+ /**
+ * Insets the reference frame of the rounded corners.
+ *
+ * @return a copy of this instance which has been inset
+ */
+ public RoundedCorners inset(int insetLeft, int insetTop, int insetRight, int insetBottom) {
+ final RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; i++) {
+ roundedCorners[i] = insetRoundedCorner(i, insetLeft, insetTop, insetRight, insetBottom);
+ }
+ return new RoundedCorners(roundedCorners);
+ }
+
+ private RoundedCorner insetRoundedCorner(@Position int position, int insetLeft,
+ int insetTop, int insetRight, int insetBottom) {
+ if (mRoundedCorners[position].isEmpty()) {
+ return new RoundedCorner(position);
+ }
+
+ final int radius = mRoundedCorners[position].getRadius();
+ final Point center = mRoundedCorners[position].getCenter();
+ boolean hasRoundedCorner;
+ switch (position) {
+ case POSITION_TOP_LEFT:
+ hasRoundedCorner = radius > insetTop && radius > insetLeft;
+ break;
+ case POSITION_TOP_RIGHT:
+ hasRoundedCorner = radius > insetTop && radius > insetRight;
+ break;
+ case POSITION_BOTTOM_RIGHT:
+ hasRoundedCorner = radius > insetBottom && radius > insetRight;
+ break;
+ case POSITION_BOTTOM_LEFT:
+ hasRoundedCorner = radius > insetBottom && radius > insetLeft;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "The position is not one of the RoundedCornerPosition =" + position);
+ }
+ return new RoundedCorner(
+ position, radius,
+ hasRoundedCorner ? center.x - insetLeft : 0,
+ hasRoundedCorner ? center.y - insetTop : 0);
+ }
+
+ /**
+ * Returns the {@link RoundedCorner} of the given position if there is one.
+ *
+ * @param position the position of the rounded corner on the display.
+ * @return the rounded corner of the given position. Returns {@code null} if
+ * {@link RoundedCorner#isEmpty()} is {@code true}.
+ */
+ @Nullable
+ public RoundedCorner getRoundedCorner(@Position int position) {
+ return mRoundedCorners[position].isEmpty()
+ ? null : new RoundedCorner(mRoundedCorners[position]);
+ }
+
+ /**
+ * Sets the rounded corner of given position.
+ *
+ * @param position the position of this rounded corner
+ * @param roundedCorner the rounded corner or null if there is none
+ */
+ public void setRoundedCorner(@Position int position, @Nullable RoundedCorner roundedCorner) {
+ mRoundedCorners[position] = roundedCorner == null
+ ? new RoundedCorner(position) : roundedCorner;
+ }
+
+ /**
+ * Returns an array of {@link RoundedCorner}s. Ordinal value of RoundedCornerPosition is used
+ * as an index of the array.
+ *
+ * @return an array of {@link RoundedCorner}s, one for each rounded corner area.
+ */
+ public RoundedCorner[] getAllRoundedCorners() {
+ RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; ++i) {
+ roundedCorners[i] = new RoundedCorner(roundedCorners[i]);
+ }
+ return roundedCorners;
+ }
+
+ /**
+ * Returns a scaled RoundedCorners.
+ */
+ public RoundedCorners scale(float scale) {
+ if (scale == 1f) {
+ return this;
+ }
+
+ RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ for (int i = 0; i < ROUNDED_CORNER_POSITION_LENGTH; ++i) {
+ final RoundedCorner roundedCorner = mRoundedCorners[i];
+ roundedCorners[i] = new RoundedCorner(
+ i,
+ (int) (roundedCorner.getRadius() * scale),
+ (int) (roundedCorner.getCenter().x * scale),
+ (int) (roundedCorner.getCenter().y * scale));
+ }
+ return new RoundedCorners(roundedCorners);
+ }
+
+ /**
+ * Returns a rotated RoundedCorners.
+ */
+ public RoundedCorners rotate(@Surface.Rotation int rotation, int initialDisplayWidth,
+ int initialDisplayHeight) {
+ if (rotation == ROTATION_0) {
+ return this;
+ }
+ final boolean isSizeFlipped = rotation == ROTATION_90 || rotation == ROTATION_270;
+ RoundedCorner[] newCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ int newPosistion;
+ for (int i = 0; i < mRoundedCorners.length; i++) {
+ newPosistion = getRotatedIndex(i, rotation);
+ newCorners[newPosistion] = createRoundedCorner(
+ newPosistion,
+ mRoundedCorners[i].getRadius(),
+ isSizeFlipped ? initialDisplayHeight : initialDisplayWidth,
+ isSizeFlipped ? initialDisplayWidth : initialDisplayHeight);
+ }
+ return new RoundedCorners(newCorners);
+ }
+
+ private static RoundedCorner createRoundedCorner(@Position int position,
+ int radius, int displayWidth, int displayHeight) {
+ switch (position) {
+ case POSITION_TOP_LEFT:
+ return new RoundedCorner(
+ POSITION_TOP_LEFT,
+ radius,
+ radius > 0 ? radius : 0,
+ radius > 0 ? radius : 0);
+ case POSITION_TOP_RIGHT:
+ return new RoundedCorner(
+ POSITION_TOP_RIGHT,
+ radius,
+ radius > 0 ? displayWidth - radius : 0,
+ radius > 0 ? radius : 0);
+ case POSITION_BOTTOM_RIGHT:
+ return new RoundedCorner(
+ POSITION_BOTTOM_RIGHT,
+ radius,
+ radius > 0 ? displayWidth - radius : 0,
+ radius > 0 ? displayHeight - radius : 0);
+ case POSITION_BOTTOM_LEFT:
+ return new RoundedCorner(
+ POSITION_BOTTOM_LEFT,
+ radius,
+ radius > 0 ? radius : 0,
+ radius > 0 ? displayHeight - radius : 0);
+ default:
+ throw new IllegalArgumentException(
+ "The position is not one of the RoundedCornerPosition =" + position);
+ }
+ }
+
+ private static int getRotatedIndex(int position, int rotation) {
+ return (position - rotation + ROUNDED_CORNER_POSITION_LENGTH) % 4;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (RoundedCorner roundedCorner : mRoundedCorners) {
+ result = result * 31 + roundedCorner.hashCode();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof RoundedCorners) {
+ RoundedCorners r = (RoundedCorners) o;
+ return Arrays.deepEquals(mRoundedCorners, r.mRoundedCorners);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "RoundedCorners{" + Arrays.toString(mRoundedCorners) + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (equals(NO_ROUNDED_CORNERS)) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ dest.writeTypedArray(mRoundedCorners, flags);
+ }
+ }
+
+ public static final @NonNull Creator<RoundedCorners> CREATOR = new Creator<RoundedCorners>() {
+ @Override
+ public RoundedCorners createFromParcel(Parcel in) {
+ int variant = in.readInt();
+ if (variant == 0) {
+ return NO_ROUNDED_CORNERS;
+ }
+ RoundedCorner[] roundedCorners = new RoundedCorner[ROUNDED_CORNER_POSITION_LENGTH];
+ in.readTypedArray(roundedCorners, RoundedCorner.CREATOR);
+ return new RoundedCorners(roundedCorners);
+ }
+
+ @Override
+ public RoundedCorners[] newArray(int size) {
+ return new RoundedCorners[size];
+ }
+ };
+}
diff --git a/android/view/ScaleGestureDetector.java b/android/view/ScaleGestureDetector.java
new file mode 100644
index 0000000..346f76c
--- /dev/null
+++ b/android/view/ScaleGestureDetector.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+
+/**
+ * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
+ * The {@link OnScaleGestureListener} callback will notify users when a particular
+ * gesture event has occurred.
+ *
+ * This class should only be used with {@link MotionEvent}s reported via touch.
+ *
+ * To use this class:
+ * <ul>
+ * <li>Create an instance of the {@code ScaleGestureDetector} for your
+ * {@link View}
+ * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
+ * {@link #onTouchEvent(MotionEvent)}. The methods defined in your
+ * callback will be executed when the events occur.
+ * </ul>
+ */
+public class ScaleGestureDetector {
+ private static final String TAG = "ScaleGestureDetector";
+
+ /**
+ * The listener for receiving notifications when gestures occur.
+ * If you want to listen for all the different gestures then implement
+ * this interface. If you only want to listen for a subset it might
+ * be easier to extend {@link SimpleOnScaleGestureListener}.
+ *
+ * An application will receive events in the following order:
+ * <ul>
+ * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
+ * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
+ * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
+ * </ul>
+ */
+ public interface OnScaleGestureListener {
+ /**
+ * Responds to scaling events for a gesture in progress.
+ * Reported by pointer motion.
+ *
+ * @param detector The detector reporting the event - use this to
+ * retrieve extended info about event state.
+ * @return Whether or not the detector should consider this event
+ * as handled. If an event was not handled, the detector
+ * will continue to accumulate movement until an event is
+ * handled. This can be useful if an application, for example,
+ * only wants to update scaling factors if the change is
+ * greater than 0.01.
+ */
+ public boolean onScale(ScaleGestureDetector detector);
+
+ /**
+ * Responds to the beginning of a scaling gesture. Reported by
+ * new pointers going down.
+ *
+ * @param detector The detector reporting the event - use this to
+ * retrieve extended info about event state.
+ * @return Whether or not the detector should continue recognizing
+ * this gesture. For example, if a gesture is beginning
+ * with a focal point outside of a region where it makes
+ * sense, onScaleBegin() may return false to ignore the
+ * rest of the gesture.
+ */
+ public boolean onScaleBegin(ScaleGestureDetector detector);
+
+ /**
+ * Responds to the end of a scale gesture. Reported by existing
+ * pointers going up.
+ *
+ * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
+ * and {@link ScaleGestureDetector#getFocusY()} will return focal point
+ * of the pointers remaining on the screen.
+ *
+ * @param detector The detector reporting the event - use this to
+ * retrieve extended info about event state.
+ */
+ public void onScaleEnd(ScaleGestureDetector detector);
+ }
+
+ /**
+ * A convenience class to extend when you only want to listen for a subset
+ * of scaling-related events. This implements all methods in
+ * {@link OnScaleGestureListener} but does nothing.
+ * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
+ * {@code false} so that a subclass can retrieve the accumulated scale
+ * factor in an overridden onScaleEnd.
+ * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
+ * {@code true}.
+ */
+ public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
+
+ public boolean onScale(ScaleGestureDetector detector) {
+ return false;
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return true;
+ }
+
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ // Intentionally empty
+ }
+ }
+
+ private final Context mContext;
+ @UnsupportedAppUsage
+ private final OnScaleGestureListener mListener;
+
+ private float mFocusX;
+ private float mFocusY;
+
+ private boolean mQuickScaleEnabled;
+ private boolean mStylusScaleEnabled;
+
+ private float mCurrSpan;
+ private float mPrevSpan;
+ private float mInitialSpan;
+ private float mCurrSpanX;
+ private float mCurrSpanY;
+ private float mPrevSpanX;
+ private float mPrevSpanY;
+ private long mCurrTime;
+ private long mPrevTime;
+ private boolean mInProgress;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768938)
+ private int mSpanSlop;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768938)
+ private int mMinSpan;
+
+ private final Handler mHandler;
+
+ private float mAnchoredScaleStartX;
+ private float mAnchoredScaleStartY;
+ private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+
+ private static final long TOUCH_STABILIZE_TIME = 128; // ms
+ private static final float SCALE_FACTOR = .5f;
+ private static final int ANCHORED_SCALE_MODE_NONE = 0;
+ private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
+ private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
+
+
+ /**
+ * Consistency verifier for debugging purposes.
+ */
+ private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled() ?
+ new InputEventConsistencyVerifier(this, 0) : null;
+ private GestureDetector mGestureDetector;
+
+ private boolean mEventBeforeOrAboveStartingGestureEvent;
+
+ /**
+ * Creates a ScaleGestureDetector with the supplied listener.
+ * You may only use this constructor from a {@link android.os.Looper Looper} thread.
+ *
+ * @param context the application's context
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ *
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
+ this(context, listener, null);
+ }
+
+ /**
+ * Creates a ScaleGestureDetector with the supplied listener.
+ * @see android.os.Handler#Handler()
+ *
+ * @param context the application's context
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ * @param handler the handler to use for running deferred listener events.
+ *
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
+ Handler handler) {
+ mContext = context;
+ mListener = listener;
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
+ mSpanSlop = viewConfiguration.getScaledTouchSlop() * 2;
+ mMinSpan = viewConfiguration.getScaledMinimumScalingSpan();
+ mHandler = handler;
+ // Quick scale is enabled by default after JB_MR2
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+ if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ setQuickScaleEnabled(true);
+ }
+ // Stylus scale is enabled by default after LOLLIPOP_MR1
+ if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
+ setStylusScaleEnabled(true);
+ }
+ }
+
+ /**
+ * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
+ * when appropriate.
+ *
+ * <p>Applications should pass a complete and consistent event stream to this method.
+ * A complete and consistent event stream involves all MotionEvents from the initial
+ * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
+ *
+ * @param event The event to process
+ * @return true if the event was processed and the detector wants to receive the
+ * rest of the MotionEvents in this event stream.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTouchEvent(event, 0);
+ }
+
+ mCurrTime = event.getEventTime();
+
+ final int action = event.getActionMasked();
+
+ // Forward the event to check for double tap gesture
+ if (mQuickScaleEnabled) {
+ mGestureDetector.onTouchEvent(event);
+ }
+
+ final int count = event.getPointerCount();
+ final boolean isStylusButtonDown =
+ (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
+
+ final boolean anchoredScaleCancelled =
+ mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
+ final boolean streamComplete = action == MotionEvent.ACTION_UP ||
+ action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
+
+ if (action == MotionEvent.ACTION_DOWN || streamComplete) {
+ // Reset any scale in progress with the listener.
+ // If it's an ACTION_DOWN we're beginning a new event stream.
+ // This means the app probably didn't give us all the events. Shame on it.
+ if (mInProgress) {
+ mListener.onScaleEnd(this);
+ mInProgress = false;
+ mInitialSpan = 0;
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+ } else if (inAnchoredScaleMode() && streamComplete) {
+ mInProgress = false;
+ mInitialSpan = 0;
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+ }
+
+ if (streamComplete) {
+ return true;
+ }
+ }
+
+ if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
+ && !streamComplete && isStylusButtonDown) {
+ // Start of a button scale gesture
+ mAnchoredScaleStartX = event.getX();
+ mAnchoredScaleStartY = event.getY();
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
+ mInitialSpan = 0;
+ }
+
+ final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
+ action == MotionEvent.ACTION_POINTER_UP ||
+ action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
+
+ final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+ final int skipIndex = pointerUp ? event.getActionIndex() : -1;
+
+ // Determine focal point
+ float sumX = 0, sumY = 0;
+ final int div = pointerUp ? count - 1 : count;
+ final float focusX;
+ final float focusY;
+ if (inAnchoredScaleMode()) {
+ // In anchored scale mode, the focal pt is always where the double tap
+ // or button down gesture started
+ focusX = mAnchoredScaleStartX;
+ focusY = mAnchoredScaleStartY;
+ if (event.getY() < focusY) {
+ mEventBeforeOrAboveStartingGestureEvent = true;
+ } else {
+ mEventBeforeOrAboveStartingGestureEvent = false;
+ }
+ } else {
+ for (int i = 0; i < count; i++) {
+ if (skipIndex == i) continue;
+ sumX += event.getX(i);
+ sumY += event.getY(i);
+ }
+
+ focusX = sumX / div;
+ focusY = sumY / div;
+ }
+
+ // Determine average deviation from focal point
+ float devSumX = 0, devSumY = 0;
+ for (int i = 0; i < count; i++) {
+ if (skipIndex == i) continue;
+
+ // Convert the resulting diameter into a radius.
+ devSumX += Math.abs(event.getX(i) - focusX);
+ devSumY += Math.abs(event.getY(i) - focusY);
+ }
+ final float devX = devSumX / div;
+ final float devY = devSumY / div;
+
+ // Span is the average distance between touch points through the focal point;
+ // i.e. the diameter of the circle with a radius of the average deviation from
+ // the focal point.
+ final float spanX = devX * 2;
+ final float spanY = devY * 2;
+ final float span;
+ if (inAnchoredScaleMode()) {
+ span = spanY;
+ } else {
+ span = (float) Math.hypot(spanX, spanY);
+ }
+
+ // Dispatch begin/end events as needed.
+ // If the configuration changes, notify the app to reset its current state by beginning
+ // a fresh scale event stream.
+ final boolean wasInProgress = mInProgress;
+ mFocusX = focusX;
+ mFocusY = focusY;
+ if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
+ mListener.onScaleEnd(this);
+ mInProgress = false;
+ mInitialSpan = span;
+ }
+ if (configChanged) {
+ mPrevSpanX = mCurrSpanX = spanX;
+ mPrevSpanY = mCurrSpanY = spanY;
+ mInitialSpan = mPrevSpan = mCurrSpan = span;
+ }
+
+ final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
+ if (!mInProgress && span >= minSpan &&
+ (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
+ mPrevSpanX = mCurrSpanX = spanX;
+ mPrevSpanY = mCurrSpanY = spanY;
+ mPrevSpan = mCurrSpan = span;
+ mPrevTime = mCurrTime;
+ mInProgress = mListener.onScaleBegin(this);
+ }
+
+ // Handle motion; focal point and span/scale factor are changing.
+ if (action == MotionEvent.ACTION_MOVE) {
+ mCurrSpanX = spanX;
+ mCurrSpanY = spanY;
+ mCurrSpan = span;
+
+ boolean updatePrev = true;
+
+ if (mInProgress) {
+ updatePrev = mListener.onScale(this);
+ }
+
+ if (updatePrev) {
+ mPrevSpanX = mCurrSpanX;
+ mPrevSpanY = mCurrSpanY;
+ mPrevSpan = mCurrSpan;
+ mPrevTime = mCurrTime;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean inAnchoredScaleMode() {
+ return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
+ }
+
+ /**
+ * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
+ * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
+ * if the app targets API 19 and newer.
+ * @param scales true to enable quick scaling, false to disable
+ */
+ public void setQuickScaleEnabled(boolean scales) {
+ mQuickScaleEnabled = scales;
+ if (mQuickScaleEnabled && mGestureDetector == null) {
+ GestureDetector.SimpleOnGestureListener gestureListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ // Double tap: start watching for a swipe
+ mAnchoredScaleStartX = e.getX();
+ mAnchoredScaleStartY = e.getY();
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
+ return true;
+ }
+ };
+ mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
+ }
+ }
+
+ /**
+ * Return whether the quick scale gesture, in which the user performs a double tap followed by a
+ * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
+ */
+ public boolean isQuickScaleEnabled() {
+ return mQuickScaleEnabled;
+ }
+
+ /**
+ * Sets whether the associates {@link OnScaleGestureListener} should receive
+ * onScale callbacks when the user uses a stylus and presses the button.
+ * Note that this is enabled by default if the app targets API 23 and newer.
+ *
+ * @param scales true to enable stylus scaling, false to disable.
+ */
+ public void setStylusScaleEnabled(boolean scales) {
+ mStylusScaleEnabled = scales;
+ }
+
+ /**
+ * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
+ * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
+ */
+ public boolean isStylusScaleEnabled() {
+ return mStylusScaleEnabled;
+ }
+
+ /**
+ * Returns {@code true} if a scale gesture is in progress.
+ */
+ public boolean isInProgress() {
+ return mInProgress;
+ }
+
+ /**
+ * Get the X coordinate of the current gesture's focal point.
+ * If a gesture is in progress, the focal point is between
+ * each of the pointers forming the gesture.
+ *
+ * If {@link #isInProgress()} would return false, the result of this
+ * function is undefined.
+ *
+ * @return X coordinate of the focal point in pixels.
+ */
+ public float getFocusX() {
+ return mFocusX;
+ }
+
+ /**
+ * Get the Y coordinate of the current gesture's focal point.
+ * If a gesture is in progress, the focal point is between
+ * each of the pointers forming the gesture.
+ *
+ * If {@link #isInProgress()} would return false, the result of this
+ * function is undefined.
+ *
+ * @return Y coordinate of the focal point in pixels.
+ */
+ public float getFocusY() {
+ return mFocusY;
+ }
+
+ /**
+ * Return the average distance between each of the pointers forming the
+ * gesture in progress through the focal point.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpan() {
+ return mCurrSpan;
+ }
+
+ /**
+ * Return the average X distance between each of the pointers forming the
+ * gesture in progress through the focal point.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpanX() {
+ return mCurrSpanX;
+ }
+
+ /**
+ * Return the average Y distance between each of the pointers forming the
+ * gesture in progress through the focal point.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpanY() {
+ return mCurrSpanY;
+ }
+
+ /**
+ * Return the previous average distance between each of the pointers forming the
+ * gesture in progress through the focal point.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpan() {
+ return mPrevSpan;
+ }
+
+ /**
+ * Return the previous average X distance between each of the pointers forming the
+ * gesture in progress through the focal point.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpanX() {
+ return mPrevSpanX;
+ }
+
+ /**
+ * Return the previous average Y distance between each of the pointers forming the
+ * gesture in progress through the focal point.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpanY() {
+ return mPrevSpanY;
+ }
+
+ /**
+ * Return the scaling factor from the previous scale event to the current
+ * event. This value is defined as
+ * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
+ *
+ * @return The current scaling factor.
+ */
+ public float getScaleFactor() {
+ if (inAnchoredScaleMode()) {
+ // Drag is moving up; the further away from the gesture
+ // start, the smaller the span should be, the closer,
+ // the larger the span, and therefore the larger the scale
+ final boolean scaleUp =
+ (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
+ (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
+ final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
+ return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
+ }
+ return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
+ }
+
+ /**
+ * Return the time difference in milliseconds between the previous
+ * accepted scaling event and the current scaling event.
+ *
+ * @return Time difference since the last scaling event in milliseconds.
+ */
+ public long getTimeDelta() {
+ return mCurrTime - mPrevTime;
+ }
+
+ /**
+ * Return the event time of the current event being processed.
+ *
+ * @return Current event time in milliseconds.
+ */
+ public long getEventTime() {
+ return mCurrTime;
+ }
+}
\ No newline at end of file
diff --git a/android/view/ScrollCaptureCallback.java b/android/view/ScrollCaptureCallback.java
new file mode 100644
index 0000000..1688616
--- /dev/null
+++ b/android/view/ScrollCaptureCallback.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.graphics.Rect;
+import android.os.CancellationSignal;
+
+import java.util.function.Consumer;
+
+/**
+ * A ScrollCaptureCallback is responsible for providing rendered snapshots of scrolling content for
+ * the scroll capture system. A single callback is responsible for providing support to a single
+ * scrolling UI element. At request time, the system will select the best candidate from among all
+ * callbacks registered within the window.
+ * <p>
+ * A callback is assigned to a View using {@link View#setScrollCaptureCallback}, or to the window as
+ * {@link Window#registerScrollCaptureCallback}. The point where the callback is registered defines
+ * the frame of reference for the bounds measurements used.
+ * <p>
+ * <b>Terminology</b>
+ * <dl>
+ * <dt>Containing View</dt>
+ * <dd>The view on which this callback is attached, or the root view of the window if the callback
+ * is assigned directly to a window.</dd>
+ *
+ * <dt>Scroll Bounds</dt>
+ * <dd>A rectangle which describes an area within the containing view where scrolling content
+ * appears. This may be the entire view or any rectangle within. This defines a frame of reference
+ * for requests as well as the width and maximum height of a single request.</dd>
+ *
+ * <dt>Scroll Delta</dt>
+ * <dd>The distance the scroll position has moved since capture started. Implementations are
+ * responsible for tracking changes in vertical scroll position during capture. This is required to
+ * map the capture area to the correct location, given the current scroll position.
+ *
+ * <dt>Capture Area</dt>
+ * <dd>A rectangle which describes the area to capture, relative to scroll bounds. The vertical
+ * position remains relative to the starting scroll position and any movement since ("Scroll Delta")
+ * should be subtracted to locate the correct local position, and scrolled into view as necessary.
+ * </dd>
+ * </dl>
+ *
+ * @see View#setScrollCaptureHint(int)
+ * @see View#setScrollCaptureCallback(ScrollCaptureCallback)
+ * @see Window#registerScrollCaptureCallback(ScrollCaptureCallback)
+ */
+@UiThread
+public interface ScrollCaptureCallback {
+
+ /**
+ * The system is searching for the appropriate scrolling container to capture and would like to
+ * know the size and position of scrolling content handled by this callback.
+ * <p>
+ * To determine scroll bounds, an implementation should inset the visible bounds of the
+ * containing view to cover only the area where scrolling content may be positioned. This
+ * should cover only the content which tracks with scrolling movement.
+ * <p>
+ * Return the updated rectangle to {@link Consumer#accept onReady.accept}. If for any reason the
+ * scrolling content is not available to capture, a empty rectangle may be returned which will
+ * exclude this view from consideration.
+ * <p>
+ * This request may be cancelled via the provided {@link CancellationSignal}. When this happens,
+ * any future call to {@link Consumer#accept onReady.accept} will have no effect and this
+ * content will be omitted from the search results.
+ *
+ * @param signal signal to cancel the operation in progress
+ * @param onReady consumer for the updated rectangle
+ */
+ void onScrollCaptureSearch(@NonNull CancellationSignal signal, @NonNull Consumer<Rect> onReady);
+
+ /**
+ * Scroll Capture has selected this callback to provide the scrolling image content.
+ * <p>
+ * {@link Runnable#run onReady.run} should be called when ready to begin handling image
+ * requests.
+ * <p>
+ * This request may be cancelled via the provided {@link CancellationSignal}. When this happens,
+ * any future call to {@link Runnable#run onReady.run} will have no effect and provided session
+ * will not be activated.
+ *
+ * @param session the current session, resources provided by it are valid for use until the
+ * {@link #onScrollCaptureEnd(Runnable) session ends}
+ * @param signal signal to cancel the operation in progress
+ * @param onReady signal used to report completion of the request
+ */
+ void onScrollCaptureStart(@NonNull ScrollCaptureSession session,
+ @NonNull CancellationSignal signal, @NonNull Runnable onReady);
+
+ /**
+ * An image capture has been requested from the scrolling content.
+ * <p>
+ * The requested rectangle describes an area inside the target view, relative to
+ * <code>scrollBounds</code>. The content may be offscreen, above or below the current visible
+ * portion of the target view. To handle the request, render the available portion of this
+ * rectangle to a buffer and return it via the Surface available from {@link
+ * ScrollCaptureSession#getSurface()}.
+ * <p>
+ * Note: Implementations are only required to render the requested content, and may do so into
+ * off-screen buffers without scrolling if they are able.
+ * <p>
+ * The resulting available portion of the request must be computed as a portion of {@code
+ * captureArea}, and sent to signal the operation is complete, using {@link Consumer#accept
+ * onComplete.accept}. If the requested rectangle is partially or fully out of bounds the
+ * resulting portion should be returned. If no portion is available (outside of available
+ * content), then skip sending any buffer and report an empty Rect as result.
+ * <p>
+ * This request may be cancelled via the provided {@link CancellationSignal}. When this happens,
+ * any future call to {@link Consumer#accept onComplete.accept} will be ignored until the next
+ * request.
+ *
+ * @param session the current session, resources provided by it are valid for use until the
+ * {@link #onScrollCaptureEnd(Runnable) session ends}
+ * @param signal signal to cancel the operation in progress
+ * @param captureArea the area to capture, a rectangle within {@code scrollBounds}
+ * @param onComplete a consumer for the captured area
+ */
+ void onScrollCaptureImageRequest(@NonNull ScrollCaptureSession session,
+ @NonNull CancellationSignal signal, @NonNull Rect captureArea,
+ @NonNull Consumer<Rect> onComplete);
+
+ /**
+ * Signals that capture has ended. Implementations should release any temporary resources or
+ * references to objects in use during the capture. Any resources obtained from the session are
+ * now invalid and attempts to use them after this point may throw an exception.
+ * <p>
+ * The window should be returned to its original state when capture started. At a minimum, the
+ * content should be scrolled to its original position.
+ * <p>
+ * {@link Runnable#run onReady.run} should be called as soon as possible after the window is
+ * ready for normal interactive use. After the callback (or after a timeout, if not called) the
+ * screenshot tool will be dismissed and the window may become visible to the user at any time.
+ *
+ * @param onReady a callback to inform the system that the application has completed any
+ * cleanup and is ready to become visible
+ */
+ void onScrollCaptureEnd(@NonNull Runnable onReady);
+}
+
diff --git a/android/view/ScrollCaptureConnection.java b/android/view/ScrollCaptureConnection.java
new file mode 100644
index 0000000..5fcb011
--- /dev/null
+++ b/android/view/ScrollCaptureConnection.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.CancellationSignal;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
+import android.os.Trace;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Mediator between a selected scroll capture target view and a remote process.
+ * <p>
+ * An instance is created to wrap the selected {@link ScrollCaptureCallback}.
+ *
+ * @hide
+ */
+public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
+
+ private static final String TAG = "ScrollCaptureConnection";
+
+ private final Object mLock = new Object();
+ private final Rect mScrollBounds;
+ private final Point mPositionInWindow;
+ private final Executor mUiThread;
+ private final CloseGuard mCloseGuard = new CloseGuard();
+
+
+ private ScrollCaptureCallback mLocal;
+ private IScrollCaptureCallbacks mRemote;
+
+ private ScrollCaptureSession mSession;
+
+ private CancellationSignal mCancellation;
+
+ private volatile boolean mActive;
+
+ /**
+ * Constructs a ScrollCaptureConnection.
+ *
+ * @param uiThread an executor for the UI thread of the containing View
+ * @param selectedTarget the target the client is controlling
+ *
+ * @hide
+ */
+ public ScrollCaptureConnection(
+ @NonNull Executor uiThread,
+ @NonNull ScrollCaptureTarget selectedTarget) {
+ mUiThread = requireNonNull(uiThread, "<uiThread> must non-null");
+ requireNonNull(selectedTarget, "<selectedTarget> must non-null");
+ mScrollBounds = requireNonNull(Rect.copyOrNull(selectedTarget.getScrollBounds()),
+ "target.getScrollBounds() must be non-null to construct a client");
+ mLocal = selectedTarget.getCallback();
+ mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
+ }
+
+ @BinderThread
+ @Override
+ public ICancellationSignal startCapture(@NonNull Surface surface,
+ @NonNull IScrollCaptureCallbacks remote) throws RemoteException {
+
+ mCloseGuard.open("close");
+
+ if (!surface.isValid()) {
+ throw new RemoteException(new IllegalArgumentException("surface must be valid"));
+ }
+ mRemote = requireNonNull(remote, "<callbacks> must non-null");
+
+ ICancellationSignal cancellation = CancellationSignal.createTransport();
+ mCancellation = CancellationSignal.fromTransport(cancellation);
+ mSession = new ScrollCaptureSession(surface, mScrollBounds, mPositionInWindow);
+
+ Runnable listener =
+ SafeCallback.create(mCancellation, mUiThread, this::onStartCaptureCompleted);
+ // -> UiThread
+ mUiThread.execute(() -> mLocal.onScrollCaptureStart(mSession, mCancellation, listener));
+ return cancellation;
+ }
+
+ @UiThread
+ private void onStartCaptureCompleted() {
+ mActive = true;
+ try {
+ mRemote.onCaptureStarted();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Shutting down due to error: ", e);
+ close();
+ }
+ }
+
+ @BinderThread
+ @Override
+ public ICancellationSignal requestImage(Rect requestRect) throws RemoteException {
+ Trace.beginSection("requestImage");
+ checkActive();
+
+ ICancellationSignal cancellation = CancellationSignal.createTransport();
+ mCancellation = CancellationSignal.fromTransport(cancellation);
+
+ Consumer<Rect> listener =
+ SafeCallback.create(mCancellation, mUiThread, this::onImageRequestCompleted);
+ // -> UiThread
+ mUiThread.execute(() -> mLocal.onScrollCaptureImageRequest(
+ mSession, mCancellation, new Rect(requestRect), listener));
+ Trace.endSection();
+ return cancellation;
+ }
+
+ @UiThread
+ void onImageRequestCompleted(Rect capturedArea) {
+ try {
+ mRemote.onImageRequestCompleted(0, capturedArea);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Shutting down due to error: ", e);
+ close();
+ }
+ }
+
+ @BinderThread
+ @Override
+ public ICancellationSignal endCapture() throws RemoteException {
+ checkActive();
+
+ ICancellationSignal cancellation = CancellationSignal.createTransport();
+ mCancellation = CancellationSignal.fromTransport(cancellation);
+
+ Runnable listener =
+ SafeCallback.create(mCancellation, mUiThread, this::onEndCaptureCompleted);
+ // -> UiThread
+ mUiThread.execute(() -> mLocal.onScrollCaptureEnd(listener));
+ return cancellation;
+ }
+
+ @UiThread
+ private void onEndCaptureCompleted() {
+ mActive = false;
+ try {
+ if (mRemote != null) {
+ mRemote.onCaptureEnded();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Caught exception confirming capture end!", e);
+ } finally {
+ close();
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void close() {
+ if (mActive) {
+ if (mCancellation != null) {
+ Log.w(TAG, "close(): cancelling pending operation.");
+ mCancellation.cancel();
+ mCancellation = null;
+ }
+ Log.w(TAG, "close(): capture session still active! Ending now.");
+ // -> UiThread
+ final ScrollCaptureCallback callback = mLocal;
+ mUiThread.execute(() -> callback.onScrollCaptureEnd(() -> { /* ignore */ }));
+ mActive = false;
+ }
+ mActive = false;
+ mSession = null;
+ mRemote = null;
+ mLocal = null;
+ mCloseGuard.close();
+ Reference.reachabilityFence(this);
+ }
+
+ @VisibleForTesting
+ public boolean isActive() {
+ return mActive;
+ }
+
+ private void checkActive() throws RemoteException {
+ synchronized (mLock) {
+ if (!mActive) {
+ throw new RemoteException(new IllegalStateException("Not started!"));
+ }
+ }
+ }
+
+ /** @return a string representation of the state of this client */
+ public String toString() {
+ return "ScrollCaptureConnection{"
+ + "active=" + mActive
+ + ", session=" + mSession
+ + ", remote=" + mRemote
+ + ", local=" + mLocal
+ + "}";
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ mCloseGuard.warnIfOpen();
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private static class SafeCallback<T> {
+ private final CancellationSignal mSignal;
+ private final WeakReference<T> mTargetRef;
+ private final Executor mExecutor;
+ private boolean mExecuted;
+
+ protected SafeCallback(CancellationSignal signal, Executor executor, T target) {
+ mSignal = signal;
+ mTargetRef = new WeakReference<>(target);
+ mExecutor = executor;
+ }
+
+ // Provide the target to the consumer to invoke, forward on handler thread ONCE,
+ // and only if noy cancelled, and the target is still available (not collected)
+ protected final void maybeAccept(Consumer<T> targetConsumer) {
+ if (mExecuted) {
+ return;
+ }
+ mExecuted = true;
+ if (mSignal.isCanceled()) {
+ return;
+ }
+ T target = mTargetRef.get();
+ if (target == null) {
+ return;
+ }
+ mExecutor.execute(() -> targetConsumer.accept(target));
+ }
+
+ static Runnable create(CancellationSignal signal, Executor executor, Runnable target) {
+ return new RunnableCallback(signal, executor, target);
+ }
+
+ static <T> Consumer<T> create(CancellationSignal signal, Executor executor,
+ Consumer<T> target) {
+ return new ConsumerCallback<T>(signal, executor, target);
+ }
+ }
+
+ private static final class RunnableCallback extends SafeCallback<Runnable> implements Runnable {
+ RunnableCallback(CancellationSignal signal, Executor executor, Runnable target) {
+ super(signal, executor, target);
+ }
+
+ @Override
+ public void run() {
+ maybeAccept(Runnable::run);
+ }
+ }
+
+ private static final class ConsumerCallback<T> extends SafeCallback<Consumer<T>>
+ implements Consumer<T> {
+ ConsumerCallback(CancellationSignal signal, Executor executor, Consumer<T> target) {
+ super(signal, executor, target);
+ }
+
+ @Override
+ public void accept(T value) {
+ maybeAccept((target) -> target.accept(value));
+ }
+ }
+}
diff --git a/android/view/ScrollCaptureResponse.java b/android/view/ScrollCaptureResponse.java
new file mode 100644
index 0000000..8808827
--- /dev/null
+++ b/android/view/ScrollCaptureResponse.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+
+/** @hide */
+@DataClass(genToString = true, genGetters = true)
+public class ScrollCaptureResponse implements Parcelable {
+
+ /** Developer-facing human readable description of the result. */
+ @NonNull
+ private String mDescription = "";
+
+ // Remaining fields are non-null when isConnected() == true
+
+ /** The active connection for a successful result. */
+ @Nullable
+ @DataClass.MaySetToNull
+ private IScrollCaptureConnection mConnection = null;
+
+ /** The bounds of the window within the display */
+ @Nullable
+ private Rect mWindowBounds = null;
+
+ /** The bounds of the scrolling content, in window space. */
+ @Nullable
+ private Rect mBoundsInWindow = null;
+
+ /** The current window title. */
+ @Nullable
+ private String mWindowTitle = null;
+
+ /** Carries additional logging and debugging information when enabled. */
+ @NonNull
+ @DataClass.PluralOf("message")
+ private ArrayList<String> mMessages = new ArrayList<>();
+
+ /** Whether an active connection is present. */
+ public boolean isConnected() {
+ return mConnection != null && mConnection.asBinder().isBinderAlive();
+ }
+
+ /** Closes a connection returned with this response. */
+ public void close() {
+ if (mConnection != null) {
+ try {
+ mConnection.close();
+ } catch (RemoteException e) {
+ // Ignore
+ }
+ mConnection = null;
+ }
+ }
+
+
+
+ // Code below generated by codegen v1.0.22.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/ScrollCaptureResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ /* package-private */ ScrollCaptureResponse(
+ @NonNull String description,
+ @Nullable IScrollCaptureConnection connection,
+ @Nullable Rect windowBounds,
+ @Nullable Rect boundsInWindow,
+ @Nullable String windowTitle,
+ @NonNull ArrayList<String> messages) {
+ this.mDescription = description;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mDescription);
+ this.mConnection = connection;
+ this.mWindowBounds = windowBounds;
+ this.mBoundsInWindow = boundsInWindow;
+ this.mWindowTitle = windowTitle;
+ this.mMessages = messages;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mMessages);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * Developer-facing human readable description of the result.
+ */
+ @DataClass.Generated.Member
+ public @NonNull String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * The active connection for a successful result.
+ */
+ @DataClass.Generated.Member
+ public @Nullable IScrollCaptureConnection getConnection() {
+ return mConnection;
+ }
+
+ /**
+ * The bounds of the window within the display
+ */
+ @DataClass.Generated.Member
+ public @Nullable Rect getWindowBounds() {
+ return mWindowBounds;
+ }
+
+ /**
+ * The bounds of the scrolling content, in window space.
+ */
+ @DataClass.Generated.Member
+ public @Nullable Rect getBoundsInWindow() {
+ return mBoundsInWindow;
+ }
+
+ /**
+ * The current window title.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String getWindowTitle() {
+ return mWindowTitle;
+ }
+
+ /**
+ * Carries additional logging and debugging information when enabled.
+ */
+ @DataClass.Generated.Member
+ public @NonNull ArrayList<String> getMessages() {
+ return mMessages;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "ScrollCaptureResponse { " +
+ "description = " + mDescription + ", " +
+ "connection = " + mConnection + ", " +
+ "windowBounds = " + mWindowBounds + ", " +
+ "boundsInWindow = " + mBoundsInWindow + ", " +
+ "windowTitle = " + mWindowTitle + ", " +
+ "messages = " + mMessages +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mConnection != null) flg |= 0x2;
+ if (mWindowBounds != null) flg |= 0x4;
+ if (mBoundsInWindow != null) flg |= 0x8;
+ if (mWindowTitle != null) flg |= 0x10;
+ dest.writeByte(flg);
+ dest.writeString(mDescription);
+ if (mConnection != null) dest.writeStrongInterface(mConnection);
+ if (mWindowBounds != null) dest.writeTypedObject(mWindowBounds, flags);
+ if (mBoundsInWindow != null) dest.writeTypedObject(mBoundsInWindow, flags);
+ if (mWindowTitle != null) dest.writeString(mWindowTitle);
+ dest.writeStringList(mMessages);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ protected ScrollCaptureResponse(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ String description = in.readString();
+ IScrollCaptureConnection connection = (flg & 0x2) == 0 ? null : IScrollCaptureConnection.Stub.asInterface(in.readStrongBinder());
+ Rect windowBounds = (flg & 0x4) == 0 ? null : (Rect) in.readTypedObject(Rect.CREATOR);
+ Rect boundsInWindow = (flg & 0x8) == 0 ? null : (Rect) in.readTypedObject(Rect.CREATOR);
+ String windowTitle = (flg & 0x10) == 0 ? null : in.readString();
+ ArrayList<String> messages = new ArrayList<>();
+ in.readStringList(messages);
+
+ this.mDescription = description;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mDescription);
+ this.mConnection = connection;
+ this.mWindowBounds = windowBounds;
+ this.mBoundsInWindow = boundsInWindow;
+ this.mWindowTitle = windowTitle;
+ this.mMessages = messages;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mMessages);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<ScrollCaptureResponse> CREATOR
+ = new Parcelable.Creator<ScrollCaptureResponse>() {
+ @Override
+ public ScrollCaptureResponse[] newArray(int size) {
+ return new ScrollCaptureResponse[size];
+ }
+
+ @Override
+ public ScrollCaptureResponse createFromParcel(@NonNull android.os.Parcel in) {
+ return new ScrollCaptureResponse(in);
+ }
+ };
+
+ /**
+ * A builder for {@link ScrollCaptureResponse}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static class Builder {
+
+ private @NonNull String mDescription;
+ private @Nullable IScrollCaptureConnection mConnection;
+ private @Nullable Rect mWindowBounds;
+ private @Nullable Rect mBoundsInWindow;
+ private @Nullable String mWindowTitle;
+ private @NonNull ArrayList<String> mMessages;
+
+ private long mBuilderFieldsSet = 0L;
+
+ public Builder() {
+ }
+
+ /**
+ * Developer-facing human readable description of the result.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setDescription(@NonNull String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mDescription = value;
+ return this;
+ }
+
+ /**
+ * The active connection for a successful result.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setConnection(@Nullable IScrollCaptureConnection value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mConnection = value;
+ return this;
+ }
+
+ /**
+ * The bounds of the window within the display
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setWindowBounds(@NonNull Rect value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mWindowBounds = value;
+ return this;
+ }
+
+ /**
+ * The bounds of the scrolling content, in window space.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setBoundsInWindow(@NonNull Rect value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8;
+ mBoundsInWindow = value;
+ return this;
+ }
+
+ /**
+ * The current window title.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setWindowTitle(@NonNull String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x10;
+ mWindowTitle = value;
+ return this;
+ }
+
+ /**
+ * Carries additional logging and debugging information when enabled.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setMessages(@NonNull ArrayList<String> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x20;
+ mMessages = value;
+ return this;
+ }
+
+ /** @see #setMessages */
+ @DataClass.Generated.Member
+ public @NonNull Builder addMessage(@NonNull String value) {
+ if (mMessages == null) setMessages(new ArrayList<>());
+ mMessages.add(value);
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull ScrollCaptureResponse build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x40; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ mDescription = "";
+ }
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mConnection = null;
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mWindowBounds = null;
+ }
+ if ((mBuilderFieldsSet & 0x8) == 0) {
+ mBoundsInWindow = null;
+ }
+ if ((mBuilderFieldsSet & 0x10) == 0) {
+ mWindowTitle = null;
+ }
+ if ((mBuilderFieldsSet & 0x20) == 0) {
+ mMessages = new ArrayList<>();
+ }
+ ScrollCaptureResponse o = new ScrollCaptureResponse(
+ mDescription,
+ mConnection,
+ mWindowBounds,
+ mBoundsInWindow,
+ mWindowTitle,
+ mMessages);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x40) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1614833185795L,
+ codegenVersion = "1.0.22",
+ sourceFile = "frameworks/base/core/java/android/view/ScrollCaptureResponse.java",
+ inputSignatures = "private @android.annotation.NonNull java.lang.String mDescription\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.MaySetToNull android.view.IScrollCaptureConnection mConnection\nprivate @android.annotation.Nullable android.graphics.Rect mWindowBounds\nprivate @android.annotation.Nullable android.graphics.Rect mBoundsInWindow\nprivate @android.annotation.Nullable java.lang.String mWindowTitle\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"message\") java.util.ArrayList<java.lang.String> mMessages\npublic boolean isConnected()\npublic void close()\nclass ScrollCaptureResponse extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true, genGetters=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/ScrollCaptureSearchResults.java b/android/view/ScrollCaptureSearchResults.java
new file mode 100644
index 0000000..3469b9d
--- /dev/null
+++ b/android/view/ScrollCaptureSearchResults.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.graphics.Rect;
+import android.os.CancellationSignal;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Collects nodes in the view hierarchy which have been identified as scrollable content.
+ *
+ * @hide
+ */
+@UiThread
+public final class ScrollCaptureSearchResults {
+ private final Executor mExecutor;
+ private final List<ScrollCaptureTarget> mTargets;
+ private final CancellationSignal mCancel;
+
+ private Runnable mOnCompleteListener;
+ private int mCompleted;
+ private boolean mComplete = true;
+
+ public ScrollCaptureSearchResults(Executor executor) {
+ mExecutor = executor;
+ mTargets = new ArrayList<>();
+ mCancel = new CancellationSignal();
+ }
+
+ // Public
+
+ /**
+ * Add the given target to the results.
+ *
+ * @param target the target to consider
+ */
+ public void addTarget(@NonNull ScrollCaptureTarget target) {
+ requireNonNull(target);
+
+ mTargets.add(target);
+ mComplete = false;
+ final ScrollCaptureCallback callback = target.getCallback();
+ final Consumer<Rect> consumer = new SearchRequest(target);
+
+ // Defer so the view hierarchy scan completes first
+ mExecutor.execute(
+ () -> callback.onScrollCaptureSearch(mCancel, consumer));
+ }
+
+ public boolean isComplete() {
+ return mComplete;
+ }
+
+ /**
+ * Provides a callback to be invoked as soon as all responses have been received from all
+ * targets to this point.
+ *
+ * @param onComplete listener to add
+ */
+ public void setOnCompleteListener(Runnable onComplete) {
+ if (mComplete) {
+ onComplete.run();
+ } else {
+ mOnCompleteListener = onComplete;
+ }
+ }
+
+ /**
+ * Indicates whether the search results are empty.
+ *
+ * @return true if no targets have been added
+ */
+ public boolean isEmpty() {
+ return mTargets.isEmpty();
+ }
+
+ /**
+ * Force the results to complete now, cancelling any pending requests and calling a complete
+ * listener if provided.
+ */
+ public void finish() {
+ if (!mComplete) {
+ mCancel.cancel();
+ signalComplete();
+ }
+ }
+
+ private void signalComplete() {
+ mComplete = true;
+ mTargets.sort(PRIORITY_ORDER);
+ if (mOnCompleteListener != null) {
+ mOnCompleteListener.run();
+ mOnCompleteListener = null;
+ }
+ }
+
+ @VisibleForTesting
+ public List<ScrollCaptureTarget> getTargets() {
+ return new ArrayList<>(mTargets);
+ }
+
+ /**
+ * Get the top ranked result out of all completed requests.
+ *
+ * @return the top ranked result
+ */
+ public ScrollCaptureTarget getTopResult() {
+ ScrollCaptureTarget target = mTargets.isEmpty() ? null : mTargets.get(0);
+ return target != null && target.getScrollBounds() != null ? target : null;
+ }
+
+ private class SearchRequest implements Consumer<Rect> {
+ private ScrollCaptureTarget mTarget;
+
+ SearchRequest(ScrollCaptureTarget target) {
+ mTarget = target;
+ }
+
+ @Override
+ public void accept(Rect scrollBounds) {
+ if (mTarget == null || mCancel.isCanceled()) {
+ return;
+ }
+ mExecutor.execute(() -> consume(scrollBounds));
+ }
+
+ private void consume(Rect scrollBounds) {
+ if (mTarget == null || mCancel.isCanceled()) {
+ return;
+ }
+ if (!nullOrEmpty(scrollBounds)) {
+ mTarget.setScrollBounds(scrollBounds);
+ mTarget.updatePositionInWindow();
+ }
+ mCompleted++;
+ mTarget = null;
+
+ // All done?
+ if (mCompleted == mTargets.size()) {
+ signalComplete();
+ }
+ }
+ }
+
+ private static final int AFTER = 1;
+ private static final int BEFORE = -1;
+ private static final int EQUAL = 0;
+
+ static final Comparator<ScrollCaptureTarget> PRIORITY_ORDER = (a, b) -> {
+ if (a == null && b == null) {
+ return 0;
+ } else if (a == null || b == null) {
+ return (a == null) ? 1 : -1;
+ }
+
+ boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds());
+ boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds());
+ if (emptyScrollBoundsA || emptyScrollBoundsB) {
+ if (emptyScrollBoundsA && emptyScrollBoundsB) {
+ return EQUAL;
+ }
+ // Prefer the one with a non-empty scroll bounds
+ if (emptyScrollBoundsA) {
+ return AFTER;
+ }
+ return BEFORE;
+ }
+
+ final View viewA = a.getContainingView();
+ final View viewB = b.getContainingView();
+
+ // Prefer any view with scrollCaptureHint="INCLUDE", over one without
+ // This is an escape hatch for the next rule (descendants first)
+ boolean hintIncludeA = hasIncludeHint(viewA);
+ boolean hintIncludeB = hasIncludeHint(viewB);
+ if (hintIncludeA != hintIncludeB) {
+ return (hintIncludeA) ? BEFORE : AFTER;
+ }
+ // If the views are relatives, prefer the descendant. This allows implementations to
+ // leverage nested scrolling APIs by interacting with the innermost scrollable view (as
+ // would happen with touch input).
+ if (isDescendant(viewA, viewB)) {
+ return BEFORE;
+ }
+ if (isDescendant(viewB, viewA)) {
+ return AFTER;
+ }
+
+ // finally, prefer one with larger scroll bounds
+ int scrollAreaA = area(a.getScrollBounds());
+ int scrollAreaB = area(b.getScrollBounds());
+ return (scrollAreaA >= scrollAreaB) ? BEFORE : AFTER;
+ };
+
+ private static int area(Rect r) {
+ return r.width() * r.height();
+ }
+
+ private static boolean nullOrEmpty(Rect r) {
+ return r == null || r.isEmpty();
+ }
+
+ private static boolean hasIncludeHint(View view) {
+ return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0;
+ }
+
+ /**
+ * Determines if {@code otherView} is a descendant of {@code view}.
+ *
+ * @param view a view
+ * @param otherView another view
+ * @return true if {@code view} is an ancestor of {@code otherView}
+ */
+ private static boolean isDescendant(@NonNull View view, @NonNull View otherView) {
+ if (view == otherView) {
+ return false;
+ }
+ ViewParent otherParent = otherView.getParent();
+ while (otherParent != view && otherParent != null) {
+ otherParent = otherParent.getParent();
+ }
+ return otherParent == view;
+ }
+
+ void dump(IndentingPrintWriter writer) {
+ writer.println("results:");
+ writer.increaseIndent();
+ writer.println("complete: " + isComplete());
+ writer.println("cancelled: " + mCancel.isCanceled());
+ writer.println("targets:");
+ writer.increaseIndent();
+ if (isEmpty()) {
+ writer.println("None");
+ } else {
+ for (int i = 0; i < mTargets.size(); i++) {
+ writer.println("[" + i + "]");
+ writer.increaseIndent();
+ mTargets.get(i).dump(writer);
+ writer.decreaseIndent();
+ }
+ writer.decreaseIndent();
+ }
+ writer.decreaseIndent();
+ }
+}
diff --git a/android/view/ScrollCaptureSession.java b/android/view/ScrollCaptureSession.java
new file mode 100644
index 0000000..748e7ea
--- /dev/null
+++ b/android/view/ScrollCaptureSession.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * A session represents the scope of interaction between a {@link ScrollCaptureCallback} and the
+ * system during an active scroll capture operation.
+ */
+public class ScrollCaptureSession {
+
+ private final Surface mSurface;
+ private final Rect mScrollBounds;
+ private final Point mPositionInWindow;
+
+ /**
+ * Constructs a new session instance.
+ *
+ * @param surface the surface to consume generated images
+ * @param scrollBounds the bounds of the capture area within the containing view
+ * @param positionInWindow the offset of scrollBounds within the window
+ */
+ public ScrollCaptureSession(@NonNull Surface surface, @NonNull Rect scrollBounds,
+ @NonNull Point positionInWindow) {
+ mSurface = requireNonNull(surface);
+ mScrollBounds = requireNonNull(scrollBounds);
+ mPositionInWindow = requireNonNull(positionInWindow);
+ }
+
+ /**
+ * Returns a
+ * <a href="https://source.android.com/devices/graphics/arch-bq-gralloc">BufferQueue</a> in the
+ * form of a {@link Surface} for transfer of image buffers.
+ * <p>
+ * The surface is guaranteed to remain {@link Surface#isValid() valid} until the session
+ * {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable) ends}.
+ *
+ * @return the surface for transferring image buffers
+ * @throws IllegalStateException if the session has been closed
+ */
+ @NonNull
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ /**
+ * Returns the {@code scroll bounds}, as provided by
+ * {@link ScrollCaptureCallback#onScrollCaptureSearch}.
+ *
+ * @return the area of scrolling content within the containing view
+ */
+ @NonNull
+ public Rect getScrollBounds() {
+ return mScrollBounds;
+ }
+
+ /**
+ * Returns the offset of {@code scroll bounds} within the window.
+ *
+ * @return the area of scrolling content within the containing view
+ */
+ @NonNull
+ public Point getPositionInWindow() {
+ return mPositionInWindow;
+ }
+}
diff --git a/android/view/ScrollCaptureTarget.java b/android/view/ScrollCaptureTarget.java
new file mode 100644
index 0000000..a8bb037
--- /dev/null
+++ b/android/view/ScrollCaptureTarget.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.CancellationSignal;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * A target collects the set of contextual information for a ScrollCaptureHandler discovered during
+ * a {@link View#dispatchScrollCaptureSearch scroll capture search}.
+ */
+public final class ScrollCaptureTarget {
+ private final View mContainingView;
+ private final ScrollCaptureCallback mCallback;
+ private final Rect mLocalVisibleRect;
+ private final Point mPositionInWindow;
+ private final int mHint;
+ private Rect mScrollBounds;
+
+ private final int[] mTmpIntArr = new int[2];
+
+ public ScrollCaptureTarget(@NonNull View scrollTarget, @NonNull Rect localVisibleRect,
+ @NonNull Point positionInWindow, @NonNull ScrollCaptureCallback callback) {
+ mContainingView = requireNonNull(scrollTarget);
+ mHint = mContainingView.getScrollCaptureHint();
+ mCallback = requireNonNull(callback);
+ mLocalVisibleRect = requireNonNull(localVisibleRect);
+ mPositionInWindow = requireNonNull(positionInWindow);
+ }
+
+ /**
+ * @return the hint that the {@code containing view} had during the scroll capture search
+ * @see View#getScrollCaptureHint()
+ */
+ @View.ScrollCaptureHint
+ public int getHint() {
+ return mHint;
+ }
+
+ /** @return the {@link ScrollCaptureCallback} for this target */
+ @NonNull
+ public ScrollCaptureCallback getCallback() {
+ return mCallback;
+ }
+
+ /** @return the {@code containing view} for this {@link ScrollCaptureCallback callback} */
+ @NonNull
+ public View getContainingView() {
+ return mContainingView;
+ }
+
+ /**
+ * Returns the visible bounds of the containing view.
+ *
+ * @return the visible bounds of the {@code containing view} in view-local coordinates
+ */
+ @NonNull
+ public Rect getLocalVisibleRect() {
+ return mLocalVisibleRect;
+ }
+
+ /** @return the position of the visible bounds of the containing view within the window */
+ @NonNull
+ public Point getPositionInWindow() {
+ return mPositionInWindow;
+ }
+
+ /**
+ * @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback}
+ *
+ * @see ScrollCaptureCallback#onScrollCaptureSearch(CancellationSignal, Consumer)
+ */
+ @Nullable
+ public Rect getScrollBounds() {
+ return mScrollBounds;
+ }
+
+ /**
+ * Sets the scroll bounds rect to the intersection of provided rect and the current bounds of
+ * the {@code containing view}.
+ */
+ public void setScrollBounds(@Nullable Rect scrollBounds) {
+ mScrollBounds = Rect.copyOrNull(scrollBounds);
+ if (mScrollBounds == null) {
+ return;
+ }
+ if (!mScrollBounds.intersect(0, 0,
+ mContainingView.getWidth(), mContainingView.getHeight())) {
+ mScrollBounds.setEmpty();
+ }
+ }
+
+ /**
+ * Refresh the local visible bounds and its offset within the window, based on the current
+ * state of the {@code containing view}.
+ */
+ @UiThread
+ public void updatePositionInWindow() {
+ mContainingView.getLocationInWindow(mTmpIntArr);
+ mPositionInWindow.x = mTmpIntArr[0];
+ mPositionInWindow.y = mTmpIntArr[1];
+ }
+
+ public String toString() {
+ return "ScrollCaptureTarget{" + "view=" + mContainingView
+ + ", callback=" + mCallback
+ + ", scrollBounds=" + mScrollBounds
+ + ", localVisibleRect=" + mLocalVisibleRect
+ + ", positionInWindow=" + mPositionInWindow
+ + "}";
+ }
+
+ void dump(@NonNull PrintWriter writer) {
+ View view = getContainingView();
+ writer.println("view: " + view);
+ writer.println("hint: " + mHint);
+ writer.println("callback: " + mCallback);
+ writer.println("scrollBounds: "
+ + (mScrollBounds == null ? "null" : mScrollBounds.toShortString()));
+ Point inWindow = getPositionInWindow();
+ writer.println("positionInWindow: "
+ + ((inWindow == null) ? "null" : "[" + inWindow.x + "," + inWindow.y + "]"));
+ Rect localVisible = getLocalVisibleRect();
+ writer.println("localVisibleRect: "
+ + (localVisible == null ? "null" : localVisible.toShortString()));
+ }
+}
diff --git a/android/view/SearchEvent.java b/android/view/SearchEvent.java
new file mode 100644
index 0000000..72b5e4b
--- /dev/null
+++ b/android/view/SearchEvent.java
@@ -0,0 +1,38 @@
+/*
+ * 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.view;
+
+/**
+ * Class that contains information about an event that triggers a search.
+ */
+public class SearchEvent {
+
+ private InputDevice mInputDevice;
+
+ /** Create a new search event. */
+ public SearchEvent(InputDevice inputDevice) {
+ mInputDevice = inputDevice;
+ }
+
+ /**
+ * Returns the {@link InputDevice} that triggered the search.
+ * @return InputDevice the InputDevice that triggered the search.
+ */
+ public InputDevice getInputDevice() {
+ return mInputDevice;
+ }
+}
diff --git a/android/view/ShadowPainter.java b/android/view/ShadowPainter.java
new file mode 100644
index 0000000..788c6c3
--- /dev/null
+++ b/android/view/ShadowPainter.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.imageio.ImageIO;
+
+public class ShadowPainter {
+
+ /**
+ * Adds a drop shadow to a semi-transparent image (of an arbitrary shape) and returns it as a
+ * new image. This method attempts to mimic the same visual characteristics as the rectangular
+ * shadow painting methods in this class, {@link #createRectangularDropShadow(java.awt.image.BufferedImage)}
+ * and {@link #createSmallRectangularDropShadow(java.awt.image.BufferedImage)}.
+ * <p/>
+ * If shadowSize is less or equals to 1, no shadow will be painted and the source image will be
+ * returned instead.
+ *
+ * @param source the source image
+ * @param shadowSize the size of the shadow, normally {@link #SHADOW_SIZE or {@link
+ * #SMALL_SHADOW_SIZE}}
+ * @param alpha alpha value to apply to the shadow
+ *
+ * @return an image with the shadow painted in or the source image if shadowSize <= 1
+ */
+ @NonNull
+ public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, float
+ alpha) {
+ shadowSize /= 2; // make shadow size have the same meaning as in the other shadow paint methods in this class
+
+ return createDropShadow(source, shadowSize, 0.7f * alpha, 0);
+ }
+
+ /**
+ * Creates a drop shadow of a given image and returns a new image which shows the input image on
+ * top of its drop shadow.
+ * <p/>
+ * <b>NOTE: If the shape is rectangular and opaque, consider using {@link
+ * #drawRectangleShadow(Graphics2D, int, int, int, int)} instead.</b>
+ *
+ * @param source the source image to be shadowed
+ * @param shadowSize the size of the shadow in pixels
+ * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
+ * @param shadowRgb the RGB int to use for the shadow color
+ *
+ * @return a new image with the source image on top of its shadow when shadowSize > 0 or the
+ * source image otherwise
+ */
+ @SuppressWarnings({"SuspiciousNameCombination", "UnnecessaryLocalVariable"}) // Imported code
+ public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
+ float shadowOpacity, int shadowRgb) {
+ if (shadowSize <= 0) {
+ return source;
+ }
+
+ // This code is based on
+ // http://www.jroller.com/gfx/entry/non_rectangular_shadow
+
+ BufferedImage image;
+ int width = source.getWidth();
+ int height = source.getHeight();
+ image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = image.createGraphics();
+ g2.drawImage(image, shadowSize, shadowSize, null);
+
+ int dstWidth = image.getWidth();
+ int dstHeight = image.getHeight();
+
+ int left = (shadowSize - 1) >> 1;
+ int right = shadowSize - left;
+ int xStart = left;
+ int xStop = dstWidth - right;
+ int yStart = left;
+ int yStop = dstHeight - right;
+
+ shadowRgb &= 0x00FFFFFF;
+
+ int[] aHistory = new int[shadowSize];
+ int historyIdx;
+
+ int aSum;
+
+ int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+ int lastPixelOffset = right * dstWidth;
+ float sumDivider = shadowOpacity / shadowSize;
+
+ // horizontal pass
+ for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
+ aSum = 0;
+ historyIdx = 0;
+ for (int x = 0; x < shadowSize; x++, bufferOffset++) {
+ int a = dataBuffer[bufferOffset] >>> 24;
+ aHistory[x] = a;
+ aSum += a;
+ }
+
+ bufferOffset -= right;
+
+ for (int x = xStart; x < xStop; x++, bufferOffset++) {
+ int a = (int) (aSum * sumDivider);
+ dataBuffer[bufferOffset] = a << 24 | shadowRgb;
+
+ // subtract the oldest pixel from the sum
+ aSum -= aHistory[historyIdx];
+
+ // get the latest pixel
+ a = dataBuffer[bufferOffset + right] >>> 24;
+ aHistory[historyIdx] = a;
+ aSum += a;
+
+ if (++historyIdx >= shadowSize) {
+ historyIdx -= shadowSize;
+ }
+ }
+ }
+ // vertical pass
+ for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
+ aSum = 0;
+ historyIdx = 0;
+ for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
+ int a = dataBuffer[bufferOffset] >>> 24;
+ aHistory[y] = a;
+ aSum += a;
+ }
+
+ bufferOffset -= lastPixelOffset;
+
+ for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
+ int a = (int) (aSum * sumDivider);
+ dataBuffer[bufferOffset] = a << 24 | shadowRgb;
+
+ // subtract the oldest pixel from the sum
+ aSum -= aHistory[historyIdx];
+
+ // get the latest pixel
+ a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
+ aHistory[historyIdx] = a;
+ aSum += a;
+
+ if (++historyIdx >= shadowSize) {
+ historyIdx -= shadowSize;
+ }
+ }
+ }
+
+ g2.drawImage(source, null, 0, 0);
+ g2.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by {@link #SHADOW_SIZE} around
+ * the given source and returns a new image with both combined
+ *
+ * @param source the source image
+ *
+ * @return the source image with a drop shadow on the bottom and right
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public static BufferedImage createRectangularDropShadow(BufferedImage source) {
+ int type = source.getType();
+ if (type == BufferedImage.TYPE_CUSTOM) {
+ type = BufferedImage.TYPE_INT_ARGB;
+ }
+
+ int width = source.getWidth();
+ int height = source.getHeight();
+ BufferedImage image;
+ image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
+ Graphics2D g = image.createGraphics();
+ g.drawImage(source, 0, 0, null);
+ drawRectangleShadow(image, 0, 0, width, height);
+ g.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a small rectangular drop shadow (of size {@link #SMALL_SHADOW_SIZE} by {@link
+ * #SMALL_SHADOW_SIZE} around the given source and returns a new image with both combined
+ *
+ * @param source the source image
+ *
+ * @return the source image with a drop shadow on the bottom and right
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public static BufferedImage createSmallRectangularDropShadow(BufferedImage source) {
+ int type = source.getType();
+ if (type == BufferedImage.TYPE_CUSTOM) {
+ type = BufferedImage.TYPE_INT_ARGB;
+ }
+
+ int width = source.getWidth();
+ int height = source.getHeight();
+
+ BufferedImage image;
+ image = new BufferedImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE, type);
+
+ Graphics2D g = image.createGraphics();
+ g.drawImage(source, 0, 0, null);
+ drawSmallRectangleShadow(image, 0, 0, width, height);
+ g.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It will not draw anything
+ * if the rectangle is smaller than a minimum determined by the assets used to draw the shadow
+ * graphics. The size of the shadow is {@link #SHADOW_SIZE}.
+ *
+ * @param image the image to draw the shadow into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static void drawRectangleShadow(BufferedImage image,
+ int x, int y, int width, int height) {
+ Graphics2D gc = image.createGraphics();
+ try {
+ drawRectangleShadow(gc, x, y, width, height);
+ } finally {
+ gc.dispose();
+ }
+ }
+
+ /**
+ * Draws a small drop shadow for the given rectangle into the given context. It will not draw
+ * anything if the rectangle is smaller than a minimum determined by the assets used to draw the
+ * shadow graphics. The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
+ *
+ * @param image the image to draw the shadow into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static void drawSmallRectangleShadow(BufferedImage image,
+ int x, int y, int width, int height) {
+ Graphics2D gc = image.createGraphics();
+ try {
+ drawSmallRectangleShadow(gc, x, y, width, height);
+ } finally {
+ gc.dispose();
+ }
+ }
+
+ /**
+ * The width and height of the drop shadow painted by
+ * {@link #drawRectangleShadow(Graphics2D, int, int, int, int)}
+ */
+ public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
+
+ /**
+ * The width and height of the drop shadow painted by
+ * {@link #drawSmallRectangleShadow(Graphics2D, int, int, int, int)}
+ */
+ public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It will not draw anything
+ * if the rectangle is smaller than a minimum determined by the assets used to draw the shadow
+ * graphics.
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static void drawRectangleShadow(Graphics2D gc, int x, int y, int width, int height) {
+ assert ShadowBottomLeft != null;
+ assert ShadowBottomRight.getWidth(null) == SHADOW_SIZE;
+ assert ShadowBottomRight.getHeight(null) == SHADOW_SIZE;
+
+ int blWidth = ShadowBottomLeft.getWidth(null);
+ int trHeight = ShadowTopRight.getHeight(null);
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(ShadowBottomLeft, x - ShadowBottomLeft.getWidth(null), y + height, null);
+ gc.drawImage(ShadowBottomRight, x + width, y + height, null);
+ gc.drawImage(ShadowTopRight, x + width, y, null);
+ gc.drawImage(ShadowTopLeft, x - ShadowTopLeft.getWidth(null), y, null);
+ gc.drawImage(ShadowBottom,
+ x, y + height, x + width, y + height + ShadowBottom.getHeight(null),
+ 0, 0, ShadowBottom.getWidth(null), ShadowBottom.getHeight(null), null);
+ gc.drawImage(ShadowRight,
+ x + width, y + ShadowTopRight.getHeight(null), x + width + ShadowRight.getWidth(null), y + height,
+ 0, 0, ShadowRight.getWidth(null), ShadowRight.getHeight(null), null);
+ gc.drawImage(ShadowLeft,
+ x - ShadowLeft.getWidth(null), y + ShadowTopLeft.getHeight(null), x, y + height,
+ 0, 0, ShadowLeft.getWidth(null), ShadowLeft.getHeight(null), null);
+ }
+
+ /**
+ * Draws a small drop shadow for the given rectangle into the given context. It will not draw
+ * anything if the rectangle is smaller than a minimum determined by the assets used to draw the
+ * shadow graphics.
+ * <p/>
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static void drawSmallRectangleShadow(Graphics2D gc, int x, int y, int width,
+ int height) {
+ assert Shadow2BottomLeft != null;
+ assert Shadow2TopRight != null;
+ assert Shadow2BottomRight.getWidth(null) == SMALL_SHADOW_SIZE;
+ assert Shadow2BottomRight.getHeight(null) == SMALL_SHADOW_SIZE;
+
+ int blWidth = Shadow2BottomLeft.getWidth(null);
+ int trHeight = Shadow2TopRight.getHeight(null);
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(Shadow2BottomLeft, x - Shadow2BottomLeft.getWidth(null), y + height, null);
+ gc.drawImage(Shadow2BottomRight, x + width, y + height, null);
+ gc.drawImage(Shadow2TopRight, x + width, y, null);
+ gc.drawImage(Shadow2TopLeft, x - Shadow2TopLeft.getWidth(null), y, null);
+ gc.drawImage(Shadow2Bottom,
+ x, y + height, x + width, y + height + Shadow2Bottom.getHeight(null),
+ 0, 0, Shadow2Bottom.getWidth(null), Shadow2Bottom.getHeight(null), null);
+ gc.drawImage(Shadow2Right,
+ x + width, y + Shadow2TopRight.getHeight(null), x + width + Shadow2Right.getWidth(null), y + height,
+ 0, 0, Shadow2Right.getWidth(null), Shadow2Right.getHeight(null), null);
+ gc.drawImage(Shadow2Left,
+ x - Shadow2Left.getWidth(null), y + Shadow2TopLeft.getHeight(null), x, y + height,
+ 0, 0, Shadow2Left.getWidth(null), Shadow2Left.getHeight(null), null);
+ }
+
+ private static Image loadIcon(String name) {
+ InputStream inputStream = ShadowPainter.class.getResourceAsStream(name);
+ if (inputStream == null) {
+ throw new RuntimeException("Unable to load image for shadow: " + name);
+ }
+ try {
+ return ImageIO.read(inputStream);
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to load image for shadow:" + name, e);
+ } finally {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ // ignore.
+ }
+ }
+ }
+
+ // Shadow graphics. This was generated by creating a drop shadow in
+ // Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
+ // (for the small drop shadows x offset=10, y offset=10, blur radius=10)
+ // color=black, and opacity=51. These values attempt to make a shadow
+ // that is legible both for dark and light themes, on top of the
+ // canvas background (rgb(150,150,150). Darker shadows would tend to
+ // blend into the foreground for a dark holo screen, and lighter shadows
+ // would be hard to spot on the canvas background. If you make adjustments,
+ // make sure to check the shadow with both dark and light themes.
+ //
+ // After making the graphics, I cut out the top right, bottom left
+ // and bottom right corners as 20x20 images, and these are reproduced by
+ // painting them in the corresponding places in the target graphics context.
+ // I then grabbed a single horizontal gradient line from the middle of the
+ // right edge,and a single vertical gradient line from the bottom. These
+ // are then painted scaled/stretched in the target to fill the gaps between
+ // the three corner images.
+ //
+ // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
+
+ // Normal Drop Shadow
+ private static final Image ShadowBottom = loadIcon("/icons/shadow-b.png");
+ private static final Image ShadowBottomLeft = loadIcon("/icons/shadow-bl.png");
+ private static final Image ShadowBottomRight = loadIcon("/icons/shadow-br.png");
+ private static final Image ShadowRight = loadIcon("/icons/shadow-r.png");
+ private static final Image ShadowTopRight = loadIcon("/icons/shadow-tr.png");
+ private static final Image ShadowTopLeft = loadIcon("/icons/shadow-tl.png");
+ private static final Image ShadowLeft = loadIcon("/icons/shadow-l.png");
+
+ // Small Drop Shadow
+ private static final Image Shadow2Bottom = loadIcon("/icons/shadow2-b.png");
+ private static final Image Shadow2BottomLeft = loadIcon("/icons/shadow2-bl.png");
+ private static final Image Shadow2BottomRight = loadIcon("/icons/shadow2-br.png");
+ private static final Image Shadow2Right = loadIcon("/icons/shadow2-r.png");
+ private static final Image Shadow2TopRight = loadIcon("/icons/shadow2-tr.png");
+ private static final Image Shadow2TopLeft = loadIcon("/icons/shadow2-tl.png");
+ private static final Image Shadow2Left = loadIcon("/icons/shadow2-l.png");
+}
diff --git a/android/view/SoundEffectConstants.java b/android/view/SoundEffectConstants.java
new file mode 100644
index 0000000..bd86a47
--- /dev/null
+++ b/android/view/SoundEffectConstants.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.media.AudioManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Random;
+
+/**
+ * Constants to be used to play sound effects via {@link View#playSoundEffect(int)}
+ */
+public class SoundEffectConstants {
+
+ private SoundEffectConstants() {}
+ private static final Random NAVIGATION_REPEAT_RANDOMIZER = new Random();
+ private static int sLastNavigationRepeatSoundEffectId = -1;
+
+ public static final int CLICK = 0;
+
+ /** Effect id for a navigation left */
+ public static final int NAVIGATION_LEFT = 1;
+ /** Effect id for a navigation up */
+ public static final int NAVIGATION_UP = 2;
+ /** Effect id for a navigation right */
+ public static final int NAVIGATION_RIGHT = 3;
+ /** Effect id for a navigation down */
+ public static final int NAVIGATION_DOWN = 4;
+ /** Effect id for a repeatedly triggered navigation left, e.g. due to long pressing a button */
+ public static final int NAVIGATION_REPEAT_LEFT = 5;
+ /** Effect id for a repeatedly triggered navigation up, e.g. due to long pressing a button */
+ public static final int NAVIGATION_REPEAT_UP = 6;
+ /** Effect id for a repeatedly triggered navigation right, e.g. due to long pressing a button */
+ public static final int NAVIGATION_REPEAT_RIGHT = 7;
+ /** Effect id for a repeatedly triggered navigation down, e.g. due to long pressing a button */
+ public static final int NAVIGATION_REPEAT_DOWN = 8;
+
+ /** @hide */
+ @IntDef(value = {
+ CLICK,
+ NAVIGATION_LEFT,
+ NAVIGATION_UP,
+ NAVIGATION_RIGHT,
+ NAVIGATION_DOWN,
+ NAVIGATION_REPEAT_LEFT,
+ NAVIGATION_REPEAT_UP,
+ NAVIGATION_REPEAT_RIGHT,
+ NAVIGATION_REPEAT_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SoundEffect {}
+
+ /** @hide */
+ @IntDef(prefix = { "NAVIGATION_" }, value = {
+ NAVIGATION_LEFT,
+ NAVIGATION_UP,
+ NAVIGATION_RIGHT,
+ NAVIGATION_DOWN,
+ NAVIGATION_REPEAT_LEFT,
+ NAVIGATION_REPEAT_UP,
+ NAVIGATION_REPEAT_RIGHT,
+ NAVIGATION_REPEAT_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NavigationSoundEffect {}
+
+ /**
+ * Get the sonification constant for the focus directions.
+ * @param direction The direction of the focus.
+ * @return The appropriate sonification constant.
+ * @throws {@link IllegalArgumentException} when the passed direction is not one of the
+ * documented values.
+ */
+ public static int getContantForFocusDirection(@View.FocusDirection int direction) {
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ return SoundEffectConstants.NAVIGATION_RIGHT;
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_DOWN:
+ return SoundEffectConstants.NAVIGATION_DOWN;
+ case View.FOCUS_LEFT:
+ return SoundEffectConstants.NAVIGATION_LEFT;
+ case View.FOCUS_BACKWARD:
+ case View.FOCUS_UP:
+ return SoundEffectConstants.NAVIGATION_UP;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}.");
+ }
+
+ /**
+ * Get the sonification constant for the focus directions
+ * @param direction The direction of the focus.
+ * @param repeating True if the user long-presses a direction
+ * @return The appropriate sonification constant
+ * @throws IllegalArgumentException when the passed direction is not one of the
+ * documented values.
+ */
+ public static @NavigationSoundEffect int getConstantForFocusDirection(
+ @View.FocusDirection int direction, boolean repeating) {
+ if (repeating) {
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ return SoundEffectConstants.NAVIGATION_REPEAT_RIGHT;
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_DOWN:
+ return SoundEffectConstants.NAVIGATION_REPEAT_DOWN;
+ case View.FOCUS_LEFT:
+ return SoundEffectConstants.NAVIGATION_REPEAT_LEFT;
+ case View.FOCUS_BACKWARD:
+ case View.FOCUS_UP:
+ return SoundEffectConstants.NAVIGATION_REPEAT_UP;
+ }
+ throw new IllegalArgumentException("direction must be one of {FOCUS_UP, FOCUS_DOWN, "
+ + "FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}.");
+ } else {
+ return getContantForFocusDirection(direction);
+ }
+ }
+
+ /**
+ * @param effectId any of the effect ids defined in {@link SoundEffectConstants}
+ * @return true if the given effect id is a navigation repeat one
+ * @hide
+ */
+ @VisibleForTesting(visibility = Visibility.PACKAGE)
+ public static boolean isNavigationRepeat(@NavigationSoundEffect int effectId) {
+ return effectId == SoundEffectConstants.NAVIGATION_REPEAT_DOWN
+ || effectId == SoundEffectConstants.NAVIGATION_REPEAT_LEFT
+ || effectId == SoundEffectConstants.NAVIGATION_REPEAT_RIGHT
+ || effectId == SoundEffectConstants.NAVIGATION_REPEAT_UP;
+ }
+
+ /**
+ * @return The next navigation repeat sound effect id, chosen at random in a non-repeating
+ * fashion
+ * @hide
+ */
+ @VisibleForTesting(visibility = Visibility.PACKAGE)
+ public static int nextNavigationRepeatSoundEffectId() {
+ int next = NAVIGATION_REPEAT_RANDOMIZER.nextInt(
+ AudioManager.NUM_NAVIGATION_REPEAT_SOUND_EFFECTS - 1);
+ if (next >= sLastNavigationRepeatSoundEffectId) {
+ next++;
+ }
+ sLastNavigationRepeatSoundEffectId = next;
+ return AudioManager.getNthNavigationRepeatSoundEffect(next);
+ }
+}
diff --git a/android/view/SubMenu.java b/android/view/SubMenu.java
new file mode 100644
index 0000000..38662b0
--- /dev/null
+++ b/android/view/SubMenu.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Subclass of {@link Menu} for sub menus.
+ * <p>
+ * Sub menus do not support item icons, or nested sub menus.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about creating menus, read the
+ * <a href="{@docRoot}guide/topics/ui/menus.html">Menus</a> developer guide.</p>
+ * </div>
+ */
+
+public interface SubMenu extends Menu {
+ /**
+ * Sets the submenu header's title to the title given in <var>titleRes</var>
+ * resource identifier.
+ *
+ * @param titleRes The string resource identifier used for the title.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderTitle(@StringRes int titleRes);
+
+ /**
+ * Sets the submenu header's title to the title given in <var>title</var>.
+ *
+ * @param title The character sequence used for the title.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderTitle(CharSequence title);
+
+ /**
+ * Sets the submenu header's icon to the icon given in <var>iconRes</var>
+ * resource id.
+ *
+ * @param iconRes The resource identifier used for the icon.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderIcon(@DrawableRes int iconRes);
+
+ /**
+ * Sets the submenu header's icon to the icon given in <var>icon</var>
+ * {@link Drawable}.
+ *
+ * @param icon The {@link Drawable} used for the icon.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderIcon(Drawable icon);
+
+ /**
+ * Sets the header of the submenu to the {@link View} given in
+ * <var>view</var>. This replaces the header title and icon (and those
+ * replace this).
+ *
+ * @param view The {@link View} used for the header.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderView(View view);
+
+ /**
+ * Clears the header of the submenu.
+ */
+ public void clearHeader();
+
+ /**
+ * Change the icon associated with this submenu's item in its parent menu.
+ *
+ * @see MenuItem#setIcon(int)
+ * @param iconRes The new icon (as a resource ID) to be displayed.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setIcon(@DrawableRes int iconRes);
+
+ /**
+ * Change the icon associated with this submenu's item in its parent menu.
+ *
+ * @see MenuItem#setIcon(Drawable)
+ * @param icon The new icon (as a Drawable) to be displayed.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setIcon(Drawable icon);
+
+ /**
+ * Gets the {@link MenuItem} that represents this submenu in the parent
+ * menu. Use this for setting additional item attributes.
+ *
+ * @return The {@link MenuItem} that launches the submenu when invoked.
+ */
+ public MenuItem getItem();
+}
diff --git a/android/view/Surface.java b/android/view/Surface.java
new file mode 100644
index 0000000..ff2d2eb
--- /dev/null
+++ b/android/view/Surface.java
@@ -0,0 +1,1120 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.system.OsConstants.EINVAL;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.pm.ActivityInfo;
+import android.content.res.CompatibilityInfo.Translator;
+import android.graphics.BLASTBufferQueue;
+import android.graphics.Canvas;
+import android.graphics.ColorSpace;
+import android.graphics.HardwareRenderer;
+import android.graphics.Matrix;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.SurfaceTexture;
+import android.hardware.HardwareBuffer;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handle onto a raw buffer that is being managed by the screen compositor.
+ *
+ * <p>A Surface is generally created by or from a consumer of image buffers (such as a
+ * {@link android.graphics.SurfaceTexture}, {@link android.media.MediaRecorder}, or
+ * {@link android.renderscript.Allocation}), and is handed to some kind of producer (such as
+ * {@link android.opengl.EGL14#eglCreateWindowSurface(android.opengl.EGLDisplay,android.opengl.EGLConfig,java.lang.Object,int[],int) OpenGL},
+ * {@link android.media.MediaPlayer#setSurface MediaPlayer}, or
+ * {@link android.hardware.camera2.CameraDevice#createCaptureSession CameraDevice}) to draw
+ * into.</p>
+ *
+ * <p><strong>Note:</strong> A Surface acts like a
+ * {@link java.lang.ref.WeakReference weak reference} to the consumer it is associated with. By
+ * itself it will not keep its parent consumer from being reclaimed.</p>
+ */
+public class Surface implements Parcelable {
+ private static final String TAG = "Surface";
+
+ private static native long nativeCreateFromSurfaceTexture(SurfaceTexture surfaceTexture)
+ throws OutOfResourcesException;
+
+ private static native long nativeCreateFromSurfaceControl(long surfaceControlNativeObject);
+ private static native long nativeGetFromSurfaceControl(long surfaceObject,
+ long surfaceControlNativeObject);
+ private static native long nativeGetFromBlastBufferQueue(long surfaceObject,
+ long blastBufferQueueNativeObject);
+
+ private static native long nativeLockCanvas(long nativeObject, Canvas canvas, Rect dirty)
+ throws OutOfResourcesException;
+ private static native void nativeUnlockCanvasAndPost(long nativeObject, Canvas canvas);
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static native void nativeRelease(long nativeObject);
+ private static native boolean nativeIsValid(long nativeObject);
+ private static native boolean nativeIsConsumerRunningBehind(long nativeObject);
+ private static native long nativeReadFromParcel(long nativeObject, Parcel source);
+ private static native void nativeWriteToParcel(long nativeObject, Parcel dest);
+
+ private static native void nativeAllocateBuffers(long nativeObject);
+
+ private static native int nativeGetWidth(long nativeObject);
+ private static native int nativeGetHeight(long nativeObject);
+
+ private static native long nativeGetNextFrameNumber(long nativeObject);
+ private static native int nativeSetScalingMode(long nativeObject, int scalingMode);
+ private static native int nativeForceScopedDisconnect(long nativeObject);
+ private static native int nativeAttachAndQueueBufferWithColorSpace(long nativeObject,
+ HardwareBuffer buffer, int colorSpaceId);
+
+ private static native int nativeSetSharedBufferModeEnabled(long nativeObject, boolean enabled);
+ private static native int nativeSetAutoRefreshEnabled(long nativeObject, boolean enabled);
+
+ private static native int nativeSetFrameRate(
+ long nativeObject, float frameRate, int compatibility, int changeFrameRateStrategy);
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Surface> CREATOR =
+ new Parcelable.Creator<Surface>() {
+ @Override
+ public Surface createFromParcel(Parcel source) {
+ try {
+ Surface s = new Surface();
+ s.readFromParcel(source);
+ return s;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception creating surface from parcel", e);
+ return null;
+ }
+ }
+
+ @Override
+ public Surface[] newArray(int size) {
+ return new Surface[size];
+ }
+ };
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ // Guarded state.
+ @UnsupportedAppUsage
+ final Object mLock = new Object(); // protects the native state
+ @UnsupportedAppUsage
+ private String mName;
+ @UnsupportedAppUsage
+ long mNativeObject; // package scope only for SurfaceControl access
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private long mLockedObject;
+ private int mGenerationId; // incremented each time mNativeObject changes
+ private final Canvas mCanvas = new CompatibleCanvas();
+
+ // A matrix to scale the matrix set by application. This is set to null for
+ // non compatibility mode.
+ private Matrix mCompatibleMatrix;
+
+ private HwuiContext mHwuiContext;
+
+ private boolean mIsSingleBuffered;
+ private boolean mIsSharedBufferModeEnabled;
+ private boolean mIsAutoRefreshEnabled;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "SCALING_MODE_" }, value = {
+ SCALING_MODE_FREEZE,
+ SCALING_MODE_SCALE_TO_WINDOW,
+ SCALING_MODE_SCALE_CROP,
+ SCALING_MODE_NO_SCALE_CROP
+ })
+ public @interface ScalingMode {}
+ // From system/window.h
+ /** @hide */
+ public static final int SCALING_MODE_FREEZE = 0;
+ /** @hide */
+ public static final int SCALING_MODE_SCALE_TO_WINDOW = 1;
+ /** @hide */
+ public static final int SCALING_MODE_SCALE_CROP = 2;
+ /** @hide */
+ public static final int SCALING_MODE_NO_SCALE_CROP = 3;
+
+ /** @hide */
+ @IntDef(prefix = { "ROTATION_" }, value = {
+ ROTATION_0,
+ ROTATION_90,
+ ROTATION_180,
+ ROTATION_270
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Rotation {}
+
+ /**
+ * Rotation constant: 0 degree rotation (natural orientation)
+ */
+ public static final int ROTATION_0 = 0;
+
+ /**
+ * Rotation constant: 90 degree rotation.
+ */
+ public static final int ROTATION_90 = 1;
+
+ /**
+ * Rotation constant: 180 degree rotation.
+ */
+ public static final int ROTATION_180 = 2;
+
+ /**
+ * Rotation constant: 270 degree rotation.
+ */
+ public static final int ROTATION_270 = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"FRAME_RATE_COMPATIBILITY_"},
+ value = {FRAME_RATE_COMPATIBILITY_DEFAULT, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE})
+ public @interface FrameRateCompatibility {}
+
+ // From native_window.h. Keep these in sync.
+ /**
+ * There are no inherent restrictions on the frame rate of this surface. When the
+ * system selects a frame rate other than what the app requested, the app will be able
+ * to run at the system frame rate without requiring pull down. This value should be
+ * used when displaying game content, UIs, and anything that isn't video.
+ */
+ public static final int FRAME_RATE_COMPATIBILITY_DEFAULT = 0;
+
+ /**
+ * This surface is being used to display content with an inherently fixed frame rate,
+ * e.g. a video that has a specific frame rate. When the system selects a frame rate
+ * other than what the app requested, the app will need to do pull down or use some
+ * other technique to adapt to the system's frame rate. The user experience is likely
+ * to be worse (e.g. more frame stuttering) than it would be if the system had chosen
+ * the app's requested frame rate. This value should be used for video content.
+ */
+ public static final int FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1;
+
+ /**
+ * This surface belongs to an app on the High Refresh Rate Deny list, and needs the display
+ * to operate at the exact frame rate.
+ *
+ * This is used internally by the platform and should not be used by apps.
+ * @hide
+ */
+ public static final int FRAME_RATE_COMPATIBILITY_EXACT = 100;
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"CHANGE_FRAME_RATE_"},
+ value = {CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS, CHANGE_FRAME_RATE_ALWAYS})
+ public @interface ChangeFrameRateStrategy {}
+
+ /**
+ * Change the frame rate only if the transition is going to be seamless.
+ */
+ public static final int CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS = 0;
+
+ /**
+ * Change the frame rate even if the transition is going to be non-seamless, i.e. with visual
+ * interruptions for the user. Non-seamless switches might be used when the benefit of matching
+ * the content's frame rate outweighs the cost of the transition, for example when
+ * displaying long-running video content.
+ */
+ public static final int CHANGE_FRAME_RATE_ALWAYS = 1;
+
+ /**
+ * Create an empty surface, which will later be filled in by readFromParcel().
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Surface() {
+ }
+
+ /**
+ * Create a Surface associated with a given {@link SurfaceControl}. Buffers submitted to this
+ * surface will be displayed by the system compositor according to the parameters
+ * specified by the control. Multiple surfaces may be constructed from one SurfaceControl,
+ * but only one can be connected (e.g. have an active EGL context) at a time.
+ *
+ * @param from The SurfaceControl to associate this Surface with
+ */
+ public Surface(@NonNull SurfaceControl from) {
+ copyFrom(from);
+ }
+
+ /**
+ * Create Surface from a {@link SurfaceTexture}.
+ *
+ * Images drawn to the Surface will be made available to the {@link
+ * SurfaceTexture}, which can attach them to an OpenGL ES texture via {@link
+ * SurfaceTexture#updateTexImage}.
+ *
+ * Please note that holding onto the Surface created here is not enough to
+ * keep the provided SurfaceTexture from being reclaimed. In that sense,
+ * the Surface will act like a
+ * {@link java.lang.ref.WeakReference weak reference} to the SurfaceTexture.
+ *
+ * @param surfaceTexture The {@link SurfaceTexture} that is updated by this
+ * Surface.
+ * @throws OutOfResourcesException if the surface could not be created.
+ */
+ public Surface(SurfaceTexture surfaceTexture) {
+ if (surfaceTexture == null) {
+ throw new IllegalArgumentException("surfaceTexture must not be null");
+ }
+ mIsSingleBuffered = surfaceTexture.isSingleBuffered();
+ synchronized (mLock) {
+ mName = surfaceTexture.toString();
+ setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture));
+ }
+ }
+
+ /* called from android_view_Surface_createFromIGraphicBufferProducer() */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Surface(long nativeObject) {
+ synchronized (mLock) {
+ setNativeObjectLocked(nativeObject);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ release();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Release the local reference to the server-side surface.
+ * Always call release() when you're done with a Surface.
+ * This will make the surface invalid.
+ */
+ public void release() {
+ synchronized (mLock) {
+ if (mHwuiContext != null) {
+ mHwuiContext.destroy();
+ mHwuiContext = null;
+ }
+ if (mNativeObject != 0) {
+ nativeRelease(mNativeObject);
+ setNativeObjectLocked(0);
+ }
+ }
+ }
+
+ /**
+ * Free all server-side state associated with this surface and
+ * release this object's reference. This method can only be
+ * called from the process that created the service.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void destroy() {
+ release();
+ }
+
+ /**
+ * Destroys the HwuiContext without completely
+ * releasing the Surface.
+ * @hide
+ */
+ public void hwuiDestroy() {
+ if (mHwuiContext != null) {
+ mHwuiContext.destroy();
+ mHwuiContext = null;
+ }
+ }
+
+ /**
+ * Returns true if this object holds a valid surface.
+ *
+ * @return True if it holds a physical surface, so lockCanvas() will succeed.
+ * Otherwise returns false.
+ */
+ public boolean isValid() {
+ synchronized (mLock) {
+ if (mNativeObject == 0) return false;
+ return nativeIsValid(mNativeObject);
+ }
+ }
+
+ /**
+ * Gets the generation number of this surface, incremented each time
+ * the native surface contained within this object changes.
+ *
+ * @return The current generation number.
+ * @hide
+ */
+ public int getGenerationId() {
+ synchronized (mLock) {
+ return mGenerationId;
+ }
+ }
+
+ /**
+ * Returns the next frame number which will be dequeued for rendering.
+ * Intended for use with SurfaceFlinger's deferred transactions API.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public long getNextFrameNumber() {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ return nativeGetNextFrameNumber(mNativeObject);
+ }
+ }
+
+ /**
+ * Returns true if the consumer of this Surface is running behind the producer.
+ *
+ * @return True if the consumer is more than one buffer ahead of the producer.
+ * @hide
+ */
+ public boolean isConsumerRunningBehind() {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ return nativeIsConsumerRunningBehind(mNativeObject);
+ }
+ }
+
+ /**
+ * Gets a {@link Canvas} for drawing into this 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.
+ *
+ * @throws IllegalArgumentException If the inOutDirty rectangle is not valid.
+ * @throws OutOfResourcesException If the canvas cannot be locked.
+ */
+ public Canvas lockCanvas(Rect inOutDirty)
+ throws Surface.OutOfResourcesException, IllegalArgumentException {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ if (mLockedObject != 0) {
+ // Ideally, nativeLockCanvas() would throw in this situation and prevent the
+ // double-lock, but that won't happen if mNativeObject was updated. We can't
+ // abandon the old mLockedObject because it might still be in use, so instead
+ // we just refuse to re-lock the Surface.
+ throw new IllegalArgumentException("Surface was already locked");
+ }
+ mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
+ return mCanvas;
+ }
+ }
+
+ /**
+ * 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}.
+ */
+ public void unlockCanvasAndPost(Canvas canvas) {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+
+ if (mHwuiContext != null) {
+ mHwuiContext.unlockAndPost(canvas);
+ } else {
+ unlockSwCanvasAndPost(canvas);
+ }
+ }
+ }
+
+ private void unlockSwCanvasAndPost(Canvas canvas) {
+ if (canvas != mCanvas) {
+ throw new IllegalArgumentException("canvas object must be the same instance that "
+ + "was previously returned by lockCanvas");
+ }
+ if (mNativeObject != mLockedObject) {
+ Log.w(TAG, "WARNING: Surface's mNativeObject (0x" +
+ Long.toHexString(mNativeObject) + ") != mLockedObject (0x" +
+ Long.toHexString(mLockedObject) +")");
+ }
+ if (mLockedObject == 0) {
+ throw new IllegalStateException("Surface was not locked");
+ }
+ try {
+ nativeUnlockCanvasAndPost(mLockedObject, canvas);
+ } finally {
+ nativeRelease(mLockedObject);
+ mLockedObject = 0;
+ }
+ }
+
+ /**
+ * Gets a {@link Canvas} for drawing into this surface.
+ *
+ * After drawing into the provided {@link Canvas}, the caller must
+ * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
+ *
+ * Unlike {@link #lockCanvas(Rect)} this will return a hardware-accelerated
+ * canvas. See the <a href="{@docRoot}guide/topics/graphics/hardware-accel.html#unsupported">
+ * unsupported drawing operations</a> for a list of what is and isn't
+ * supported in a hardware-accelerated canvas. It is also required to
+ * fully cover the surface every time {@link #lockHardwareCanvas()} is
+ * called as the buffer is not preserved between frames. Partial updates
+ * are not supported.
+ *
+ * @return A canvas for drawing into the surface.
+ *
+ * @throws IllegalStateException If the canvas cannot be locked.
+ */
+ public Canvas lockHardwareCanvas() {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ if (mHwuiContext == null) {
+ mHwuiContext = new HwuiContext(false);
+ }
+ return mHwuiContext.lockCanvas(
+ nativeGetWidth(mNativeObject),
+ nativeGetHeight(mNativeObject));
+ }
+ }
+
+ /**
+ * Gets a {@link Canvas} for drawing into this surface that supports wide color gamut.
+ *
+ * After drawing into the provided {@link Canvas}, the caller must
+ * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
+ *
+ * Unlike {@link #lockCanvas(Rect)} and {@link #lockHardwareCanvas()},
+ * this will return a hardware-accelerated canvas that supports wide color gamut.
+ * See the <a href="{@docRoot}guide/topics/graphics/hardware-accel.html#unsupported">
+ * unsupported drawing operations</a> for a list of what is and isn't
+ * supported in a hardware-accelerated canvas. It is also required to
+ * fully cover the surface every time {@link #lockHardwareCanvas()} is
+ * called as the buffer is not preserved between frames. Partial updates
+ * are not supported.
+ *
+ * @return A canvas for drawing into the surface.
+ *
+ * @throws IllegalStateException If the canvas cannot be locked.
+ *
+ * @hide
+ */
+ public Canvas lockHardwareWideColorGamutCanvas() {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ if (mHwuiContext != null && !mHwuiContext.isWideColorGamut()) {
+ mHwuiContext.destroy();
+ mHwuiContext = null;
+ }
+ if (mHwuiContext == null) {
+ mHwuiContext = new HwuiContext(true);
+ }
+ return mHwuiContext.lockCanvas(
+ nativeGetWidth(mNativeObject),
+ nativeGetHeight(mNativeObject));
+ }
+ }
+
+ /**
+ * @deprecated This API has been removed and is not supported. Do not use.
+ */
+ @Deprecated
+ public void unlockCanvas(Canvas canvas) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the translator used to scale canvas's width/height in compatibility
+ * mode.
+ */
+ void setCompatibilityTranslator(Translator translator) {
+ if (translator != null) {
+ float appScale = translator.applicationScale;
+ mCompatibleMatrix = new Matrix();
+ mCompatibleMatrix.setScale(appScale, appScale);
+ }
+ }
+
+ private void updateNativeObject(long newNativeObject) {
+ synchronized (mLock) {
+ if (newNativeObject == mNativeObject) {
+ return;
+ }
+ if (mNativeObject != 0) {
+ nativeRelease(mNativeObject);
+ }
+ setNativeObjectLocked(newNativeObject);
+ }
+ }
+
+ /**
+ * Copy another surface to this one. This surface now holds a reference
+ * to the same data as the original surface, and is -not- the owner.
+ * This is for use by the window manager when returning a window surface
+ * back from a client, converting it from the representation being managed
+ * by the window manager to the representation the client uses to draw
+ * in to it.
+ *
+ * @param other {@link SurfaceControl} to copy from.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void copyFrom(SurfaceControl other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null");
+ }
+
+ long surfaceControlPtr = other.mNativeObject;
+ if (surfaceControlPtr == 0) {
+ throw new NullPointerException(
+ "null SurfaceControl native object. Are you using a released SurfaceControl?");
+ }
+ long newNativeObject = nativeGetFromSurfaceControl(mNativeObject, surfaceControlPtr);
+ updateNativeObject(newNativeObject);
+ }
+
+ /**
+ * Update the surface if the BLASTBufferQueue IGraphicBufferProducer is different from this
+ * surface's IGraphicBufferProducer.
+ *
+ * @param queue {@link BLASTBufferQueue} to copy from.
+ * @hide
+ */
+ public void copyFrom(BLASTBufferQueue queue) {
+ if (queue == null) {
+ throw new IllegalArgumentException("queue must not be null");
+ }
+
+ long blastBufferQueuePtr = queue.mNativeObject;
+ if (blastBufferQueuePtr == 0) {
+ throw new NullPointerException("Null BLASTBufferQueue native object");
+ }
+ long newNativeObject = nativeGetFromBlastBufferQueue(mNativeObject, blastBufferQueuePtr);
+ updateNativeObject(newNativeObject);
+ }
+
+ /**
+ * Gets a reference a surface created from this one. This surface now holds a reference
+ * to the same data as the original surface, and is -not- the owner.
+ * This is for use by the window manager when returning a window surface
+ * back from a client, converting it from the representation being managed
+ * by the window manager to the representation the client uses to draw
+ * in to it.
+ *
+ * @param other {@link SurfaceControl} to create surface from.
+ *
+ * @hide
+ */
+ public void createFrom(SurfaceControl other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null");
+ }
+
+ long surfaceControlPtr = other.mNativeObject;
+ if (surfaceControlPtr == 0) {
+ throw new NullPointerException(
+ "null SurfaceControl native object. Are you using a released SurfaceControl?");
+ }
+ long newNativeObject = nativeCreateFromSurfaceControl(surfaceControlPtr);
+
+ synchronized (mLock) {
+ if (mNativeObject != 0) {
+ nativeRelease(mNativeObject);
+ }
+ setNativeObjectLocked(newNativeObject);
+ }
+ }
+
+ /**
+ * This is intended to be used by {@link SurfaceView#updateWindow} only.
+ * @param other access is not thread safe
+ * @hide
+ * @deprecated
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public void transferFrom(Surface other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null");
+ }
+ if (other != this) {
+ final long newPtr;
+ synchronized (other.mLock) {
+ newPtr = other.mNativeObject;
+ other.setNativeObjectLocked(0);
+ }
+
+ synchronized (mLock) {
+ if (mNativeObject != 0) {
+ nativeRelease(mNativeObject);
+ }
+ setNativeObjectLocked(newPtr);
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public void readFromParcel(Parcel source) {
+ if (source == null) {
+ throw new IllegalArgumentException("source must not be null");
+ }
+
+ synchronized (mLock) {
+ // nativeReadFromParcel() will either return mNativeObject, or
+ // create a new native Surface and return it after reducing
+ // the reference count on mNativeObject. Either way, it is
+ // not necessary to call nativeRelease() here.
+ // NOTE: This must be kept synchronized with the native parceling code
+ // in frameworks/native/libs/Surface.cpp
+ mName = source.readString();
+ mIsSingleBuffered = source.readInt() != 0;
+ setNativeObjectLocked(nativeReadFromParcel(mNativeObject, source));
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (dest == null) {
+ throw new IllegalArgumentException("dest must not be null");
+ }
+ synchronized (mLock) {
+ // NOTE: This must be kept synchronized with the native parceling code
+ // in frameworks/native/libs/Surface.cpp
+ dest.writeString(mName);
+ dest.writeInt(mIsSingleBuffered ? 1 : 0);
+ nativeWriteToParcel(mNativeObject, dest);
+ }
+ if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+ release();
+ }
+ }
+
+ @Override
+ public String toString() {
+ synchronized (mLock) {
+ return "Surface(name=" + mName + ")/@0x" +
+ Integer.toHexString(System.identityHashCode(this));
+ }
+ }
+
+ private void setNativeObjectLocked(long ptr) {
+ if (mNativeObject != ptr) {
+ if (mNativeObject == 0 && ptr != 0) {
+ mCloseGuard.open("release");
+ } else if (mNativeObject != 0 && ptr == 0) {
+ mCloseGuard.close();
+ }
+ mNativeObject = ptr;
+ mGenerationId += 1;
+ if (mHwuiContext != null) {
+ mHwuiContext.updateSurface();
+ }
+ }
+ }
+
+ private void checkNotReleasedLocked() {
+ if (mNativeObject == 0) {
+ throw new IllegalStateException("Surface has already been released.");
+ }
+ }
+
+ /**
+ * Allocate buffers ahead of time to avoid allocation delays during rendering
+ * @hide
+ */
+ public void allocateBuffers() {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ nativeAllocateBuffers(mNativeObject);
+ }
+ }
+
+ /**
+ * Set the scaling mode to be used for this surfaces buffers
+ * @hide
+ */
+ public void setScalingMode(@ScalingMode int scalingMode) {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ int err = nativeSetScalingMode(mNativeObject, scalingMode);
+ if (err != 0) {
+ throw new IllegalArgumentException("Invalid scaling mode: " + scalingMode);
+ }
+ }
+ }
+
+ void forceScopedDisconnect() {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ int err = nativeForceScopedDisconnect(mNativeObject);
+ if (err != 0) {
+ throw new RuntimeException("Failed to disconnect Surface instance (bad object?)");
+ }
+ }
+ }
+
+ /**
+ * Transfer ownership of buffer with a color space and present it on the Surface.
+ * The supported color spaces are SRGB and Display P3, other color spaces will be
+ * treated as SRGB.
+ * @hide
+ */
+ public void attachAndQueueBufferWithColorSpace(HardwareBuffer buffer, ColorSpace colorSpace) {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ if (colorSpace == null) {
+ colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ }
+ int err = nativeAttachAndQueueBufferWithColorSpace(mNativeObject, buffer,
+ colorSpace.getId());
+ if (err != 0) {
+ throw new RuntimeException(
+ "Failed to attach and queue buffer to Surface (bad object?), "
+ + "native error: " + err);
+ }
+ }
+ }
+
+ /**
+ * Returns whether or not this Surface is backed by a single-buffered SurfaceTexture
+ * @hide
+ */
+ public boolean isSingleBuffered() {
+ return mIsSingleBuffered;
+ }
+
+ /**
+ * <p>The shared buffer mode allows both the application and the surface compositor
+ * (SurfaceFlinger) to concurrently access this surface's buffer. While the
+ * application is still required to issue a present request
+ * (see {@link #unlockCanvasAndPost(Canvas)}) to the compositor when an update is required,
+ * the compositor may trigger an update at any time. Since the surface's buffer is shared
+ * between the application and the compositor, updates triggered by the compositor may
+ * cause visible tearing.</p>
+ *
+ * <p>The shared buffer mode can be used with
+ * {@link #setAutoRefreshEnabled(boolean) auto-refresh} to avoid the overhead of
+ * issuing present requests.</p>
+ *
+ * <p>If the application uses the shared buffer mode to reduce latency, it is
+ * recommended to use software rendering (see {@link #lockCanvas(Rect)} to ensure
+ * the graphics workloads are not affected by other applications and/or the system
+ * using the GPU. When using software rendering, the application should update the
+ * smallest possible region of the surface required.</p>
+ *
+ * <p class="note">The shared buffer mode might not be supported by the underlying
+ * hardware. Enabling shared buffer mode on hardware that does not support it will
+ * not yield an error but the application will not benefit from lower latency (and
+ * tearing will not be visible).</p>
+ *
+ * <p class="note">Depending on how many and what kind of surfaces are visible, the
+ * surface compositor may need to copy the shared buffer before it is displayed. When
+ * this happens, the latency benefits of shared buffer mode will be reduced.</p>
+ *
+ * @param enabled True to enable the shared buffer mode on this surface, false otherwise
+ *
+ * @see #isSharedBufferModeEnabled()
+ * @see #setAutoRefreshEnabled(boolean)
+ *
+ * @hide
+ */
+ public void setSharedBufferModeEnabled(boolean enabled) {
+ if (mIsSharedBufferModeEnabled != enabled) {
+ int error = nativeSetSharedBufferModeEnabled(mNativeObject, enabled);
+ if (error != 0) {
+ throw new RuntimeException(
+ "Failed to set shared buffer mode on Surface (bad object?)");
+ } else {
+ mIsSharedBufferModeEnabled = enabled;
+ }
+ }
+ }
+
+ /**
+ * @return True if shared buffer mode is enabled on this surface, false otherwise
+ *
+ * @see #setSharedBufferModeEnabled(boolean)
+ *
+ * @hide
+ */
+ public boolean isSharedBufferModeEnabled() {
+ return mIsSharedBufferModeEnabled;
+ }
+
+ /**
+ * <p>When auto-refresh is enabled, the surface compositor (SurfaceFlinger)
+ * automatically updates the display on a regular refresh cycle. The application
+ * can continue to issue present requests but it is not required. Enabling
+ * auto-refresh may result in visible tearing.</p>
+ *
+ * <p>Auto-refresh has no effect if the {@link #setSharedBufferModeEnabled(boolean)
+ * shared buffer mode} is not enabled.</p>
+ *
+ * <p>Because auto-refresh will trigger continuous updates of the display, it is
+ * recommended to turn it on only when necessary. For example, in a drawing/painting
+ * application auto-refresh should be enabled on finger/pen down and disabled on
+ * finger/pen up.</p>
+ *
+ * @param enabled True to enable auto-refresh on this surface, false otherwise
+ *
+ * @see #isAutoRefreshEnabled()
+ * @see #setSharedBufferModeEnabled(boolean)
+ *
+ * @hide
+ */
+ public void setAutoRefreshEnabled(boolean enabled) {
+ if (mIsAutoRefreshEnabled != enabled) {
+ int error = nativeSetAutoRefreshEnabled(mNativeObject, enabled);
+ if (error != 0) {
+ throw new RuntimeException("Failed to set auto refresh on Surface (bad object?)");
+ } else {
+ mIsAutoRefreshEnabled = enabled;
+ }
+ }
+ }
+
+ /**
+ * @return True if auto-refresh is enabled on this surface, false otherwise
+ *
+ * @hide
+ */
+ public boolean isAutoRefreshEnabled() {
+ return mIsAutoRefreshEnabled;
+ }
+
+ /**
+ * Sets the intended frame rate for this surface.
+ *
+ * <p>On devices that are capable of running the display at different refresh rates,
+ * the system may choose a display refresh rate to better match this surface's frame
+ * rate. Usage of this API won't introduce frame rate throttling, or affect other
+ * aspects of the application's frame production pipeline. However, because the system
+ * may change the display refresh rate, calls to this function may result in changes
+ * to Choreographer callback timings, and changes to the time interval at which the
+ * system releases buffers back to the application.</p>
+ *
+ * <p>Note that this only has an effect for surfaces presented on the display. If this
+ * surface is consumed by something other than the system compositor, e.g. a media
+ * codec, this call has no effect.</p>
+ *
+ * @param frameRate The intended frame rate of this surface, in frames per second. 0
+ * is a special value that indicates the app will accept the system's choice for the
+ * display frame rate, which is the default behavior if this function isn't
+ * called. The <code>frameRate</code> parameter does <em>not</em> need to be a valid refresh
+ * rate for this device's display - e.g., it's fine to pass 30fps to a device that can only run
+ * the display at 60fps.
+ *
+ * @param compatibility The frame rate compatibility of this surface. The
+ * compatibility value may influence the system's choice of display frame rate.
+ * This parameter is ignored when <code>frameRate</code> is 0.
+ *
+ * @param changeFrameRateStrategy Whether display refresh rate transitions caused by this
+ * surface should be seamless. A seamless transition is one that doesn't have any visual
+ * interruptions, such as a black screen for a second or two. This parameter is ignored when
+ * <code>frameRate</code> is 0.
+ *
+ * @throws IllegalArgumentException If <code>frameRate</code>, <code>compatibility</code> or
+ * <code>changeFrameRateStrategy</code> are invalid.
+ */
+ public void setFrameRate(@FloatRange(from = 0.0) float frameRate,
+ @FrameRateCompatibility int compatibility,
+ @ChangeFrameRateStrategy int changeFrameRateStrategy) {
+ synchronized (mLock) {
+ checkNotReleasedLocked();
+ int error = nativeSetFrameRate(mNativeObject, frameRate, compatibility,
+ changeFrameRateStrategy);
+ if (error == -EINVAL) {
+ throw new IllegalArgumentException("Invalid argument to Surface.setFrameRate()");
+ } else if (error != 0) {
+ throw new RuntimeException("Failed to set frame rate on Surface");
+ }
+ }
+ }
+
+ /**
+ * Sets the intended frame rate for this surface. Any switching of refresh rates is
+ * most probably going to be seamless.
+ *
+ * @see #setFrameRate(float, int, int)
+ */
+ public void setFrameRate(
+ @FloatRange(from = 0.0) float frameRate, @FrameRateCompatibility int compatibility) {
+ setFrameRate(frameRate, compatibility, CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+ }
+
+ /**
+ * Exception thrown when a Canvas couldn't be locked with {@link Surface#lockCanvas}, or
+ * when a SurfaceTexture could not successfully be allocated.
+ */
+ @SuppressWarnings("serial")
+ public static class OutOfResourcesException extends RuntimeException {
+ public OutOfResourcesException() {
+ }
+ public OutOfResourcesException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * Returns a human readable representation of a rotation.
+ *
+ * @param rotation The rotation.
+ * @return The rotation symbolic name.
+ *
+ * @hide
+ */
+ public static String rotationToString(int rotation) {
+ switch (rotation) {
+ case Surface.ROTATION_0: {
+ return "ROTATION_0";
+ }
+ case Surface.ROTATION_90: {
+ return "ROTATION_90";
+ }
+ case Surface.ROTATION_180: {
+ return "ROTATION_180";
+ }
+ case Surface.ROTATION_270: {
+ return "ROTATION_270";
+ }
+ default: {
+ return Integer.toString(rotation);
+ }
+ }
+ }
+
+ /**
+ * A Canvas class that can handle the compatibility mode.
+ * This does two things differently.
+ * <ul>
+ * <li>Returns the width and height of the target metrics, rather than
+ * native. For example, the canvas returns 320x480 even if an app is running
+ * in WVGA high density.
+ * <li>Scales the matrix in setMatrix by the application scale, except if
+ * the matrix looks like obtained from getMatrix. This is a hack to handle
+ * the case that an application uses getMatrix to keep the original matrix,
+ * set matrix of its own, then set the original matrix back. There is no
+ * perfect solution that works for all cases, and there are a lot of cases
+ * that this model does not work, but we hope this works for many apps.
+ * </ul>
+ */
+ private final class CompatibleCanvas extends Canvas {
+ // A temp matrix to remember what an application obtained via {@link getMatrix}
+ private Matrix mOrigMatrix = null;
+
+ @Override
+ public void setMatrix(Matrix matrix) {
+ if (mCompatibleMatrix == null || mOrigMatrix == null || mOrigMatrix.equals(matrix)) {
+ // don't scale the matrix if it's not compatibility mode, or
+ // the matrix was obtained from getMatrix.
+ super.setMatrix(matrix);
+ } else {
+ Matrix m = new Matrix(mCompatibleMatrix);
+ m.preConcat(matrix);
+ super.setMatrix(m);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void getMatrix(Matrix m) {
+ super.getMatrix(m);
+ if (mOrigMatrix == null) {
+ mOrigMatrix = new Matrix();
+ }
+ mOrigMatrix.set(m);
+ }
+ }
+
+ private final class HwuiContext {
+ private final RenderNode mRenderNode;
+ private HardwareRenderer mHardwareRenderer;
+ private RecordingCanvas mCanvas;
+ private final boolean mIsWideColorGamut;
+
+ HwuiContext(boolean isWideColorGamut) {
+ mRenderNode = RenderNode.create("HwuiCanvas", null);
+ mRenderNode.setClipToBounds(false);
+ mRenderNode.setForceDarkAllowed(false);
+ mIsWideColorGamut = isWideColorGamut;
+
+ mHardwareRenderer = new HardwareRenderer();
+ mHardwareRenderer.setContentRoot(mRenderNode);
+ mHardwareRenderer.setSurface(Surface.this, true);
+ mHardwareRenderer.setColorMode(
+ isWideColorGamut
+ ? ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
+ : ActivityInfo.COLOR_MODE_DEFAULT);
+ mHardwareRenderer.setLightSourceAlpha(0.0f, 0.0f);
+ mHardwareRenderer.setLightSourceGeometry(0.0f, 0.0f, 0.0f, 0.0f);
+ }
+
+ Canvas lockCanvas(int width, int height) {
+ if (mCanvas != null) {
+ throw new IllegalStateException("Surface was already locked!");
+ }
+ mCanvas = mRenderNode.beginRecording(width, height);
+ return mCanvas;
+ }
+
+ void unlockAndPost(Canvas canvas) {
+ if (canvas != mCanvas) {
+ throw new IllegalArgumentException("canvas object must be the same instance that "
+ + "was previously returned by lockCanvas");
+ }
+ mRenderNode.endRecording();
+ mCanvas = null;
+ mHardwareRenderer.createRenderRequest()
+ .setVsyncTime(System.nanoTime())
+ .syncAndDraw();
+ }
+
+ void updateSurface() {
+ mHardwareRenderer.setSurface(Surface.this, true);
+ }
+
+ void destroy() {
+ mHardwareRenderer.destroy();
+ }
+
+ boolean isWideColorGamut() {
+ return mIsWideColorGamut;
+ }
+ }
+}
diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java
new file mode 100644
index 0000000..8143cf9
--- /dev/null
+++ b/android/view/SurfaceControl.java
@@ -0,0 +1,3633 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.graphics.Matrix.MSCALE_X;
+import static android.graphics.Matrix.MSCALE_Y;
+import static android.graphics.Matrix.MSKEW_X;
+import static android.graphics.Matrix.MSKEW_Y;
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
+import static android.view.SurfaceControlProto.HASH_CODE;
+import static android.view.SurfaceControlProto.NAME;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.GraphicBuffer;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.hardware.HardwareBuffer;
+import android.hardware.display.DeviceProductInfo;
+import android.hardware.display.DisplayedContentSample;
+import android.hardware.display.DisplayedContentSamplingAttributes;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.Surface.OutOfResourcesException;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.VirtualRefBasePtr;
+
+import dalvik.system.CloseGuard;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handle to an on-screen Surface managed by the system compositor. The SurfaceControl is
+ * a combination of a buffer source, and metadata about how to display the buffers.
+ * By constructing a {@link Surface} from this SurfaceControl you can submit buffers to be
+ * composited. Using {@link SurfaceControl.Transaction} you can manipulate various
+ * properties of how the buffer will be displayed on-screen. SurfaceControl's are
+ * arranged into a scene-graph like hierarchy, and as such any SurfaceControl may have
+ * a parent. Geometric properties like transform, crop, and Z-ordering will be inherited
+ * from the parent, as if the child were content in the parents buffer stream.
+ */
+public final class SurfaceControl implements Parcelable {
+ private static final String TAG = "SurfaceControl";
+
+ private static native long nativeCreate(SurfaceSession session, String name,
+ int w, int h, int format, int flags, long parentObject, Parcel metadata)
+ throws OutOfResourcesException;
+ private static native long nativeReadFromParcel(Parcel in);
+ private static native long nativeCopyFromSurfaceControl(long nativeObject);
+ private static native void nativeWriteToParcel(long nativeObject, Parcel out);
+ private static native void nativeRelease(long nativeObject);
+ private static native void nativeDisconnect(long nativeObject);
+ private static native void nativeUpdateDefaultBufferSize(long nativeObject, int width, int height);
+ private static native int nativeCaptureDisplay(DisplayCaptureArgs captureArgs,
+ ScreenCaptureListener captureListener);
+ private static native int nativeCaptureLayers(LayerCaptureArgs captureArgs,
+ ScreenCaptureListener captureListener);
+ private static native long nativeMirrorSurface(long mirrorOfObject);
+ private static native long nativeCreateTransaction();
+ private static native long nativeGetNativeTransactionFinalizer();
+ private static native void nativeApplyTransaction(long transactionObj, boolean sync);
+ private static native void nativeMergeTransaction(long transactionObj,
+ long otherTransactionObj);
+ private static native void nativeClearTransaction(long transactionObj);
+ private static native void nativeSetAnimationTransaction(long transactionObj);
+ private static native void nativeSetEarlyWakeupStart(long transactionObj);
+ private static native void nativeSetEarlyWakeupEnd(long transactionObj);
+
+ private static native void nativeSetLayer(long transactionObj, long nativeObject, int zorder);
+ private static native void nativeSetRelativeLayer(long transactionObj, long nativeObject,
+ long relativeToObject, int zorder);
+ private static native void nativeSetPosition(long transactionObj, long nativeObject,
+ float x, float y);
+ private static native void nativeSetSize(long transactionObj, long nativeObject, int w, int h);
+ private static native void nativeSetTransparentRegionHint(long transactionObj,
+ long nativeObject, Region region);
+ private static native void nativeSetAlpha(long transactionObj, long nativeObject, float alpha);
+ private static native void nativeSetMatrix(long transactionObj, long nativeObject,
+ float dsdx, float dtdx,
+ float dtdy, float dsdy);
+ private static native void nativeSetColorTransform(long transactionObj, long nativeObject,
+ float[] matrix, float[] translation);
+ private static native void nativeSetColorSpaceAgnostic(long transactionObj, long nativeObject,
+ boolean agnostic);
+ private static native void nativeSetGeometry(long transactionObj, long nativeObject,
+ Rect sourceCrop, Rect dest, long orientation);
+ private static native void nativeSetColor(long transactionObj, long nativeObject, float[] color);
+ private static native void nativeSetFlags(long transactionObj, long nativeObject,
+ int flags, int mask);
+ private static native void nativeSetFrameRateSelectionPriority(long transactionObj,
+ long nativeObject, int priority);
+ private static native void nativeSetWindowCrop(long transactionObj, long nativeObject,
+ int l, int t, int r, int b);
+ private static native void nativeSetCornerRadius(long transactionObj, long nativeObject,
+ float cornerRadius);
+ private static native void nativeSetBackgroundBlurRadius(long transactionObj, long nativeObject,
+ int blurRadius);
+ private static native void nativeSetLayerStack(long transactionObj, long nativeObject,
+ int layerStack);
+ private static native void nativeSetBlurRegions(long transactionObj, long nativeObj,
+ float[][] regions, int length);
+ private static native void nativeSetStretchEffect(long transactionObj, long nativeObj,
+ float width, float height, float vecX, float vecY,
+ float maxStretchAmountX, float maxStretchAmountY, float childRelativeLeft,
+ float childRelativeTop, float childRelativeRight, float childRelativeBottom);
+ private static native void nativeSetTrustedOverlay(long transactionObj, long nativeObject,
+ boolean isTrustedOverlay);
+
+ private static native boolean nativeClearContentFrameStats(long nativeObject);
+ private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats);
+ private static native boolean nativeClearAnimationFrameStats();
+ private static native boolean nativeGetAnimationFrameStats(WindowAnimationFrameStats outStats);
+
+ private static native long[] nativeGetPhysicalDisplayIds();
+ private static native IBinder nativeGetPhysicalDisplayToken(long physicalDisplayId);
+ private static native IBinder nativeCreateDisplay(String name, boolean secure);
+ private static native void nativeDestroyDisplay(IBinder displayToken);
+ private static native void nativeSetDisplaySurface(long transactionObj,
+ IBinder displayToken, long nativeSurfaceObject);
+ private static native void nativeSetDisplayLayerStack(long transactionObj,
+ IBinder displayToken, int layerStack);
+ private static native void nativeSetDisplayProjection(long transactionObj,
+ IBinder displayToken, int orientation,
+ int l, int t, int r, int b,
+ int L, int T, int R, int B);
+ private static native void nativeSetDisplaySize(long transactionObj, IBinder displayToken,
+ int width, int height);
+ private static native StaticDisplayInfo nativeGetStaticDisplayInfo(IBinder displayToken);
+ private static native DynamicDisplayInfo nativeGetDynamicDisplayInfo(IBinder displayToken);
+ private static native DisplayedContentSamplingAttributes
+ nativeGetDisplayedContentSamplingAttributes(IBinder displayToken);
+ private static native boolean nativeSetDisplayedContentSamplingEnabled(IBinder displayToken,
+ boolean enable, int componentMask, int maxFrames);
+ private static native DisplayedContentSample nativeGetDisplayedContentSample(
+ IBinder displayToken, long numFrames, long timestamp);
+ private static native boolean nativeSetDesiredDisplayModeSpecs(IBinder displayToken,
+ DesiredDisplayModeSpecs desiredDisplayModeSpecs);
+ private static native DesiredDisplayModeSpecs
+ nativeGetDesiredDisplayModeSpecs(IBinder displayToken);
+ private static native DisplayPrimaries nativeGetDisplayNativePrimaries(
+ IBinder displayToken);
+ private static native int[] nativeGetCompositionDataspaces();
+ private static native boolean nativeSetActiveColorMode(IBinder displayToken,
+ int colorMode);
+ private static native void nativeSetAutoLowLatencyMode(IBinder displayToken, boolean on);
+ private static native void nativeSetGameContentType(IBinder displayToken, boolean on);
+ private static native void nativeSetDisplayPowerMode(
+ IBinder displayToken, int mode);
+ private static native void nativeReparent(long transactionObj, long nativeObject,
+ long newParentNativeObject);
+ private static native void nativeSetBuffer(long transactionObj, long nativeObject,
+ GraphicBuffer buffer);
+ private static native void nativeSetColorSpace(long transactionObj, long nativeObject,
+ int colorSpace);
+
+ private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
+
+ private static native void nativeSetInputWindowInfo(long transactionObj, long nativeObject,
+ InputWindowHandle handle);
+
+ private static native boolean nativeGetProtectedContentSupport();
+ private static native void nativeSetMetadata(long transactionObj, long nativeObject, int key,
+ Parcel data);
+ private static native void nativeSyncInputWindows(long transactionObj);
+ private static native boolean nativeGetDisplayBrightnessSupport(IBinder displayToken);
+ private static native boolean nativeSetDisplayBrightness(IBinder displayToken,
+ float sdrBrightness, float sdrBrightnessNits, float displayBrightness,
+ float displayBrightnessNits);
+ private static native long nativeReadTransactionFromParcel(Parcel in);
+ private static native void nativeWriteTransactionToParcel(long nativeObject, Parcel out);
+ private static native void nativeSetShadowRadius(long transactionObj, long nativeObject,
+ float shadowRadius);
+ private static native void nativeSetGlobalShadowSettings(@Size(4) float[] ambientColor,
+ @Size(4) float[] spotColor, float lightPosY, float lightPosZ, float lightRadius);
+
+ private static native void nativeSetFrameRate(long transactionObj, long nativeObject,
+ float frameRate, int compatibility, int changeFrameRateStrategy);
+ private static native long nativeGetHandle(long nativeObject);
+
+ private static native long nativeAcquireFrameRateFlexibilityToken();
+ private static native void nativeReleaseFrameRateFlexibilityToken(long token);
+ private static native void nativeSetFixedTransformHint(long transactionObj, long nativeObject,
+ int transformHint);
+ private static native void nativeSetFocusedWindow(long transactionObj, IBinder toToken,
+ String windowName, IBinder focusedToken, String focusedWindowName, int displayId);
+ private static native void nativeSetFrameTimelineVsync(long transactionObj,
+ long frameTimelineVsyncId);
+ private static native void nativeAddJankDataListener(long nativeListener,
+ long nativeSurfaceControl);
+ private static native void nativeRemoveJankDataListener(long nativeListener);
+ private static native long nativeCreateJankDataListenerWrapper(OnJankDataListener listener);
+ private static native int nativeGetGPUContextPriority();
+ private static native void nativeSetTransformHint(long nativeObject, int transformHint);
+ private static native int nativeGetTransformHint(long nativeObject);
+
+ @Nullable
+ @GuardedBy("mLock")
+ private ArrayList<OnReparentListener> mReparentListeners;
+
+ /**
+ * Listener to observe surface reparenting.
+ *
+ * @hide
+ */
+ public interface OnReparentListener {
+
+ /**
+ * Callback for reparenting surfaces.
+ *
+ * Important: You should only interact with the provided surface control
+ * only if you have a contract with its owner to avoid them closing it
+ * under you or vise versa.
+ *
+ * @param transaction The transaction that would commit reparenting.
+ * @param parent The future parent surface.
+ */
+ void onReparent(@NonNull Transaction transaction, @Nullable SurfaceControl parent);
+ }
+
+ /**
+ * Jank information to be fed back via {@link OnJankDataListener}.
+ * @hide
+ */
+ public static class JankData {
+
+ /** @hide */
+ @IntDef(flag = true, value = {JANK_NONE,
+ DISPLAY_HAL,
+ JANK_SURFACEFLINGER_DEADLINE_MISSED,
+ JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED,
+ JANK_APP_DEADLINE_MISSED,
+ PREDICTION_ERROR,
+ SURFACE_FLINGER_SCHEDULING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface JankType {}
+
+ // Needs to be kept in sync with frameworks/native/libs/gui/include/gui/JankInfo.h
+
+ // No Jank
+ public static final int JANK_NONE = 0x0;
+
+ // Jank not related to SurfaceFlinger or the App
+ public static final int DISPLAY_HAL = 0x1;
+ // SF took too long on the CPU
+ public static final int JANK_SURFACEFLINGER_DEADLINE_MISSED = 0x2;
+ // SF took too long on the GPU
+ public static final int JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED = 0x4;
+ // Either App or GPU took too long on the frame
+ public static final int JANK_APP_DEADLINE_MISSED = 0x8;
+ // Predictions live for 120ms, if prediction is expired for a frame, there is definitely a
+ // jank
+ // associated with the App if this is for a SurfaceFrame, and SF for a DisplayFrame.
+ public static final int PREDICTION_ERROR = 0x10;
+ // Latching a buffer early might cause an early present of the frame
+ public static final int SURFACE_FLINGER_SCHEDULING = 0x20;
+ // A buffer is said to be stuffed if it was expected to be presented on a vsync but was
+ // presented later because the previous buffer was presented in its expected vsync. This
+ // usually happens if there is an unexpectedly long frame causing the rest of the buffers
+ // to enter a stuffed state.
+ public static final int BUFFER_STUFFING = 0x40;
+ // Jank due to unknown reasons.
+ public static final int UNKNOWN = 0x80;
+
+ public JankData(long frameVsyncId, @JankType int jankType) {
+ this.frameVsyncId = frameVsyncId;
+ this.jankType = jankType;
+ }
+
+ public final long frameVsyncId;
+ public final @JankType int jankType;
+ }
+
+ /**
+ * Listener interface to be informed about SurfaceFlinger's jank classification for a specific
+ * surface.
+ *
+ * @see JankData
+ * @see #addJankDataListener
+ * @hide
+ */
+ public static abstract class OnJankDataListener {
+ private final VirtualRefBasePtr mNativePtr;
+
+ public OnJankDataListener() {
+ mNativePtr = new VirtualRefBasePtr(nativeCreateJankDataListenerWrapper(this));
+ }
+
+ /**
+ * Called when new jank classifications are available.
+ */
+ public abstract void onJankDataAvailable(JankData[] jankStats);
+ }
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+ private String mName;
+
+ /**
+ * @hide
+ */
+ public long mNativeObject;
+ private long mNativeHandle;
+
+ // TODO: Move width/height to native and fix locking through out.
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private int mWidth;
+ @GuardedBy("mLock")
+ private int mHeight;
+
+ private int mTransformHint;
+
+ private WeakReference<View> mLocalOwnerView;
+
+ static GlobalTransactionWrapper sGlobalTransaction;
+ static long sTransactionNestCount = 0;
+
+ /**
+ * Adds a reparenting listener.
+ *
+ * @param listener The listener.
+ * @return Whether listener was added.
+ *
+ * @hide
+ */
+ public boolean addOnReparentListener(@NonNull OnReparentListener listener) {
+ synchronized (mLock) {
+ if (mReparentListeners == null) {
+ mReparentListeners = new ArrayList<>(1);
+ }
+ return mReparentListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes a reparenting listener.
+ *
+ * @param listener The listener.
+ * @return Whether listener was removed.
+ *
+ * @hide
+ */
+ public boolean removeOnReparentListener(@NonNull OnReparentListener listener) {
+ synchronized (mLock) {
+ final boolean removed = mReparentListeners.remove(listener);
+ if (mReparentListeners.isEmpty()) {
+ mReparentListeners = null;
+ }
+ return removed;
+ }
+ }
+
+ /* flags used in constructor (keep in sync with ISurfaceComposerClient.h) */
+
+ /**
+ * Surface creation flag: Surface is created hidden
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int HIDDEN = 0x00000004;
+
+ /**
+ * Surface creation flag: Skip this layer and its children when taking a screenshot. This
+ * also includes mirroring and screen recording, so the layers with flag SKIP_SCREENSHOT
+ * will not be included on non primary displays.
+ * @hide
+ */
+ public static final int SKIP_SCREENSHOT = 0x00000040;
+
+ /**
+ * Surface creation flag: Special measures will be taken to disallow the surface's content to
+ * be copied. In particular, screenshots and secondary, non-secure displays will render black
+ * content instead of the surface content.
+ *
+ * @see #createDisplay(String, boolean)
+ * @hide
+ */
+ public static final int SECURE = 0x00000080;
+
+
+ /**
+ * Queue up BufferStateLayer buffers instead of dropping the oldest buffer when this flag is
+ * set. This blocks the client until all the buffers have been presented. If the buffers
+ * have presentation timestamps, then we may drop buffers.
+ * @hide
+ */
+ public static final int ENABLE_BACKPRESSURE = 0x00000100;
+
+ /**
+ * Surface creation flag: Creates a surface where color components are interpreted
+ * as "non pre-multiplied" by their alpha channel. Of course this flag is
+ * meaningless for surfaces without an alpha channel. By default
+ * surfaces are pre-multiplied, which means that each color component is
+ * already multiplied by its alpha value. In this case the blending
+ * equation used is:
+ * <p>
+ * <code>DEST = SRC + DEST * (1-SRC_ALPHA)</code>
+ * <p>
+ * By contrast, non pre-multiplied surfaces use the following equation:
+ * <p>
+ * <code>DEST = SRC * SRC_ALPHA * DEST * (1-SRC_ALPHA)</code>
+ * <p>
+ * pre-multiplied surfaces must always be used if transparent pixels are
+ * composited on top of each-other into the surface. A pre-multiplied
+ * surface can never lower the value of the alpha component of a given
+ * pixel.
+ * <p>
+ * In some rare situations, a non pre-multiplied surface is preferable.
+ * @hide
+ */
+ public static final int NON_PREMULTIPLIED = 0x00000100;
+
+ /**
+ * Surface creation flag: Indicates that the surface must be considered opaque,
+ * even if its pixel format contains an alpha channel. This can be useful if an
+ * application needs full RGBA 8888 support for instance but will
+ * still draw every pixel opaque.
+ * <p>
+ * This flag is ignored if setAlpha() is used to make the surface non-opaque.
+ * Combined effects are (assuming a buffer format with an alpha channel):
+ * <ul>
+ * <li>OPAQUE + alpha(1.0) == opaque composition
+ * <li>OPAQUE + alpha(0.x) == blended composition
+ * <li>!OPAQUE + alpha(1.0) == blended composition
+ * <li>!OPAQUE + alpha(0.x) == blended composition
+ * </ul>
+ * If the underlying buffer lacks an alpha channel, the OPAQUE flag is effectively
+ * set automatically.
+ * @hide
+ */
+ public static final int OPAQUE = 0x00000400;
+
+ /**
+ * Surface creation flag: Application requires a hardware-protected path to an
+ * external display sink. If a hardware-protected path is not available,
+ * then this surface will not be displayed on the external sink.
+ *
+ * @hide
+ */
+ public static final int PROTECTED_APP = 0x00000800;
+
+ // 0x1000 is reserved for an independent DRM protected flag in framework
+
+ /**
+ * Surface creation flag: Window represents a cursor glyph.
+ * @hide
+ */
+ public static final int CURSOR_WINDOW = 0x00002000;
+
+ /**
+ * Surface creation flag: Indicates the effect layer will not have a color fill on
+ * creation.
+ *
+ * @hide
+ */
+ public static final int NO_COLOR_FILL = 0x00004000;
+
+ /**
+ * Surface creation flag: Creates a normal surface.
+ * This is the default.
+ *
+ * @hide
+ */
+ public static final int FX_SURFACE_NORMAL = 0x00000000;
+
+ /**
+ * Surface creation flag: Creates a effect surface which
+ * represents a solid color and or shadows.
+ *
+ * @hide
+ */
+ public static final int FX_SURFACE_EFFECT = 0x00020000;
+
+ /**
+ * Surface creation flag: Creates a container surface.
+ * This surface will have no buffers and will only be used
+ * as a container for other surfaces, or for its InputInfo.
+ * @hide
+ */
+ public static final int FX_SURFACE_CONTAINER = 0x00080000;
+
+ /**
+ * @hide
+ */
+ public static final int FX_SURFACE_BLAST = 0x00040000;
+
+ /**
+ * Mask used for FX values above.
+ *
+ * @hide
+ */
+ public static final int FX_SURFACE_MASK = 0x000F0000;
+
+ /* flags used with setFlags() (keep in sync with ISurfaceComposer.h) */
+
+ /**
+ * Surface flag: Hide the surface.
+ * Equivalent to calling hide().
+ * Updates the value set during Surface creation (see {@link #HIDDEN}).
+ */
+ private static final int SURFACE_HIDDEN = 0x01;
+
+ /**
+ * Surface flag: composite without blending when possible.
+ * Updates the value set during Surface creation (see {@link #OPAQUE}).
+ */
+ private static final int SURFACE_OPAQUE = 0x02;
+
+ // Display power modes.
+ /**
+ * Display power mode off: used while blanking the screen.
+ * Use only with {@link SurfaceControl#setDisplayPowerMode}.
+ * @hide
+ */
+ public static final int POWER_MODE_OFF = 0;
+
+ /**
+ * Display power mode doze: used while putting the screen into low power mode.
+ * Use only with {@link SurfaceControl#setDisplayPowerMode}.
+ * @hide
+ */
+ public static final int POWER_MODE_DOZE = 1;
+
+ /**
+ * Display power mode normal: used while unblanking the screen.
+ * Use only with {@link SurfaceControl#setDisplayPowerMode}.
+ * @hide
+ */
+ public static final int POWER_MODE_NORMAL = 2;
+
+ /**
+ * Display power mode doze: used while putting the screen into a suspended
+ * low power mode. Use only with {@link SurfaceControl#setDisplayPowerMode}.
+ * @hide
+ */
+ public static final int POWER_MODE_DOZE_SUSPEND = 3;
+
+ /**
+ * Display power mode on: used while putting the screen into a suspended
+ * full power mode. Use only with {@link SurfaceControl#setDisplayPowerMode}.
+ * @hide
+ */
+ public static final int POWER_MODE_ON_SUSPEND = 4;
+
+ /**
+ * internal representation of how to interpret pixel value, used only to convert to ColorSpace.
+ */
+ private static final int INTERNAL_DATASPACE_SRGB = 142671872;
+ private static final int INTERNAL_DATASPACE_DISPLAY_P3 = 143261696;
+ private static final int INTERNAL_DATASPACE_SCRGB = 411107328;
+
+ private void assignNativeObject(long nativeObject, String callsite) {
+ if (mNativeObject != 0) {
+ release();
+ }
+ if (nativeObject != 0) {
+ mCloseGuard.openWithCallSite("release", callsite);
+ }
+ mNativeObject = nativeObject;
+ mNativeHandle = mNativeObject != 0 ? nativeGetHandle(nativeObject) : 0;
+ }
+
+ /**
+ * @hide
+ */
+ public void copyFrom(@NonNull SurfaceControl other, String callsite) {
+ mName = other.mName;
+ mWidth = other.mWidth;
+ mHeight = other.mHeight;
+ mLocalOwnerView = other.mLocalOwnerView;
+ assignNativeObject(nativeCopyFromSurfaceControl(other.mNativeObject), callsite);
+ }
+
+ /**
+ * owner UID.
+ * @hide
+ */
+ public static final int METADATA_OWNER_UID = 1;
+
+ /**
+ * Window type as per {@link WindowManager.LayoutParams}.
+ * @hide
+ */
+ public static final int METADATA_WINDOW_TYPE = 2;
+
+ /**
+ * Task id to allow association between surfaces and task.
+ * @hide
+ */
+ public static final int METADATA_TASK_ID = 3;
+
+ /**
+ * The style of mouse cursor and hotspot.
+ * @hide
+ */
+ public static final int METADATA_MOUSE_CURSOR = 4;
+
+ /**
+ * Accessibility ID to allow association between surfaces and accessibility tree.
+ * @hide
+ */
+ public static final int METADATA_ACCESSIBILITY_ID = 5;
+
+ /**
+ * owner PID.
+ * @hide
+ */
+ public static final int METADATA_OWNER_PID = 6;
+
+ /**
+ * game mode for the layer - used for metrics
+ * @hide
+ */
+ public static final int METADATA_GAME_MODE = 8;
+
+ /**
+ * A wrapper around HardwareBuffer that contains extra information about how to
+ * interpret the screenshot HardwareBuffer.
+ *
+ * @hide
+ */
+ public static class ScreenshotHardwareBuffer {
+ private final HardwareBuffer mHardwareBuffer;
+ private final ColorSpace mColorSpace;
+ private final boolean mContainsSecureLayers;
+
+ public ScreenshotHardwareBuffer(HardwareBuffer hardwareBuffer, ColorSpace colorSpace,
+ boolean containsSecureLayers) {
+ mHardwareBuffer = hardwareBuffer;
+ mColorSpace = colorSpace;
+ mContainsSecureLayers = containsSecureLayers;
+ }
+
+ /**
+ * Create ScreenshotHardwareBuffer from an existing HardwareBuffer object.
+ * @param hardwareBuffer The existing HardwareBuffer object
+ * @param namedColorSpace Integer value of a named color space {@link ColorSpace.Named}
+ * @param containsSecureLayers Indicates whether this graphic buffer contains captured
+ * contents
+ * of secure layers, in which case the screenshot should not be persisted.
+ */
+ private static ScreenshotHardwareBuffer createFromNative(HardwareBuffer hardwareBuffer,
+ int namedColorSpace, boolean containsSecureLayers) {
+ ColorSpace colorSpace = ColorSpace.get(ColorSpace.Named.values()[namedColorSpace]);
+ return new ScreenshotHardwareBuffer(hardwareBuffer, colorSpace, containsSecureLayers);
+ }
+
+ public ColorSpace getColorSpace() {
+ return mColorSpace;
+ }
+
+ public HardwareBuffer getHardwareBuffer() {
+ return mHardwareBuffer;
+ }
+
+ public boolean containsSecureLayers() {
+ return mContainsSecureLayers;
+ }
+
+ /**
+ * Copy content of ScreenshotHardwareBuffer into a hardware bitmap and return it.
+ * Note: If you want to modify the Bitmap in software, you will need to copy the Bitmap
+ * into
+ * a software Bitmap using {@link Bitmap#copy(Bitmap.Config, boolean)}
+ *
+ * CAVEAT: This can be extremely slow; avoid use unless absolutely necessary; prefer to
+ * directly
+ * use the {@link HardwareBuffer} directly.
+ *
+ * @return Bitmap generated from the {@link HardwareBuffer}
+ */
+ public Bitmap asBitmap() {
+ if (mHardwareBuffer == null) {
+ Log.w(TAG, "Failed to take screenshot. Null screenshot object");
+ return null;
+ }
+ return Bitmap.wrapHardwareBuffer(mHardwareBuffer, mColorSpace);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public interface ScreenCaptureListener {
+ /**
+ * The callback invoked when the screen capture is complete.
+ * @param hardwareBuffer Data containing info about the screen capture.
+ */
+ void onScreenCaptureComplete(ScreenshotHardwareBuffer hardwareBuffer);
+ }
+
+ private static class SyncScreenCaptureListener implements ScreenCaptureListener {
+ private static final int SCREENSHOT_WAIT_TIME_S = 1;
+ private ScreenshotHardwareBuffer mScreenshotHardwareBuffer;
+ private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ @Override
+ public void onScreenCaptureComplete(ScreenshotHardwareBuffer hardwareBuffer) {
+ mScreenshotHardwareBuffer = hardwareBuffer;
+ mCountDownLatch.countDown();
+ }
+
+ private ScreenshotHardwareBuffer waitForScreenshot() {
+ try {
+ mCountDownLatch.await(SCREENSHOT_WAIT_TIME_S, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to wait for screen capture result", e);
+ }
+
+ return mScreenshotHardwareBuffer;
+ }
+ }
+
+ /**
+ * A common arguments class used for various screenshot requests. This contains arguments that
+ * are shared between {@link DisplayCaptureArgs} and {@link LayerCaptureArgs}
+ * @hide
+ */
+ private abstract static class CaptureArgs {
+ private final int mPixelFormat;
+ private final Rect mSourceCrop = new Rect();
+ private final float mFrameScaleX;
+ private final float mFrameScaleY;
+ private final boolean mCaptureSecureLayers;
+ private final boolean mAllowProtected;
+ private final long mUid;
+ private final boolean mGrayscale;
+
+ private CaptureArgs(Builder<? extends Builder<?>> builder) {
+ mPixelFormat = builder.mPixelFormat;
+ mSourceCrop.set(builder.mSourceCrop);
+ mFrameScaleX = builder.mFrameScaleX;
+ mFrameScaleY = builder.mFrameScaleY;
+ mCaptureSecureLayers = builder.mCaptureSecureLayers;
+ mAllowProtected = builder.mAllowProtected;
+ mUid = builder.mUid;
+ mGrayscale = builder.mGrayscale;
+ }
+
+ /**
+ * The Builder class used to construct {@link CaptureArgs}
+ *
+ * @param <T> A builder that extends {@link Builder}
+ */
+ abstract static class Builder<T extends Builder<T>> {
+ private int mPixelFormat = PixelFormat.RGBA_8888;
+ private final Rect mSourceCrop = new Rect();
+ private float mFrameScaleX = 1;
+ private float mFrameScaleY = 1;
+ private boolean mCaptureSecureLayers;
+ private boolean mAllowProtected;
+ private long mUid = -1;
+ private boolean mGrayscale;
+
+ /**
+ * The desired pixel format of the returned buffer.
+ */
+ public T setPixelFormat(int pixelFormat) {
+ mPixelFormat = pixelFormat;
+ return getThis();
+ }
+
+ /**
+ * The portion of the screen to capture into the buffer. Caller may pass in
+ * 'new Rect()' if no cropping is desired.
+ */
+ public T setSourceCrop(Rect sourceCrop) {
+ mSourceCrop.set(sourceCrop);
+ return getThis();
+ }
+
+ /**
+ * The desired scale of the returned buffer. The raw screen will be scaled up/down.
+ */
+ public T setFrameScale(float frameScale) {
+ mFrameScaleX = frameScale;
+ mFrameScaleY = frameScale;
+ return getThis();
+ }
+
+ /**
+ * The desired scale of the returned buffer, allowing separate values for x and y scale.
+ * The raw screen will be scaled up/down.
+ */
+ public T setFrameScale(float frameScaleX, float frameScaleY) {
+ mFrameScaleX = frameScaleX;
+ mFrameScaleY = frameScaleY;
+ return getThis();
+ }
+
+ /**
+ * Whether to allow the screenshot of secure layers. Warning: This should only be done
+ * if the content will be placed in a secure SurfaceControl.
+ *
+ * @see ScreenshotHardwareBuffer#containsSecureLayers()
+ */
+ public T setCaptureSecureLayers(boolean captureSecureLayers) {
+ mCaptureSecureLayers = captureSecureLayers;
+ return getThis();
+ }
+
+ /**
+ * Whether to allow the screenshot of protected (DRM) content. Warning: The screenshot
+ * cannot be read in unprotected space.
+ *
+ * @see HardwareBuffer#USAGE_PROTECTED_CONTENT
+ */
+ public T setAllowProtected(boolean allowProtected) {
+ mAllowProtected = allowProtected;
+ return getThis();
+ }
+
+ /**
+ * Set the uid of the content that should be screenshot. The code will skip any surfaces
+ * that don't belong to the specified uid.
+ */
+ public T setUid(long uid) {
+ mUid = uid;
+ return getThis();
+ }
+
+ /**
+ * Set whether the screenshot should use grayscale or not.
+ */
+ public T setGrayscale(boolean grayscale) {
+ mGrayscale = grayscale;
+ return getThis();
+ }
+
+ /**
+ * Each sub class should return itself to allow the builder to chain properly
+ */
+ abstract T getThis();
+ }
+ }
+
+ /**
+ * The arguments class used to make display capture requests.
+ *
+ * @see #nativeCaptureDisplay(DisplayCaptureArgs, ScreenCaptureListener)
+ * @hide
+ */
+ public static class DisplayCaptureArgs extends CaptureArgs {
+ private final IBinder mDisplayToken;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mUseIdentityTransform;
+
+ private DisplayCaptureArgs(Builder builder) {
+ super(builder);
+ mDisplayToken = builder.mDisplayToken;
+ mWidth = builder.mWidth;
+ mHeight = builder.mHeight;
+ mUseIdentityTransform = builder.mUseIdentityTransform;
+ }
+
+ /**
+ * The Builder class used to construct {@link DisplayCaptureArgs}
+ */
+ public static class Builder extends CaptureArgs.Builder<Builder> {
+ private IBinder mDisplayToken;
+ private int mWidth;
+ private int mHeight;
+ private boolean mUseIdentityTransform;
+
+ /**
+ * Construct a new {@link LayerCaptureArgs} with the set parameters. The builder
+ * remains valid.
+ */
+ public DisplayCaptureArgs build() {
+ if (mDisplayToken == null) {
+ throw new IllegalStateException(
+ "Can't take screenshot with null display token");
+ }
+ return new DisplayCaptureArgs(this);
+ }
+
+ public Builder(IBinder displayToken) {
+ setDisplayToken(displayToken);
+ }
+
+ /**
+ * The display to take the screenshot of.
+ */
+ public Builder setDisplayToken(IBinder displayToken) {
+ mDisplayToken = displayToken;
+ return this;
+ }
+
+ /**
+ * Set the desired size of the returned buffer. The raw screen will be scaled down to
+ * this size
+ *
+ * @param width The desired width of the returned buffer. Caller may pass in 0 if no
+ * scaling is desired.
+ * @param height The desired height of the returned buffer. Caller may pass in 0 if no
+ * scaling is desired.
+ */
+ public Builder setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ return this;
+ }
+
+ /**
+ * Replace the rotation transform of the display with the identity transformation while
+ * taking the screenshot. This ensures the screenshot is taken in the ROTATION_0
+ * orientation. Set this value to false if the screenshot should be taken in the
+ * current screen orientation.
+ */
+ public Builder setUseIdentityTransform(boolean useIdentityTransform) {
+ mUseIdentityTransform = useIdentityTransform;
+ return this;
+ }
+
+ @Override
+ Builder getThis() {
+ return this;
+ }
+ }
+ }
+
+ /**
+ * The arguments class used to make layer capture requests.
+ *
+ * @see #nativeCaptureLayers(LayerCaptureArgs, ScreenCaptureListener)
+ * @hide
+ */
+ public static class LayerCaptureArgs extends CaptureArgs {
+ private final long mNativeLayer;
+ private final long[] mNativeExcludeLayers;
+ private final boolean mChildrenOnly;
+
+ private LayerCaptureArgs(Builder builder) {
+ super(builder);
+ mChildrenOnly = builder.mChildrenOnly;
+ mNativeLayer = builder.mLayer.mNativeObject;
+ if (builder.mExcludeLayers != null) {
+ mNativeExcludeLayers = new long[builder.mExcludeLayers.length];
+ for (int i = 0; i < builder.mExcludeLayers.length; i++) {
+ mNativeExcludeLayers[i] = builder.mExcludeLayers[i].mNativeObject;
+ }
+ } else {
+ mNativeExcludeLayers = null;
+ }
+ }
+
+ /**
+ * The Builder class used to construct {@link LayerCaptureArgs}
+ */
+ public static class Builder extends CaptureArgs.Builder<Builder> {
+ private SurfaceControl mLayer;
+ private SurfaceControl[] mExcludeLayers;
+ private boolean mChildrenOnly = true;
+
+ /**
+ * Construct a new {@link LayerCaptureArgs} with the set parameters. The builder
+ * remains valid.
+ */
+ public LayerCaptureArgs build() {
+ if (mLayer == null) {
+ throw new IllegalStateException(
+ "Can't take screenshot with null layer");
+ }
+ return new LayerCaptureArgs(this);
+ }
+
+ public Builder(SurfaceControl layer) {
+ setLayer(layer);
+ }
+
+ /**
+ * The root layer to capture.
+ */
+ public Builder setLayer(SurfaceControl layer) {
+ mLayer = layer;
+ return this;
+ }
+
+
+ /**
+ * An array of layer handles to exclude.
+ */
+ public Builder setExcludeLayers(@Nullable SurfaceControl[] excludeLayers) {
+ mExcludeLayers = excludeLayers;
+ return this;
+ }
+
+ /**
+ * Whether to include the layer itself in the screenshot or just the children and their
+ * descendants.
+ */
+ public Builder setChildrenOnly(boolean childrenOnly) {
+ mChildrenOnly = childrenOnly;
+ return this;
+ }
+
+ @Override
+ Builder getThis() {
+ return this;
+ }
+
+ }
+ }
+
+ /**
+ * Builder class for {@link SurfaceControl} objects.
+ *
+ * By default the surface will be hidden, and have "unset" bounds, meaning it can
+ * be as large as the bounds of its parent if a buffer or child so requires.
+ *
+ * It is necessary to set at least a name via {@link Builder#setName}
+ */
+ public static class Builder {
+ private SurfaceSession mSession;
+ private int mFlags = HIDDEN;
+ private int mWidth;
+ private int mHeight;
+ private int mFormat = PixelFormat.OPAQUE;
+ private String mName;
+ private WeakReference<View> mLocalOwnerView;
+ private SurfaceControl mParent;
+ private SparseIntArray mMetadata;
+ private String mCallsite = "SurfaceControl.Builder";
+
+ /**
+ * Begin building a SurfaceControl with a given {@link SurfaceSession}.
+ *
+ * @param session The {@link SurfaceSession} with which to eventually construct the surface.
+ * @hide
+ */
+ public Builder(SurfaceSession session) {
+ mSession = session;
+ }
+
+ /**
+ * Begin building a SurfaceControl.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Construct a new {@link SurfaceControl} with the set parameters. The builder
+ * remains valid.
+ */
+ @NonNull
+ public SurfaceControl build() {
+ if (mWidth < 0 || mHeight < 0) {
+ throw new IllegalStateException(
+ "width and height must be positive or unset");
+ }
+ if ((mWidth > 0 || mHeight > 0) && (isEffectLayer() || isContainerLayer())) {
+ throw new IllegalStateException(
+ "Only buffer layers can set a valid buffer size.");
+ }
+
+ if ((mFlags & FX_SURFACE_MASK) == FX_SURFACE_NORMAL) {
+ setBLASTLayer();
+ }
+
+ return new SurfaceControl(
+ mSession, mName, mWidth, mHeight, mFormat, mFlags, mParent, mMetadata,
+ mLocalOwnerView, mCallsite);
+ }
+
+ /**
+ * Set a debugging-name for the SurfaceControl.
+ *
+ * @param name A name to identify the Surface in debugging.
+ */
+ @NonNull
+ public Builder setName(@NonNull String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Set the local owner view for the surface. This view is only
+ * valid in the same process and is not transferred in an IPC.
+ *
+ * Note: This is used for cases where we want to know the view
+ * that manages the surface control while intercepting reparenting.
+ * A specific example is InlineContentView which exposes is surface
+ * control for reparenting as a way to implement clipping of several
+ * InlineContentView instances within a certain area.
+ *
+ * @param view The owner view.
+ * @return This builder.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setLocalOwnerView(@NonNull View view) {
+ mLocalOwnerView = new WeakReference<>(view);
+ return this;
+ }
+
+ /**
+ * Set the initial size of the controlled surface's buffers in pixels.
+ *
+ * @param width The buffer width in pixels.
+ * @param height The buffer height in pixels.
+ */
+ @NonNull
+ public Builder setBufferSize(@IntRange(from = 0) int width,
+ @IntRange(from = 0) int height) {
+ if (width < 0 || height < 0) {
+ throw new IllegalArgumentException(
+ "width and height must be positive");
+ }
+ mWidth = width;
+ mHeight = height;
+ // set this as a buffer layer since we are specifying a buffer size.
+ return setFlags(FX_SURFACE_NORMAL, FX_SURFACE_MASK);
+ }
+
+ private void unsetBufferSize() {
+ mWidth = 0;
+ mHeight = 0;
+ }
+
+ /**
+ * Set the pixel format of the controlled surface's buffers, using constants from
+ * {@link android.graphics.PixelFormat}.
+ */
+ @NonNull
+ public Builder setFormat(@PixelFormat.Format int format) {
+ mFormat = format;
+ return this;
+ }
+
+ /**
+ * Specify if the app requires a hardware-protected path to
+ * an external display sync. If protected content is enabled, but
+ * such a path is not available, then the controlled Surface will
+ * not be displayed.
+ *
+ * @param protectedContent Whether to require a protected sink.
+ * @hide
+ */
+ @NonNull
+ public Builder setProtected(boolean protectedContent) {
+ if (protectedContent) {
+ mFlags |= PROTECTED_APP;
+ } else {
+ mFlags &= ~PROTECTED_APP;
+ }
+ return this;
+ }
+
+ /**
+ * Specify whether the Surface contains secure content. If true, the system
+ * will prevent the surfaces content from being copied by another process. In
+ * particular screenshots and VNC servers will be disabled. This is however
+ * not a complete prevention of readback as {@link #setProtected}.
+ * @hide
+ */
+ @NonNull
+ public Builder setSecure(boolean secure) {
+ if (secure) {
+ mFlags |= SECURE;
+ } else {
+ mFlags &= ~SECURE;
+ }
+ return this;
+ }
+
+ /**
+ * Indicates whether the surface must be considered opaque,
+ * even if its pixel format is set to translucent. This can be useful if an
+ * application needs full RGBA 8888 support for instance but will
+ * still draw every pixel opaque.
+ * <p>
+ * This flag only determines whether opacity will be sampled from the alpha channel.
+ * Plane-alpha from calls to setAlpha() can still result in blended composition
+ * regardless of the opaque setting.
+ *
+ * Combined effects are (assuming a buffer format with an alpha channel):
+ * <ul>
+ * <li>OPAQUE + alpha(1.0) == opaque composition
+ * <li>OPAQUE + alpha(0.x) == blended composition
+ * <li>OPAQUE + alpha(0.0) == no composition
+ * <li>!OPAQUE + alpha(1.0) == blended composition
+ * <li>!OPAQUE + alpha(0.x) == blended composition
+ * <li>!OPAQUE + alpha(0.0) == no composition
+ * </ul>
+ * If the underlying buffer lacks an alpha channel, it is as if setOpaque(true)
+ * were set automatically.
+ * @param opaque Whether the Surface is OPAQUE.
+ */
+ @NonNull
+ public Builder setOpaque(boolean opaque) {
+ if (opaque) {
+ mFlags |= OPAQUE;
+ } else {
+ mFlags &= ~OPAQUE;
+ }
+ return this;
+ }
+
+ /**
+ * Set the initial visibility for the SurfaceControl.
+ *
+ * @param hidden Whether the Surface is initially HIDDEN.
+ * @hide
+ */
+ @NonNull
+ public Builder setHidden(boolean hidden) {
+ if (hidden) {
+ mFlags |= HIDDEN;
+ } else {
+ mFlags &= ~HIDDEN;
+ }
+ return this;
+ }
+
+ /**
+ * Set a parent surface for our new SurfaceControl.
+ *
+ * Child surfaces are constrained to the onscreen region of their parent.
+ * Furthermore they stack relatively in Z order, and inherit the transformation
+ * of the parent.
+ *
+ * @param parent The parent control.
+ */
+ @NonNull
+ public Builder setParent(@Nullable SurfaceControl parent) {
+ mParent = parent;
+ return this;
+ }
+
+ /**
+ * Sets a metadata int.
+ *
+ * @param key metadata key
+ * @param data associated data
+ * @hide
+ */
+ public Builder setMetadata(int key, int data) {
+ if (mMetadata == null) {
+ mMetadata = new SparseIntArray();
+ }
+ mMetadata.put(key, data);
+ return this;
+ }
+
+ /**
+ * Indicate whether an 'EffectLayer' is to be constructed.
+ *
+ * An effect layer behaves like a container layer by default but it can support
+ * color fill, shadows and/or blur. These layers will not have an associated buffer.
+ * When created, this layer has no effects set and will be transparent but the caller
+ * can render an effect by calling:
+ * - {@link Transaction#setColor(SurfaceControl, float[])}
+ * - {@link Transaction#setBackgroundBlurRadius(SurfaceControl, int)}
+ * - {@link Transaction#setShadowRadius(SurfaceControl, float)}
+ *
+ * @hide
+ */
+ public Builder setEffectLayer() {
+ mFlags |= NO_COLOR_FILL;
+ unsetBufferSize();
+ return setFlags(FX_SURFACE_EFFECT, FX_SURFACE_MASK);
+ }
+
+ /**
+ * A convenience function to create an effect layer with a default color fill
+ * applied to it. Currently that color is black.
+ *
+ * @hide
+ */
+ public Builder setColorLayer() {
+ unsetBufferSize();
+ return setFlags(FX_SURFACE_EFFECT, FX_SURFACE_MASK);
+ }
+
+ private boolean isEffectLayer() {
+ return (mFlags & FX_SURFACE_EFFECT) == FX_SURFACE_EFFECT;
+ }
+
+ /**
+ * @hide
+ */
+ public Builder setBLASTLayer() {
+ return setFlags(FX_SURFACE_BLAST, FX_SURFACE_MASK);
+ }
+
+ /**
+ * Indicates whether a 'ContainerLayer' is to be constructed.
+ *
+ * Container layers will not be rendered in any fashion and instead are used
+ * as a parent of renderable layers.
+ *
+ * @hide
+ */
+ public Builder setContainerLayer() {
+ unsetBufferSize();
+ return setFlags(FX_SURFACE_CONTAINER, FX_SURFACE_MASK);
+ }
+
+ private boolean isContainerLayer() {
+ return (mFlags & FX_SURFACE_CONTAINER) == FX_SURFACE_CONTAINER;
+ }
+
+ /**
+ * Set 'Surface creation flags' such as {@link #HIDDEN}, {@link #SECURE}.
+ *
+ * TODO: Finish conversion to individual builder methods?
+ * @param flags The combined flags
+ * @hide
+ */
+ public Builder setFlags(int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets the callsite this SurfaceControl is constructed from.
+ *
+ * @param callsite String uniquely identifying callsite that created this object. Used for
+ * leakage tracking.
+ * @hide
+ */
+ public Builder setCallsite(String callsite) {
+ mCallsite = callsite;
+ return this;
+ }
+
+ private Builder setFlags(int flags, int mask) {
+ mFlags = (mFlags & ~mask) | flags;
+ return this;
+ }
+ }
+
+ /**
+ * Create a surface with a name.
+ * <p>
+ * The surface creation flags specify what kind of surface to create and
+ * certain options such as whether the surface can be assumed to be opaque
+ * and whether it should be initially hidden. Surfaces should always be
+ * created with the {@link #HIDDEN} flag set to ensure that they are not
+ * made visible prematurely before all of the surface's properties have been
+ * configured.
+ * <p>
+ * Good practice is to first create the surface with the {@link #HIDDEN} flag
+ * specified, open a transaction, set the surface layer, layer stack, alpha,
+ * and position, call {@link Transaction#show(SurfaceControl)} if appropriate, and close the
+ * transaction.
+ * <p>
+ * Bounds of the surface is determined by its crop and its buffer size. If the
+ * surface has no buffer or crop, the surface is boundless and only constrained
+ * by the size of its parent bounds.
+ *
+ * @param session The surface session, must not be null.
+ * @param name The surface name, must not be null.
+ * @param w The surface initial width.
+ * @param h The surface initial height.
+ * @param flags The surface creation flags.
+ * @param metadata Initial metadata.
+ * @param callsite String uniquely identifying callsite that created this object. Used for
+ * leakage tracking.
+ * @throws throws OutOfResourcesException If the SurfaceControl cannot be created.
+ */
+ private SurfaceControl(SurfaceSession session, String name, int w, int h, int format, int flags,
+ SurfaceControl parent, SparseIntArray metadata, WeakReference<View> localOwnerView,
+ String callsite)
+ throws OutOfResourcesException, IllegalArgumentException {
+ if (name == null) {
+ throw new IllegalArgumentException("name must not be null");
+ }
+
+ mName = name;
+ mWidth = w;
+ mHeight = h;
+ mLocalOwnerView = localOwnerView;
+ Parcel metaParcel = Parcel.obtain();
+ try {
+ if (metadata != null && metadata.size() > 0) {
+ metaParcel.writeInt(metadata.size());
+ for (int i = 0; i < metadata.size(); ++i) {
+ metaParcel.writeInt(metadata.keyAt(i));
+ metaParcel.writeByteArray(
+ ByteBuffer.allocate(4).order(ByteOrder.nativeOrder())
+ .putInt(metadata.valueAt(i)).array());
+ }
+ metaParcel.setDataPosition(0);
+ }
+ mNativeObject = nativeCreate(session, name, w, h, format, flags,
+ parent != null ? parent.mNativeObject : 0, metaParcel);
+ } finally {
+ metaParcel.recycle();
+ }
+ if (mNativeObject == 0) {
+ throw new OutOfResourcesException(
+ "Couldn't allocate SurfaceControl native object");
+ }
+ mNativeHandle = nativeGetHandle(mNativeObject);
+ mCloseGuard.openWithCallSite("release", callsite);
+ }
+
+ /**
+ * Copy constructor. Creates a new native object pointing to the same surface as {@code other}.
+ *
+ * @param other The object to copy the surface from.
+ * @param callsite String uniquely identifying callsite that created this object. Used for
+ * leakage tracking.
+ * @hide
+ */
+ @TestApi
+ public SurfaceControl(@NonNull SurfaceControl other, @NonNull String callsite) {
+ copyFrom(other, callsite);
+ }
+
+ private SurfaceControl(Parcel in) {
+ readFromParcel(in);
+ }
+
+ /**
+ * @hide
+ */
+ public SurfaceControl() {
+ }
+
+ public void readFromParcel(Parcel in) {
+ if (in == null) {
+ throw new IllegalArgumentException("source must not be null");
+ }
+
+ mName = in.readString8();
+ mWidth = in.readInt();
+ mHeight = in.readInt();
+
+ long object = 0;
+ if (in.readInt() != 0) {
+ object = nativeReadFromParcel(in);
+ }
+ assignNativeObject(object, "readFromParcel");
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString8(mName);
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
+ if (mNativeObject == 0) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ }
+ nativeWriteToParcel(mNativeObject, dest);
+
+ if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+ release();
+ }
+ }
+
+ /**
+ * Checks whether two {@link SurfaceControl} objects represent the same surface.
+ *
+ * @param other The other object to check
+ * @return {@code true} if these two {@link SurfaceControl} objects represent the same surface.
+ * @hide
+ */
+ @TestApi
+ public boolean isSameSurface(@NonNull SurfaceControl other) {
+ return other.mNativeHandle == mNativeHandle;
+ }
+
+ /**
+ * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
+ * android.view.SurfaceControlProto}.
+ *
+ * @param proto Stream to write the SurfaceControl object to.
+ * @param fieldId Field Id of the SurfaceControl as defined in the parent message.
+ * @hide
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(HASH_CODE, System.identityHashCode(this));
+ proto.write(NAME, mName);
+ proto.end(token);
+ }
+
+ public static final @android.annotation.NonNull Creator<SurfaceControl> CREATOR
+ = new Creator<SurfaceControl>() {
+ public SurfaceControl createFromParcel(Parcel in) {
+ return new SurfaceControl(in);
+ }
+
+ public SurfaceControl[] newArray(int size) {
+ return new SurfaceControl[size];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ if (mNativeObject != 0) {
+ nativeRelease(mNativeObject);
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Release the local reference to the server-side surface. The surface
+ * may continue to exist on-screen as long as its parent continues
+ * to exist. To explicitly remove a surface from the screen use
+ * {@link Transaction#reparent} with a null-parent. After release,
+ * {@link #isValid} will return false and other methods will throw
+ * an exception.
+ *
+ * Always call release() when you're done with a SurfaceControl.
+ */
+ public void release() {
+ if (mNativeObject != 0) {
+ nativeRelease(mNativeObject);
+ mNativeObject = 0;
+ mNativeHandle = 0;
+ mCloseGuard.close();
+ }
+ }
+
+ /**
+ * Disconnect any client still connected to the surface.
+ * @hide
+ */
+ public void disconnect() {
+ if (mNativeObject != 0) {
+ nativeDisconnect(mNativeObject);
+ }
+ }
+
+ private void checkNotReleased() {
+ if (mNativeObject == 0) throw new NullPointerException(
+ "Invalid " + this + ", mNativeObject is null. Have you called release() already?");
+ }
+
+ /**
+ * Check whether this instance points to a valid layer with the system-compositor. For
+ * example this may be false if construction failed, or the layer was released
+ * ({@link #release}).
+ *
+ * @return Whether this SurfaceControl is valid.
+ */
+ public boolean isValid() {
+ return mNativeObject != 0;
+ }
+
+ /*
+ * set surface parameters.
+ * needs to be inside open/closeTransaction block
+ */
+
+ /** start a transaction
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static void openTransaction() {
+ synchronized (SurfaceControl.class) {
+ if (sGlobalTransaction == null) {
+ sGlobalTransaction = new GlobalTransactionWrapper();
+ }
+ synchronized(SurfaceControl.class) {
+ sTransactionNestCount++;
+ }
+ }
+ }
+
+ /**
+ * Merge the supplied transaction in to the deprecated "global" transaction.
+ * This clears the supplied transaction in an identical fashion to {@link Transaction#merge}.
+ * <p>
+ * This is a utility for interop with legacy-code and will go away with the Global Transaction.
+ * @hide
+ */
+ @Deprecated
+ public static void mergeToGlobalTransaction(Transaction t) {
+ synchronized(SurfaceControl.class) {
+ sGlobalTransaction.merge(t);
+ }
+ }
+
+ /** end a transaction
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static void closeTransaction() {
+ synchronized(SurfaceControl.class) {
+ if (sTransactionNestCount == 0) {
+ Log.e(TAG,
+ "Call to SurfaceControl.closeTransaction without matching openTransaction");
+ } else if (--sTransactionNestCount > 0) {
+ return;
+ }
+ sGlobalTransaction.applyGlobalTransaction(false);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean clearContentFrameStats() {
+ checkNotReleased();
+ return nativeClearContentFrameStats(mNativeObject);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean getContentFrameStats(WindowContentFrameStats outStats) {
+ checkNotReleased();
+ return nativeGetContentFrameStats(mNativeObject, outStats);
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean clearAnimationFrameStats() {
+ return nativeClearAnimationFrameStats();
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean getAnimationFrameStats(WindowAnimationFrameStats outStats) {
+ return nativeGetAnimationFrameStats(outStats);
+ }
+
+ /**
+ * @hide
+ */
+ public int getWidth() {
+ synchronized (mLock) {
+ return mWidth;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public int getHeight() {
+ synchronized (mLock) {
+ return mHeight;
+ }
+ }
+
+ /**
+ * Gets the local view that owns this surface.
+ *
+ * @return The owner view.
+ *
+ * @hide
+ */
+ public @Nullable View getLocalOwnerView() {
+ return (mLocalOwnerView != null) ? mLocalOwnerView.get() : null;
+ }
+
+ @Override
+ public String toString() {
+ return "Surface(name=" + mName + ")/@0x" +
+ Integer.toHexString(System.identityHashCode(this));
+ }
+
+ /**
+ * Immutable information about physical display.
+ *
+ * @hide
+ */
+ public static final class StaticDisplayInfo {
+ public boolean isInternal;
+ public float density;
+ public boolean secure;
+ public DeviceProductInfo deviceProductInfo;
+
+ @Override
+ public String toString() {
+ return "StaticDisplayInfo{isInternal=" + isInternal
+ + ", density=" + density
+ + ", secure=" + secure
+ + ", deviceProductInfo=" + deviceProductInfo + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ StaticDisplayInfo that = (StaticDisplayInfo) o;
+ return isInternal == that.isInternal
+ && density == that.density
+ && secure == that.secure
+ && Objects.equals(deviceProductInfo, that.deviceProductInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isInternal, density, secure, deviceProductInfo);
+ }
+ }
+
+ /**
+ * Dynamic information about physical display.
+ *
+ * @hide
+ */
+ public static final class DynamicDisplayInfo {
+ public DisplayMode[] supportedDisplayModes;
+ public int activeDisplayModeId;
+
+ public int[] supportedColorModes;
+ public int activeColorMode;
+
+ public Display.HdrCapabilities hdrCapabilities;
+
+ public boolean autoLowLatencyModeSupported;
+ public boolean gameContentTypeSupported;
+
+ @Override
+ public String toString() {
+ return "DynamicDisplayInfo{"
+ + "supportedDisplayModes=" + Arrays.toString(supportedDisplayModes)
+ + ", activeDisplayModeId=" + activeDisplayModeId
+ + ", supportedColorModes=" + Arrays.toString(supportedColorModes)
+ + ", activeColorMode=" + activeColorMode
+ + ", hdrCapabilities=" + hdrCapabilities
+ + ", autoLowLatencyModeSupported=" + autoLowLatencyModeSupported
+ + ", gameContentTypeSupported" + gameContentTypeSupported + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DynamicDisplayInfo that = (DynamicDisplayInfo) o;
+ return Arrays.equals(supportedDisplayModes, that.supportedDisplayModes)
+ && activeDisplayModeId == that.activeDisplayModeId
+ && Arrays.equals(supportedColorModes, that.supportedColorModes)
+ && activeColorMode == that.activeColorMode
+ && Objects.equals(hdrCapabilities, that.hdrCapabilities);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(supportedDisplayModes, activeDisplayModeId, activeDisplayModeId,
+ activeColorMode, hdrCapabilities);
+ }
+ }
+
+ /**
+ * Configuration supported by physical display.
+ *
+ * @hide
+ */
+ public static final class DisplayMode {
+ /**
+ * Invalid display config id.
+ */
+ public static final int INVALID_DISPLAY_MODE_ID = -1;
+
+ public int id;
+ public int width;
+ public int height;
+ public float xDpi;
+ public float yDpi;
+
+ public float refreshRate;
+ public long appVsyncOffsetNanos;
+ public long presentationDeadlineNanos;
+
+ /**
+ * The config group ID this config is associated to.
+ * Configs in the same group are similar from vendor's perspective and switching between
+ * configs within the same group can be done seamlessly in most cases.
+ * @see: [email protected]::IComposerClient::Attribute::CONFIG_GROUP
+ */
+ public int group;
+
+ @Override
+ public String toString() {
+ return "DisplayMode{id=" + id
+ + ", width=" + width
+ + ", height=" + height
+ + ", xDpi=" + xDpi
+ + ", yDpi=" + yDpi
+ + ", refreshRate=" + refreshRate
+ + ", appVsyncOffsetNanos=" + appVsyncOffsetNanos
+ + ", presentationDeadlineNanos=" + presentationDeadlineNanos
+ + ", group=" + group + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DisplayMode that = (DisplayMode) o;
+ return id == that.id
+ && width == that.width
+ && height == that.height
+ && Float.compare(that.xDpi, xDpi) == 0
+ && Float.compare(that.yDpi, yDpi) == 0
+ && Float.compare(that.refreshRate, refreshRate) == 0
+ && appVsyncOffsetNanos == that.appVsyncOffsetNanos
+ && presentationDeadlineNanos == that.presentationDeadlineNanos
+ && group == that.group;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, width, height, xDpi, yDpi, refreshRate, appVsyncOffsetNanos,
+ presentationDeadlineNanos, group);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void setDisplayPowerMode(IBinder displayToken, int mode) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ nativeSetDisplayPowerMode(displayToken, mode);
+ }
+
+ /**
+ * @hide
+ */
+ public static StaticDisplayInfo getStaticDisplayInfo(IBinder displayToken) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ return nativeGetStaticDisplayInfo(displayToken);
+ }
+
+ /**
+ * @hide
+ */
+ public static DynamicDisplayInfo getDynamicDisplayInfo(IBinder displayToken) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ return nativeGetDynamicDisplayInfo(displayToken);
+ }
+
+ /**
+ * @hide
+ */
+ public static DisplayedContentSamplingAttributes getDisplayedContentSamplingAttributes(
+ IBinder displayToken) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ return nativeGetDisplayedContentSamplingAttributes(displayToken);
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean setDisplayedContentSamplingEnabled(
+ IBinder displayToken, boolean enable, int componentMask, int maxFrames) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ final int maxColorComponents = 4;
+ if ((componentMask >> maxColorComponents) != 0) {
+ throw new IllegalArgumentException("invalid componentMask when enabling sampling");
+ }
+ return nativeSetDisplayedContentSamplingEnabled(
+ displayToken, enable, componentMask, maxFrames);
+ }
+
+ /**
+ * @hide
+ */
+ public static DisplayedContentSample getDisplayedContentSample(
+ IBinder displayToken, long maxFrames, long timestamp) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ return nativeGetDisplayedContentSample(displayToken, maxFrames, timestamp);
+ }
+
+
+ /**
+ * Contains information about desired display configuration.
+ *
+ * @hide
+ */
+ public static final class DesiredDisplayModeSpecs {
+ public int defaultMode;
+ /**
+ * The primary refresh rate range represents display manager's general guidance on the
+ * display configs surface flinger will consider when switching refresh rates. Unless
+ * surface flinger has a specific reason to do otherwise, it will stay within this range.
+ */
+ public float primaryRefreshRateMin;
+ public float primaryRefreshRateMax;
+ /**
+ * The app request refresh rate range allows surface flinger to consider more display
+ * configs when switching refresh rates. Although surface flinger will generally stay within
+ * the primary range, specific considerations, such as layer frame rate settings specified
+ * via the setFrameRate() api, may cause surface flinger to go outside the primary
+ * range. Surface flinger never goes outside the app request range. The app request range
+ * will be greater than or equal to the primary refresh rate range, never smaller.
+ */
+ public float appRequestRefreshRateMin;
+ public float appRequestRefreshRateMax;
+
+ /**
+ * If true this will allow switching between modes in different display configuration
+ * groups. This way the user may see visual interruptions when the display mode changes.
+ */
+ public boolean allowGroupSwitching;
+
+ public DesiredDisplayModeSpecs() {}
+
+ public DesiredDisplayModeSpecs(DesiredDisplayModeSpecs other) {
+ copyFrom(other);
+ }
+
+ public DesiredDisplayModeSpecs(int defaultMode, boolean allowGroupSwitching,
+ float primaryRefreshRateMin, float primaryRefreshRateMax,
+ float appRequestRefreshRateMin, float appRequestRefreshRateMax) {
+ this.defaultMode = defaultMode;
+ this.allowGroupSwitching = allowGroupSwitching;
+ this.primaryRefreshRateMin = primaryRefreshRateMin;
+ this.primaryRefreshRateMax = primaryRefreshRateMax;
+ this.appRequestRefreshRateMin = appRequestRefreshRateMin;
+ this.appRequestRefreshRateMax = appRequestRefreshRateMax;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return o instanceof DesiredDisplayModeSpecs && equals((DesiredDisplayModeSpecs) o);
+ }
+
+ /**
+ * Tests for equality.
+ */
+ public boolean equals(DesiredDisplayModeSpecs other) {
+ return other != null && defaultMode == other.defaultMode
+ && primaryRefreshRateMin == other.primaryRefreshRateMin
+ && primaryRefreshRateMax == other.primaryRefreshRateMax
+ && appRequestRefreshRateMin == other.appRequestRefreshRateMin
+ && appRequestRefreshRateMax == other.appRequestRefreshRateMax;
+ }
+
+ @Override
+ public int hashCode() {
+ return 0; // don't care
+ }
+
+ /**
+ * Copies the supplied object's values to this object.
+ */
+ public void copyFrom(DesiredDisplayModeSpecs other) {
+ defaultMode = other.defaultMode;
+ primaryRefreshRateMin = other.primaryRefreshRateMin;
+ primaryRefreshRateMax = other.primaryRefreshRateMax;
+ appRequestRefreshRateMin = other.appRequestRefreshRateMin;
+ appRequestRefreshRateMax = other.appRequestRefreshRateMax;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("defaultConfig=%d primaryRefreshRateRange=[%.0f %.0f]"
+ + " appRequestRefreshRateRange=[%.0f %.0f]",
+ defaultMode, primaryRefreshRateMin, primaryRefreshRateMax,
+ appRequestRefreshRateMin, appRequestRefreshRateMax);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean setDesiredDisplayModeSpecs(IBinder displayToken,
+ DesiredDisplayModeSpecs desiredDisplayModeSpecs) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ if (desiredDisplayModeSpecs == null) {
+ throw new IllegalArgumentException("desiredDisplayModeSpecs must not be null");
+ }
+ if (desiredDisplayModeSpecs.defaultMode < 0) {
+ throw new IllegalArgumentException("defaultMode must be non-negative");
+ }
+
+ return nativeSetDesiredDisplayModeSpecs(displayToken, desiredDisplayModeSpecs);
+ }
+
+ /**
+ * @hide
+ */
+ public static DesiredDisplayModeSpecs getDesiredDisplayModeSpecs(
+ IBinder displayToken) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+
+ return nativeGetDesiredDisplayModeSpecs(displayToken);
+ }
+
+ /**
+ * Color coordinates in CIE1931 XYZ color space
+ *
+ * @hide
+ */
+ public static final class CieXyz {
+ /**
+ * @hide
+ */
+ public float X;
+
+ /**
+ * @hide
+ */
+ public float Y;
+
+ /**
+ * @hide
+ */
+ public float Z;
+ }
+
+ /**
+ * Contains a display's color primaries
+ *
+ * @hide
+ */
+ public static final class DisplayPrimaries {
+ /**
+ * @hide
+ */
+ public CieXyz red;
+
+ /**
+ * @hide
+ */
+ public CieXyz green;
+
+ /**
+ * @hide
+ */
+ public CieXyz blue;
+
+ /**
+ * @hide
+ */
+ public CieXyz white;
+
+ /**
+ * @hide
+ */
+ public DisplayPrimaries() {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static DisplayPrimaries getDisplayNativePrimaries(
+ IBinder displayToken) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+
+ return nativeGetDisplayNativePrimaries(displayToken);
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean setActiveColorMode(IBinder displayToken, int colorMode) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ return nativeSetActiveColorMode(displayToken, colorMode);
+ }
+
+ /**
+ * Returns an array of color spaces with 2 elements. The first color space is the
+ * default color space and second one is wide color gamut color space.
+ * @hide
+ */
+ public static ColorSpace[] getCompositionColorSpaces() {
+ int[] dataspaces = nativeGetCompositionDataspaces();
+ ColorSpace srgb = ColorSpace.get(ColorSpace.Named.SRGB);
+ ColorSpace[] colorSpaces = { srgb, srgb };
+ if (dataspaces.length == 2) {
+ for (int i = 0; i < 2; ++i) {
+ switch(dataspaces[i]) {
+ case INTERNAL_DATASPACE_DISPLAY_P3:
+ colorSpaces[i] = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
+ break;
+ case INTERNAL_DATASPACE_SCRGB:
+ colorSpaces[i] = ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB);
+ break;
+ case INTERNAL_DATASPACE_SRGB:
+ // Other dataspace is not recognized, use SRGB color space instead,
+ // the default value of the array is already SRGB, thus do nothing.
+ default:
+ break;
+ }
+ }
+ }
+ return colorSpaces;
+ }
+
+ /**
+ * @hide
+ */
+ public static void setAutoLowLatencyMode(IBinder displayToken, boolean on) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+
+ nativeSetAutoLowLatencyMode(displayToken, on);
+ }
+
+ /**
+ * @hide
+ */
+ public static void setGameContentType(IBinder displayToken, boolean on) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+
+ nativeSetGameContentType(displayToken, on);
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static void setDisplayProjection(IBinder displayToken,
+ int orientation, Rect layerStackRect, Rect displayRect) {
+ synchronized (SurfaceControl.class) {
+ sGlobalTransaction.setDisplayProjection(displayToken, orientation,
+ layerStackRect, displayRect);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
+ synchronized (SurfaceControl.class) {
+ sGlobalTransaction.setDisplayLayerStack(displayToken, layerStack);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static void setDisplaySurface(IBinder displayToken, Surface surface) {
+ synchronized (SurfaceControl.class) {
+ sGlobalTransaction.setDisplaySurface(displayToken, surface);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void setDisplaySize(IBinder displayToken, int width, int height) {
+ synchronized (SurfaceControl.class) {
+ sGlobalTransaction.setDisplaySize(displayToken, width, height);
+ }
+ }
+
+ /**
+ * Overrides HDR modes for a display device.
+ *
+ * If the caller does not have ACCESS_SURFACE_FLINGER permission, this will throw a Security
+ * Exception.
+ * @hide
+ */
+ @TestApi
+ public static void overrideHdrTypes(@NonNull IBinder displayToken, @NonNull int[] modes) {
+ nativeOverrideHdrTypes(displayToken, modes);
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static IBinder createDisplay(String name, boolean secure) {
+ if (name == null) {
+ throw new IllegalArgumentException("name must not be null");
+ }
+ return nativeCreateDisplay(name, secure);
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static void destroyDisplay(IBinder displayToken) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ nativeDestroyDisplay(displayToken);
+ }
+
+ /**
+ * @hide
+ */
+ public static long[] getPhysicalDisplayIds() {
+ return nativeGetPhysicalDisplayIds();
+ }
+
+ /**
+ * @hide
+ */
+ public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
+ return nativeGetPhysicalDisplayToken(physicalDisplayId);
+ }
+
+ /**
+ * TODO(b/116025192): Remove this stopgap once framework is display-agnostic.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public static IBinder getInternalDisplayToken() {
+ final long[] physicalDisplayIds = getPhysicalDisplayIds();
+ if (physicalDisplayIds.length == 0) {
+ return null;
+ }
+ return getPhysicalDisplayToken(physicalDisplayIds[0]);
+ }
+
+ /**
+ * @param captureArgs Arguments about how to take the screenshot
+ * @param captureListener A listener to receive the screenshot callback
+ * @hide
+ */
+ public static int captureDisplay(@NonNull DisplayCaptureArgs captureArgs,
+ @NonNull ScreenCaptureListener captureListener) {
+ return nativeCaptureDisplay(captureArgs, captureListener);
+ }
+
+ /**
+ * Captures all the surfaces in a display and returns a {@link ScreenshotHardwareBuffer} with
+ * the content.
+ *
+ * @hide
+ */
+ public static ScreenshotHardwareBuffer captureDisplay(DisplayCaptureArgs captureArgs) {
+ SyncScreenCaptureListener screenCaptureListener = new SyncScreenCaptureListener();
+
+ int status = captureDisplay(captureArgs, screenCaptureListener);
+ if (status != 0) {
+ return null;
+ }
+
+ return screenCaptureListener.waitForScreenshot();
+ }
+
+ /**
+ * Captures a layer and its children and returns a {@link HardwareBuffer} with the content.
+ *
+ * @param layer The root layer to capture.
+ * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new
+ * Rect()' or null if no cropping is desired. If the root layer does not
+ * have a buffer or a crop set, then a non-empty source crop must be
+ * specified.
+ * @param frameScale The desired scale of the returned buffer; the raw
+ * screen will be scaled up/down.
+ *
+ * @return Returns a HardwareBuffer that contains the layer capture.
+ * @hide
+ */
+ public static ScreenshotHardwareBuffer captureLayers(SurfaceControl layer, Rect sourceCrop,
+ float frameScale) {
+ return captureLayers(layer, sourceCrop, frameScale, PixelFormat.RGBA_8888);
+ }
+
+ /**
+ * Captures a layer and its children and returns a {@link HardwareBuffer} with the content.
+ *
+ * @param layer The root layer to capture.
+ * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new
+ * Rect()' or null if no cropping is desired. If the root layer does not
+ * have a buffer or a crop set, then a non-empty source crop must be
+ * specified.
+ * @param frameScale The desired scale of the returned buffer; the raw
+ * screen will be scaled up/down.
+ * @param format The desired pixel format of the returned buffer.
+ *
+ * @return Returns a HardwareBuffer that contains the layer capture.
+ * @hide
+ */
+ public static ScreenshotHardwareBuffer captureLayers(SurfaceControl layer, Rect sourceCrop,
+ float frameScale, int format) {
+ LayerCaptureArgs captureArgs = new LayerCaptureArgs.Builder(layer)
+ .setSourceCrop(sourceCrop)
+ .setFrameScale(frameScale)
+ .setPixelFormat(format)
+ .build();
+
+ return captureLayers(captureArgs);
+ }
+
+ /**
+ * @hide
+ */
+ public static ScreenshotHardwareBuffer captureLayers(LayerCaptureArgs captureArgs) {
+ SyncScreenCaptureListener screenCaptureListener = new SyncScreenCaptureListener();
+
+ int status = captureLayers(captureArgs, screenCaptureListener);
+ if (status != 0) {
+ return null;
+ }
+
+ return screenCaptureListener.waitForScreenshot();
+ }
+
+ /**
+ * Like {@link #captureLayers(SurfaceControl, Rect, float, int)} but with an array of layer
+ * handles to exclude.
+ * @hide
+ */
+ public static ScreenshotHardwareBuffer captureLayersExcluding(SurfaceControl layer,
+ Rect sourceCrop, float frameScale, int format, SurfaceControl[] exclude) {
+ LayerCaptureArgs captureArgs = new LayerCaptureArgs.Builder(layer)
+ .setSourceCrop(sourceCrop)
+ .setFrameScale(frameScale)
+ .setPixelFormat(format)
+ .setExcludeLayers(exclude)
+ .build();
+
+ return captureLayers(captureArgs);
+ }
+
+ /**
+ * @param captureArgs Arguments about how to take the screenshot
+ * @param captureListener A listener to receive the screenshot callback
+ * @hide
+ */
+ public static int captureLayers(@NonNull LayerCaptureArgs captureArgs,
+ @NonNull ScreenCaptureListener captureListener) {
+ return nativeCaptureLayers(captureArgs, captureListener);
+ }
+
+ /**
+ * Returns whether protected content is supported in GPU composition.
+ * @hide
+ */
+ public static boolean getProtectedContentSupport() {
+ return nativeGetProtectedContentSupport();
+ }
+
+ /**
+ * Returns whether brightness operations are supported on a display.
+ *
+ * @param displayToken
+ * The token for the display.
+ *
+ * @return Whether brightness operations are supported on the display.
+ *
+ * @hide
+ */
+ public static boolean getDisplayBrightnessSupport(IBinder displayToken) {
+ return nativeGetDisplayBrightnessSupport(displayToken);
+ }
+
+ /**
+ * Sets the brightness of a display.
+ *
+ * @param displayToken
+ * The token for the display whose brightness is set.
+ * @param brightness
+ * A number between 0.0f (minimum brightness) and 1.0f (maximum brightness), or -1.0f to
+ * turn the backlight off.
+ *
+ * @return Whether the method succeeded or not.
+ *
+ * @throws IllegalArgumentException if:
+ * - displayToken is null;
+ * - brightness is NaN or greater than 1.0f.
+ *
+ * @hide
+ */
+ public static boolean setDisplayBrightness(IBinder displayToken, float brightness) {
+ return setDisplayBrightness(displayToken, brightness, -1, brightness, -1);
+ }
+
+ /**
+ * Sets the brightness of a display.
+ *
+ * @param displayToken
+ * The token for the display whose brightness is set.
+ * @param sdrBrightness
+ * A number between 0.0f (minimum brightness) and 1.0f (maximum brightness), or -1.0f to
+ * turn the backlight off. Specifies the desired brightness of SDR content.
+ * @param sdrBrightnessNits
+ * The value of sdrBrightness converted to calibrated nits. -1 if this isn't available.
+ * @param displayBrightness
+ * A number between 0.0f (minimum brightness) and 1.0f (maximum brightness), or
+ * -1.0f to turn the backlight off. Specifies the desired brightness of the display itself,
+ * used directly for HDR content.
+ * @param displayBrightnessNits
+ * The value of displayBrightness converted to calibrated nits. -1 if this isn't
+ * available.
+ *
+ * @return Whether the method succeeded or not.
+ *
+ * @throws IllegalArgumentException if:
+ * - displayToken is null;
+ * - brightness is NaN or greater than 1.0f.
+ *
+ * @hide
+ */
+ public static boolean setDisplayBrightness(IBinder displayToken, float sdrBrightness,
+ float sdrBrightnessNits, float displayBrightness, float displayBrightnessNits) {
+ Objects.requireNonNull(displayToken);
+ if (Float.isNaN(displayBrightness) || displayBrightness > 1.0f
+ || (displayBrightness < 0.0f && displayBrightness != -1.0f)) {
+ throw new IllegalArgumentException("displayBrightness must be a number between 0.0f "
+ + " and 1.0f, or -1 to turn the backlight off: " + displayBrightness);
+ }
+ if (Float.isNaN(sdrBrightness) || sdrBrightness > 1.0f
+ || (sdrBrightness < 0.0f && sdrBrightness != -1.0f)) {
+ throw new IllegalArgumentException("sdrBrightness must be a number between 0.0f "
+ + "and 1.0f, or -1 to turn the backlight off: " + sdrBrightness);
+ }
+ return nativeSetDisplayBrightness(displayToken, sdrBrightness, sdrBrightnessNits,
+ displayBrightness, displayBrightnessNits);
+ }
+
+ /**
+ * Creates a mirrored hierarchy for the mirrorOf {@link SurfaceControl}
+ *
+ * Real Hierarchy Mirror
+ * SC (value that's returned)
+ * |
+ * A A'
+ * | |
+ * B B'
+ *
+ * @param mirrorOf The root of the hierarchy that should be mirrored.
+ * @return A SurfaceControl that's the parent of the root of the mirrored hierarchy.
+ *
+ * @hide
+ */
+ public static SurfaceControl mirrorSurface(SurfaceControl mirrorOf) {
+ long nativeObj = nativeMirrorSurface(mirrorOf.mNativeObject);
+ SurfaceControl sc = new SurfaceControl();
+ sc.assignNativeObject(nativeObj, "mirrorSurface");
+ return sc;
+ }
+
+ private static void validateColorArg(@Size(4) float[] color) {
+ final String msg = "Color must be specified as a float array with"
+ + " four values to represent r, g, b, a in range [0..1]";
+ if (color.length != 4) {
+ throw new IllegalArgumentException(msg);
+ }
+ for (float c:color) {
+ if ((c < 0.f) || (c > 1.f)) {
+ throw new IllegalArgumentException(msg);
+ }
+ }
+ }
+
+ /**
+ * Sets the global configuration for all the shadows drawn by SurfaceFlinger. Shadow follows
+ * material design guidelines.
+ *
+ * @param ambientColor Color applied to the ambient shadow. The alpha is premultiplied. A
+ * float array with four values to represent r, g, b, a in range [0..1]
+ * @param spotColor Color applied to the spot shadow. The alpha is premultiplied. The position
+ * of the spot shadow depends on the light position. A float array with
+ * four values to represent r, g, b, a in range [0..1]
+ * @param lightPosY Y axis position of the light used to cast the spot shadow in pixels.
+ * @param lightPosZ Z axis position of the light used to cast the spot shadow in pixels. The X
+ * axis position is set to the display width / 2.
+ * @param lightRadius Radius of the light casting the shadow in pixels.
+ *[
+ * @hide
+ */
+ public static void setGlobalShadowSettings(@Size(4) float[] ambientColor,
+ @Size(4) float[] spotColor, float lightPosY, float lightPosZ, float lightRadius) {
+ validateColorArg(ambientColor);
+ validateColorArg(spotColor);
+ nativeSetGlobalShadowSettings(ambientColor, spotColor, lightPosY, lightPosZ, lightRadius);
+ }
+
+ /**
+ * Adds a callback to be informed about SF's jank classification for a specific surface.
+ * @hide
+ */
+ public static void addJankDataListener(OnJankDataListener listener, SurfaceControl surface) {
+ nativeAddJankDataListener(listener.mNativePtr.get(), surface.mNativeObject);
+ }
+
+ /**
+ * Removes a jank callback previously added with {@link #addJankDataListener}
+ * @hide
+ */
+ public static void removeJankDataListener(OnJankDataListener listener) {
+ nativeRemoveJankDataListener(listener.mNativePtr.get());
+ }
+
+ /**
+ * Return GPU Context priority that is set in SurfaceFlinger's Render Engine.
+ * @hide
+ */
+ public static int getGPUContextPriority() {
+ return nativeGetGPUContextPriority();
+ }
+
+ /**
+ * An atomic set of changes to a set of SurfaceControl.
+ */
+ public static class Transaction implements Closeable, Parcelable {
+ /**
+ * @hide
+ */
+ public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ Transaction.class.getClassLoader(),
+ nativeGetNativeTransactionFinalizer(), 512);
+ /**
+ * @hide
+ */
+ public long mNativeObject;
+
+ private final ArrayMap<SurfaceControl, Point> mResizedSurfaces = new ArrayMap<>();
+ private final ArrayMap<SurfaceControl, SurfaceControl> mReparentedSurfaces =
+ new ArrayMap<>();
+
+ Runnable mFreeNativeResources;
+ private static final float[] INVALID_COLOR = {-1, -1, -1};
+
+ /**
+ * @hide
+ */
+ protected void checkPreconditions(SurfaceControl sc) {
+ sc.checkNotReleased();
+ }
+
+ /**
+ * Open a new transaction object. The transaction may be filed with commands to
+ * manipulate {@link SurfaceControl} instances, and then applied atomically with
+ * {@link #apply}. Eventually the user should invoke {@link #close}, when the object
+ * is no longer required. Note however that re-using a transaction after a call to apply
+ * is allowed as a convenience.
+ */
+ public Transaction() {
+ mNativeObject = nativeCreateTransaction();
+ mFreeNativeResources
+ = sRegistry.registerNativeAllocation(this, mNativeObject);
+ }
+
+ private Transaction(Parcel in) {
+ readFromParcel(in);
+ }
+
+ /**
+ * Apply the transaction, clearing it's state, and making it usable
+ * as a new transaction.
+ */
+ public void apply() {
+ apply(false);
+ }
+
+ /**
+ * Clear the transaction object, without applying it.
+ *
+ * @hide
+ */
+ public void clear() {
+ mResizedSurfaces.clear();
+ mReparentedSurfaces.clear();
+ if (mNativeObject != 0) {
+ nativeClearTransaction(mNativeObject);
+ }
+ }
+
+ /**
+ * Release the native transaction object, without applying it.
+ */
+ @Override
+ public void close() {
+ mResizedSurfaces.clear();
+ mReparentedSurfaces.clear();
+ mFreeNativeResources.run();
+ mNativeObject = 0;
+ }
+
+ /**
+ * Jankier version of apply. Avoid use (b/28068298).
+ * @hide
+ */
+ public void apply(boolean sync) {
+ applyResizedSurfaces();
+ notifyReparentedSurfaces();
+ nativeApplyTransaction(mNativeObject, sync);
+ }
+
+ /**
+ * @hide
+ */
+ protected void applyResizedSurfaces() {
+ for (int i = mResizedSurfaces.size() - 1; i >= 0; i--) {
+ final Point size = mResizedSurfaces.valueAt(i);
+ final SurfaceControl surfaceControl = mResizedSurfaces.keyAt(i);
+ synchronized (surfaceControl.mLock) {
+ surfaceControl.resize(size.x, size.y);
+ }
+ }
+ mResizedSurfaces.clear();
+ }
+
+ /**
+ * @hide
+ */
+ protected void notifyReparentedSurfaces() {
+ final int reparentCount = mReparentedSurfaces.size();
+ for (int i = reparentCount - 1; i >= 0; i--) {
+ final SurfaceControl child = mReparentedSurfaces.keyAt(i);
+ synchronized (child.mLock) {
+ final int listenerCount = (child.mReparentListeners != null)
+ ? child.mReparentListeners.size() : 0;
+ for (int j = 0; j < listenerCount; j++) {
+ final OnReparentListener listener = child.mReparentListeners.get(j);
+ listener.onReparent(this, mReparentedSurfaces.valueAt(i));
+ }
+ mReparentedSurfaces.removeAt(i);
+ }
+ }
+ }
+
+ /**
+ * Toggle the visibility of a given Layer and it's sub-tree.
+ *
+ * @param sc The SurfaceControl for which to set the visibility
+ * @param visible The new visibility
+ * @return This transaction object.
+ */
+ @NonNull
+ public Transaction setVisibility(@NonNull SurfaceControl sc, boolean visible) {
+ checkPreconditions(sc);
+ if (visible) {
+ return show(sc);
+ } else {
+ return hide(sc);
+ }
+ }
+
+ /**
+ * This information is passed to SurfaceFlinger to decide which window should have a
+ * priority when deciding about the refresh rate of the display. All windows have the
+ * lowest priority by default.
+ * @hide
+ */
+ @NonNull
+ public Transaction setFrameRateSelectionPriority(@NonNull SurfaceControl sc, int priority) {
+ sc.checkNotReleased();
+ nativeSetFrameRateSelectionPriority(mNativeObject, sc.mNativeObject, priority);
+ return this;
+ }
+
+ /**
+ * Request that a given surface and it's sub-tree be shown.
+ *
+ * @param sc The surface to show.
+ * @return This transaction.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction show(SurfaceControl sc) {
+ checkPreconditions(sc);
+ nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_HIDDEN);
+ return this;
+ }
+
+ /**
+ * Request that a given surface and it's sub-tree be hidden.
+ *
+ * @param sc The surface to hidden.
+ * @return This transaction.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction hide(SurfaceControl sc) {
+ checkPreconditions(sc);
+ nativeSetFlags(mNativeObject, sc.mNativeObject, SURFACE_HIDDEN, SURFACE_HIDDEN);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction setPosition(SurfaceControl sc, float x, float y) {
+ checkPreconditions(sc);
+ nativeSetPosition(mNativeObject, sc.mNativeObject, x, y);
+ return this;
+ }
+
+ /**
+ * Set the default buffer size for the SurfaceControl, if there is a
+ * {@link Surface} associated with the control, then
+ * this will be the default size for buffers dequeued from it.
+ * @param sc The surface to set the buffer size for.
+ * @param w The default width
+ * @param h The default height
+ * @return This Transaction
+ */
+ @NonNull
+ public Transaction setBufferSize(@NonNull SurfaceControl sc,
+ @IntRange(from = 0) int w, @IntRange(from = 0) int h) {
+ checkPreconditions(sc);
+ mResizedSurfaces.put(sc, new Point(w, h));
+ nativeSetSize(mNativeObject, sc.mNativeObject, w, h);
+ return this;
+ }
+
+ /**
+ * Provide the graphic producer a transform hint if the layer and its children are
+ * in an orientation different from the display's orientation. The caller is responsible
+ * for clearing this transform hint if the layer is no longer in a fixed orientation.
+ *
+ * The transform hint is used to prevent allocating a buffer of different size when a
+ * layer is rotated. The producer can choose to consume the hint and allocate the buffer
+ * with the same size.
+ *
+ * @return This Transaction.
+ * @hide
+ */
+ @NonNull
+ public Transaction setFixedTransformHint(@NonNull SurfaceControl sc,
+ @Surface.Rotation int transformHint) {
+ checkPreconditions(sc);
+ nativeSetFixedTransformHint(mNativeObject, sc.mNativeObject, transformHint);
+ return this;
+ }
+
+ /**
+ * Clearing any transform hint if set on this layer.
+ *
+ * @return This Transaction.
+ * @hide
+ */
+ @NonNull
+ public Transaction unsetFixedTransformHint(@NonNull SurfaceControl sc) {
+ checkPreconditions(sc);
+ nativeSetFixedTransformHint(mNativeObject, sc.mNativeObject, -1/* INVALID_ROTATION */);
+ return this;
+ }
+
+ /**
+ * Set the Z-order for a given SurfaceControl, relative to it's siblings.
+ * If two siblings share the same Z order the ordering is undefined. Surfaces
+ * with a negative Z will be placed below the parent surface.
+ *
+ * @param sc The SurfaceControl to set the Z order on
+ * @param z The Z-order
+ * @return This Transaction.
+ */
+ @NonNull
+ public Transaction setLayer(@NonNull SurfaceControl sc,
+ @IntRange(from = Integer.MIN_VALUE, to = Integer.MAX_VALUE) int z) {
+ checkPreconditions(sc);
+ nativeSetLayer(mNativeObject, sc.mNativeObject, z);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setRelativeLayer(SurfaceControl sc, SurfaceControl relativeTo, int z) {
+ checkPreconditions(sc);
+ nativeSetRelativeLayer(mNativeObject, sc.mNativeObject, relativeTo.mNativeObject, z);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setTransparentRegionHint(SurfaceControl sc, Region transparentRegion) {
+ checkPreconditions(sc);
+ nativeSetTransparentRegionHint(mNativeObject,
+ sc.mNativeObject, transparentRegion);
+ return this;
+ }
+
+ /**
+ * Set the alpha for a given surface. If the alpha is non-zero the SurfaceControl
+ * will be blended with the Surfaces under it according to the specified ratio.
+ *
+ * @param sc The given SurfaceControl.
+ * @param alpha The alpha to set.
+ */
+ @NonNull
+ public Transaction setAlpha(@NonNull SurfaceControl sc,
+ @FloatRange(from = 0.0, to = 1.0) float alpha) {
+ checkPreconditions(sc);
+ nativeSetAlpha(mNativeObject, sc.mNativeObject, alpha);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setInputWindowInfo(SurfaceControl sc, InputWindowHandle handle) {
+ checkPreconditions(sc);
+ nativeSetInputWindowInfo(mNativeObject, sc.mNativeObject, handle);
+ return this;
+ }
+
+ /**
+ * Waits until any changes to input windows have been sent from SurfaceFlinger to
+ * InputFlinger before returning.
+ *
+ * @hide
+ */
+ public Transaction syncInputWindows() {
+ nativeSyncInputWindows(mNativeObject);
+ return this;
+ }
+
+ /**
+ * Specify how the buffer associated with this Surface is mapped in to the
+ * parent coordinate space. The source frame will be scaled to fit the destination
+ * frame, after being rotated according to the orientation parameter.
+ *
+ * @param sc The SurfaceControl to specify the geometry of
+ * @param sourceCrop The source rectangle in buffer space. Or null for the entire buffer.
+ * @param destFrame The destination rectangle in parent space. Or null for the source frame.
+ * @param orientation The buffer rotation
+ * @return This transaction object.
+ */
+ @NonNull
+ public Transaction setGeometry(@NonNull SurfaceControl sc, @Nullable Rect sourceCrop,
+ @Nullable Rect destFrame, @Surface.Rotation int orientation) {
+ checkPreconditions(sc);
+ nativeSetGeometry(mNativeObject, sc.mNativeObject, sourceCrop, destFrame, orientation);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction setMatrix(SurfaceControl sc,
+ float dsdx, float dtdx, float dtdy, float dsdy) {
+ checkPreconditions(sc);
+ nativeSetMatrix(mNativeObject, sc.mNativeObject,
+ dsdx, dtdx, dtdy, dsdy);
+ return this;
+ }
+
+ /**
+ * Sets the transform and position of a {@link SurfaceControl} from a 3x3 transformation
+ * matrix.
+ *
+ * @param sc SurfaceControl to set matrix of
+ * @param matrix The matrix to apply.
+ * @param float9 An array of 9 floats to be used to extract the values from the matrix.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction setMatrix(SurfaceControl sc, Matrix matrix, float[] float9) {
+ matrix.getValues(float9);
+ setMatrix(sc, float9[MSCALE_X], float9[MSKEW_Y],
+ float9[MSKEW_X], float9[MSCALE_Y]);
+ setPosition(sc, float9[MTRANS_X], float9[MTRANS_Y]);
+ return this;
+ }
+
+ /**
+ * Sets the color transform for the Surface.
+ *
+ * @param sc SurfaceControl to set color transform of
+ * @param matrix A float array with 9 values represents a 3x3 transform matrix
+ * @param translation A float array with 3 values represents a translation vector
+ * @hide
+ */
+ public Transaction setColorTransform(SurfaceControl sc, @Size(9) float[] matrix,
+ @Size(3) float[] translation) {
+ checkPreconditions(sc);
+ nativeSetColorTransform(mNativeObject, sc.mNativeObject, matrix, translation);
+ return this;
+ }
+
+ /**
+ * Sets the Surface to be color space agnostic. If a surface is color space agnostic,
+ * the color can be interpreted in any color space.
+ * @param agnostic A boolean to indicate whether the surface is color space agnostic
+ * @hide
+ */
+ public Transaction setColorSpaceAgnostic(SurfaceControl sc, boolean agnostic) {
+ checkPreconditions(sc);
+ nativeSetColorSpaceAgnostic(mNativeObject, sc.mNativeObject, agnostic);
+ return this;
+ }
+
+ /**
+ * Bounds the surface and its children to the bounds specified. Size of the surface will be
+ * ignored and only the crop and buffer size will be used to determine the bounds of the
+ * surface. If no crop is specified and the surface has no buffer, the surface bounds is
+ * only constrained by the size of its parent bounds.
+ *
+ * @param sc SurfaceControl to set crop of.
+ * @param crop Bounds of the crop to apply.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction setWindowCrop(SurfaceControl sc, Rect crop) {
+ checkPreconditions(sc);
+ if (crop != null) {
+ nativeSetWindowCrop(mNativeObject, sc.mNativeObject,
+ crop.left, crop.top, crop.right, crop.bottom);
+ } else {
+ nativeSetWindowCrop(mNativeObject, sc.mNativeObject, 0, 0, 0, 0);
+ }
+
+ return this;
+ }
+
+ /**
+ * Same as {@link Transaction#setWindowCrop(SurfaceControl, Rect)} but sets the crop rect
+ * top left at 0, 0.
+ *
+ * @param sc SurfaceControl to set crop of.
+ * @param width width of crop rect
+ * @param height height of crop rect
+ * @hide
+ */
+ public Transaction setWindowCrop(SurfaceControl sc, int width, int height) {
+ checkPreconditions(sc);
+ nativeSetWindowCrop(mNativeObject, sc.mNativeObject, 0, 0, width, height);
+ return this;
+ }
+
+ /**
+ * Sets the corner radius of a {@link SurfaceControl}.
+ * @param sc SurfaceControl
+ * @param cornerRadius Corner radius in pixels.
+ * @return Itself.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public Transaction setCornerRadius(SurfaceControl sc, float cornerRadius) {
+ checkPreconditions(sc);
+ nativeSetCornerRadius(mNativeObject, sc.mNativeObject, cornerRadius);
+
+ return this;
+ }
+
+ /**
+ * Sets the background blur radius of the {@link SurfaceControl}.
+ *
+ * @param sc SurfaceControl.
+ * @param radius Blur radius in pixels.
+ * @return itself.
+ * @hide
+ */
+ public Transaction setBackgroundBlurRadius(SurfaceControl sc, int radius) {
+ checkPreconditions(sc);
+ nativeSetBackgroundBlurRadius(mNativeObject, sc.mNativeObject, radius);
+ return this;
+ }
+
+ /**
+ * Specify what regions should be blurred on the {@link SurfaceControl}.
+ *
+ * @param sc SurfaceControl.
+ * @param regions List of regions that will have blurs.
+ * @return itself.
+ * @see BlurRegion#toFloatArray()
+ * @hide
+ */
+ public Transaction setBlurRegions(SurfaceControl sc, float[][] regions) {
+ checkPreconditions(sc);
+ nativeSetBlurRegions(mNativeObject, sc.mNativeObject, regions, regions.length);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setStretchEffect(SurfaceControl sc, float width, float height,
+ float vecX, float vecY, float maxStretchAmountX,
+ float maxStretchAmountY, float childRelativeLeft, float childRelativeTop, float childRelativeRight,
+ float childRelativeBottom) {
+ checkPreconditions(sc);
+ nativeSetStretchEffect(mNativeObject, sc.mNativeObject, width, height,
+ vecX, vecY, maxStretchAmountX, maxStretchAmountY, childRelativeLeft, childRelativeTop,
+ childRelativeRight, childRelativeBottom);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.O)
+ public Transaction setLayerStack(SurfaceControl sc, int layerStack) {
+ checkPreconditions(sc);
+ nativeSetLayerStack(mNativeObject, sc.mNativeObject, layerStack);
+ return this;
+ }
+
+ /**
+ * Re-parents a given layer to a new parent. Children inherit transform (position, scaling)
+ * crop, visibility, and Z-ordering from their parents, as if the children were pixels within the
+ * parent Surface.
+ *
+ * @param sc The SurfaceControl to reparent
+ * @param newParent The new parent for the given control.
+ * @return This Transaction
+ */
+ @NonNull
+ public Transaction reparent(@NonNull SurfaceControl sc,
+ @Nullable SurfaceControl newParent) {
+ checkPreconditions(sc);
+ long otherObject = 0;
+ if (newParent != null) {
+ newParent.checkNotReleased();
+ otherObject = newParent.mNativeObject;
+ }
+ nativeReparent(mNativeObject, sc.mNativeObject, otherObject);
+ mReparentedSurfaces.put(sc, newParent);
+ return this;
+ }
+
+ /**
+ * Fills the surface with the specified color.
+ * @param color A float array with three values to represent r, g, b in range [0..1]. An
+ * invalid color will remove the color fill.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Transaction setColor(SurfaceControl sc, @Size(3) float[] color) {
+ checkPreconditions(sc);
+ nativeSetColor(mNativeObject, sc.mNativeObject, color);
+ return this;
+ }
+
+ /**
+ * Removes color fill.
+ * @hide
+ */
+ public Transaction unsetColor(SurfaceControl sc) {
+ checkPreconditions(sc);
+ nativeSetColor(mNativeObject, sc.mNativeObject, INVALID_COLOR);
+ return this;
+ }
+
+ /**
+ * Sets the security of the surface. Setting the flag is equivalent to creating the
+ * Surface with the {@link #SECURE} flag.
+ * @hide
+ */
+ public Transaction setSecure(SurfaceControl sc, boolean isSecure) {
+ checkPreconditions(sc);
+ if (isSecure) {
+ nativeSetFlags(mNativeObject, sc.mNativeObject, SECURE, SECURE);
+ } else {
+ nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SECURE);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the opacity of the surface. Setting the flag is equivalent to creating the
+ * Surface with the {@link #OPAQUE} flag.
+ * @hide
+ */
+ public Transaction setOpaque(SurfaceControl sc, boolean isOpaque) {
+ checkPreconditions(sc);
+ if (isOpaque) {
+ nativeSetFlags(mNativeObject, sc.mNativeObject, SURFACE_OPAQUE, SURFACE_OPAQUE);
+ } else {
+ nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_OPAQUE);
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setDisplaySurface(IBinder displayToken, Surface surface) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+
+ if (surface != null) {
+ synchronized (surface.mLock) {
+ nativeSetDisplaySurface(mNativeObject, displayToken, surface.mNativeObject);
+ }
+ } else {
+ nativeSetDisplaySurface(mNativeObject, displayToken, 0);
+ }
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setDisplayLayerStack(IBinder displayToken, int layerStack) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ nativeSetDisplayLayerStack(mNativeObject, displayToken, layerStack);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setDisplayProjection(IBinder displayToken,
+ int orientation, Rect layerStackRect, Rect displayRect) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ if (layerStackRect == null) {
+ throw new IllegalArgumentException("layerStackRect must not be null");
+ }
+ if (displayRect == null) {
+ throw new IllegalArgumentException("displayRect must not be null");
+ }
+ nativeSetDisplayProjection(mNativeObject, displayToken, orientation,
+ layerStackRect.left, layerStackRect.top, layerStackRect.right, layerStackRect.bottom,
+ displayRect.left, displayRect.top, displayRect.right, displayRect.bottom);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Transaction setDisplaySize(IBinder displayToken, int width, int height) {
+ if (displayToken == null) {
+ throw new IllegalArgumentException("displayToken must not be null");
+ }
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("width and height must be positive");
+ }
+
+ nativeSetDisplaySize(mNativeObject, displayToken, width, height);
+ return this;
+ }
+
+ /** flag the transaction as an animation
+ * @hide
+ */
+ public Transaction setAnimationTransaction() {
+ nativeSetAnimationTransaction(mNativeObject);
+ return this;
+ }
+
+ /**
+ * Provides a hint to SurfaceFlinger to change its offset so that SurfaceFlinger wakes up
+ * earlier to compose surfaces. The caller should use this as a hint to SurfaceFlinger
+ * when the scene is complex enough to use GPU composition. The hint will remain active
+ * until until the client calls {@link Transaction#setEarlyWakeupEnd}.
+ *
+ * @hide
+ */
+ public Transaction setEarlyWakeupStart() {
+ nativeSetEarlyWakeupStart(mNativeObject);
+ return this;
+ }
+
+ /**
+ * Removes the early wake up hint set by {@link Transaction#setEarlyWakeupStart}.
+ *
+ * @hide
+ */
+ public Transaction setEarlyWakeupEnd() {
+ nativeSetEarlyWakeupEnd(mNativeObject);
+ return this;
+ }
+
+ /**
+ * Sets an arbitrary piece of metadata on the surface. This is a helper for int data.
+ * @hide
+ */
+ public Transaction setMetadata(SurfaceControl sc, int key, int data) {
+ Parcel parcel = Parcel.obtain();
+ parcel.writeInt(data);
+ try {
+ setMetadata(sc, key, parcel);
+ } finally {
+ parcel.recycle();
+ }
+ return this;
+ }
+
+ /**
+ * Sets an arbitrary piece of metadata on the surface.
+ * @hide
+ */
+ public Transaction setMetadata(SurfaceControl sc, int key, Parcel data) {
+ checkPreconditions(sc);
+ nativeSetMetadata(mNativeObject, sc.mNativeObject, key, data);
+ return this;
+ }
+
+ /**
+ * Draws shadows of length {@code shadowRadius} around the surface {@link SurfaceControl}.
+ * If the length is 0.0f then the shadows will not be drawn.
+ *
+ * Shadows are drawn around the screen bounds, these are the post transformed cropped
+ * bounds. They can draw over their parent bounds and will be occluded by layers with a
+ * higher z-order. The shadows will respect the surface's corner radius if the
+ * rounded corner bounds (transformed source bounds) are within the screen bounds.
+ *
+ * A shadow will only be drawn on buffer and color layers. If the radius is applied on a
+ * container layer, it will be passed down the hierarchy to be applied on buffer and color
+ * layers but not its children. A scenario where this is useful is when SystemUI animates
+ * a task by controlling a leash to it, can draw a shadow around the app surface by
+ * setting a shadow on the leash. This is similar to how rounded corners are set.
+ *
+ * @hide
+ */
+ public Transaction setShadowRadius(SurfaceControl sc, float shadowRadius) {
+ checkPreconditions(sc);
+ nativeSetShadowRadius(mNativeObject, sc.mNativeObject, shadowRadius);
+ return this;
+ }
+
+ /**
+ * Sets the intended frame rate for this surface. Any switching of refresh rates is
+ * most probably going to be seamless.
+ *
+ * @see #setFrameRate(SurfaceControl, float, int, int)
+ */
+ @NonNull
+ public Transaction setFrameRate(@NonNull SurfaceControl sc,
+ @FloatRange(from = 0.0) float frameRate,
+ @Surface.FrameRateCompatibility int compatibility) {
+ return setFrameRate(sc, frameRate, compatibility,
+ Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
+ }
+
+ /**
+ * Sets the intended frame rate for the surface {@link SurfaceControl}.
+ * <p>
+ * On devices that are capable of running the display at different refresh rates, the system
+ * may choose a display refresh rate to better match this surface's frame rate. Usage of
+ * this API won't directly affect the application's frame production pipeline. However,
+ * because the system may change the display refresh rate, calls to this function may result
+ * in changes to Choreographer callback timings, and changes to the time interval at which
+ * the system releases buffers back to the application.
+ * <p>
+ * Note that this only has an effect for surfaces presented on the display. If this
+ * surface is consumed by something other than the system compositor, e.g. a media
+ * codec, this call has no effect.
+ *
+ * @param sc The SurfaceControl to specify the frame rate of.
+ * @param frameRate The intended frame rate for this surface, in frames per second. 0 is a
+ * special value that indicates the app will accept the system's choice for
+ * the display frame rate, which is the default behavior if this function
+ * isn't called. The <code>frameRate</code> param does <em>not</em> need
+ * to be a valid refresh rate for this device's display - e.g., it's fine
+ * to pass 30fps to a device that can only run the display at 60fps.
+ * @param compatibility The frame rate compatibility of this surface. The compatibility
+ * value may influence the system's choice of display frame rate.
+ * This parameter is ignored when <code>frameRate</code> is 0.
+ * @param changeFrameRateStrategy Whether display refresh rate transitions caused by this
+ * surface should be seamless. A seamless transition is one
+ * that doesn't have any visual interruptions, such as a
+ * black screen for a second or two. This parameter is
+ * ignored when <code>frameRate</code> is 0.
+ * @return This transaction object.
+ */
+ @NonNull
+ public Transaction setFrameRate(@NonNull SurfaceControl sc,
+ @FloatRange(from = 0.0) float frameRate,
+ @Surface.FrameRateCompatibility int compatibility,
+ @Surface.ChangeFrameRateStrategy int changeFrameRateStrategy) {
+ checkPreconditions(sc);
+ nativeSetFrameRate(mNativeObject, sc.mNativeObject, frameRate, compatibility,
+ changeFrameRateStrategy);
+ return this;
+ }
+
+ /**
+ * Sets focus on the window identified by the input {@code token} if the window is focusable
+ * otherwise the request is dropped.
+ *
+ * If the window is not visible, the request will be queued until the window becomes
+ * visible or the request is overrriden by another request. The currently focused window
+ * will lose focus immediately. This is to send the newly focused window any focus
+ * dispatched events that occur while it is completing its first draw.
+ *
+ * @hide
+ */
+ public Transaction setFocusedWindow(@NonNull IBinder token, String windowName,
+ int displayId) {
+ nativeSetFocusedWindow(mNativeObject, token, windowName,
+ null /* focusedToken */, null /* focusedWindowName */, displayId);
+ return this;
+ }
+
+ /**
+ * Set focus on the window identified by the input {@code token} if the window identified by
+ * the input {@code focusedToken} is currently focused. If the {@code focusedToken} does not
+ * have focus, the request is dropped.
+ *
+ * This is used by forward focus transfer requests from clients that host embedded windows,
+ * and want to transfer focus to/from them.
+ *
+ * @hide
+ */
+ public Transaction requestFocusTransfer(@NonNull IBinder token,
+ String windowName,
+ @NonNull IBinder focusedToken,
+ String focusedWindowName,
+ int displayId) {
+ nativeSetFocusedWindow(mNativeObject, token, windowName, focusedToken,
+ focusedWindowName, displayId);
+ return this;
+ }
+
+ /**
+ * Adds or removes the flag SKIP_SCREENSHOT of the surface. Setting the flag is equivalent
+ * to creating the Surface with the {@link #SKIP_SCREENSHOT} flag.
+ *
+ * @hide
+ */
+ public Transaction setSkipScreenshot(SurfaceControl sc, boolean skipScrenshot) {
+ checkPreconditions(sc);
+ if (skipScrenshot) {
+ nativeSetFlags(mNativeObject, sc.mNativeObject, SKIP_SCREENSHOT, SKIP_SCREENSHOT);
+ } else {
+ nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SKIP_SCREENSHOT);
+ }
+ return this;
+ }
+
+ /**
+ * Set a buffer for a SurfaceControl. This can only be used for SurfaceControls that were
+ * created as type {@link #FX_SURFACE_BLAST}
+ *
+ * @hide
+ */
+ public Transaction setBuffer(SurfaceControl sc, GraphicBuffer buffer) {
+ checkPreconditions(sc);
+ nativeSetBuffer(mNativeObject, sc.mNativeObject, buffer);
+ return this;
+ }
+
+ /**
+ * Set the color space for the SurfaceControl. The supported color spaces are SRGB
+ * and Display P3, other color spaces will be treated as SRGB. This can only be used for
+ * SurfaceControls that were created as type {@link #FX_SURFACE_BLAST}
+ *
+ * @hide
+ */
+ public Transaction setColorSpace(SurfaceControl sc, ColorSpace colorSpace) {
+ checkPreconditions(sc);
+ nativeSetColorSpace(mNativeObject, sc.mNativeObject, colorSpace.getId());
+ return this;
+ }
+
+ /**
+ * Sets the trusted overlay state on this SurfaceControl and it is inherited to all the
+ * children. The caller must hold the ACCESS_SURFACE_FLINGER permission.
+ * @hide
+ */
+ public Transaction setTrustedOverlay(SurfaceControl sc, boolean isTrustedOverlay) {
+ checkPreconditions(sc);
+ nativeSetTrustedOverlay(mNativeObject, sc.mNativeObject, isTrustedOverlay);
+ return this;
+ }
+
+ /**
+ * Merge the other transaction into this transaction, clearing the
+ * other transaction as if it had been applied.
+ *
+ * @param other The transaction to merge in to this one.
+ * @return This transaction.
+ */
+ @NonNull
+ public Transaction merge(@NonNull Transaction other) {
+ if (this == other) {
+ return this;
+ }
+ mResizedSurfaces.putAll(other.mResizedSurfaces);
+ other.mResizedSurfaces.clear();
+ mReparentedSurfaces.putAll(other.mReparentedSurfaces);
+ other.mReparentedSurfaces.clear();
+ nativeMergeTransaction(mNativeObject, other.mNativeObject);
+ return this;
+ }
+
+ /**
+ * Equivalent to reparent with a null parent, in that it removes
+ * the SurfaceControl from the scene, but it also releases
+ * the local resources (by calling {@link SurfaceControl#release})
+ * after this method returns, {@link SurfaceControl#isValid} will return
+ * false for the argument.
+ *
+ * @param sc The surface to remove and release.
+ * @return This transaction
+ * @hide
+ */
+ @NonNull
+ public Transaction remove(@NonNull SurfaceControl sc) {
+ reparent(sc, null);
+ sc.release();
+ return this;
+ }
+
+ /**
+ * Sets the frame timeline vsync id received from choreographer
+ * {@link Choreographer#getVsyncId()} that corresponds to the transaction submitted on that
+ * surface control.
+ *
+ * @hide
+ */
+ @NonNull
+ public Transaction setFrameTimelineVsync(long frameTimelineVsyncId) {
+ nativeSetFrameTimelineVsync(mNativeObject, frameTimelineVsyncId);
+ return this;
+ }
+
+ /**
+ * Writes the transaction to parcel, clearing the transaction as if it had been applied so
+ * it can be used to store future transactions. It's the responsibility of the parcel
+ * reader to apply the original transaction.
+ *
+ * @param dest parcel to write the transaction to
+ * @param flags
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags) {
+ if (mNativeObject == 0) {
+ dest.writeInt(0);
+ return;
+ }
+
+ dest.writeInt(1);
+ nativeWriteTransactionToParcel(mNativeObject, dest);
+ if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+ nativeClearTransaction(mNativeObject);
+ }
+ }
+
+ private void readFromParcel(Parcel in) {
+ mNativeObject = 0;
+ if (in.readInt() != 0) {
+ mNativeObject = nativeReadTransactionFromParcel(in);
+ mFreeNativeResources = sRegistry.registerNativeAllocation(this, mNativeObject);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<Transaction> CREATOR = new Creator<Transaction>() {
+ @Override
+ public Transaction createFromParcel(Parcel in) {
+ return new Transaction(in);
+ }
+ @Override
+ public Transaction[] newArray(int size) {
+ return new Transaction[size];
+ }
+ };
+ }
+
+ /**
+ * A debugging utility subclass of SurfaceControl.Transaction. At construction
+ * you can pass in a monitor object, and all the other methods will throw an exception
+ * if the monitor is not held when they are called.
+ * @hide
+ */
+ public static class LockDebuggingTransaction extends SurfaceControl.Transaction {
+ Object mMonitor;
+
+ public LockDebuggingTransaction(Object o) {
+ mMonitor = o;
+ }
+
+ @Override
+ protected void checkPreconditions(SurfaceControl sc) {
+ super.checkPreconditions(sc);
+ if (!Thread.holdsLock(mMonitor)) {
+ throw new RuntimeException(
+ "Unlocked access to synchronized SurfaceControl.Transaction");
+ }
+ }
+ }
+
+ /**
+ * As part of eliminating usage of the global Transaction we expose
+ * a SurfaceControl.getGlobalTransaction function. However calling
+ * apply on this global transaction (rather than using closeTransaction)
+ * would be very dangerous. So for the global transaction we use this
+ * subclass of Transaction where the normal apply throws an exception.
+ */
+ private static class GlobalTransactionWrapper extends SurfaceControl.Transaction {
+ void applyGlobalTransaction(boolean sync) {
+ applyResizedSurfaces();
+ notifyReparentedSurfaces();
+ nativeApplyTransaction(mNativeObject, sync);
+ }
+
+ @Override
+ public void apply(boolean sync) {
+ throw new RuntimeException("Global transaction must be applied from closeTransaction");
+ }
+ }
+
+ /**
+ * Acquire a frame rate flexibility token, which allows surface flinger to freely switch display
+ * frame rates. This is used by CTS tests to put the device in a consistent state. See
+ * ISurfaceComposer::acquireFrameRateFlexibilityToken(). The caller must have the
+ * ACCESS_SURFACE_FLINGER permission, or else the call will fail, returning 0.
+ * @hide
+ */
+ @TestApi
+ public static long acquireFrameRateFlexibilityToken() {
+ return nativeAcquireFrameRateFlexibilityToken();
+ }
+
+ /**
+ * Release a frame rate flexibility token.
+ * @hide
+ */
+ @TestApi
+ public static void releaseFrameRateFlexibilityToken(long token) {
+ nativeReleaseFrameRateFlexibilityToken(token);
+ }
+
+ /**
+ * This is a refactoring utility function to enable lower levels of code to be refactored
+ * from using the global transaction (and instead use a passed in Transaction) without
+ * having to refactor the higher levels at the same time.
+ * The returned global transaction can't be applied, it must be applied from closeTransaction
+ * Unless you are working on removing Global Transaction usage in the WindowManager, this
+ * probably isn't a good function to use.
+ * @hide
+ */
+ public static Transaction getGlobalTransaction() {
+ return sGlobalTransaction;
+ }
+
+ /**
+ * @hide
+ */
+ public void resize(int w, int h) {
+ mWidth = w;
+ mHeight = h;
+ nativeUpdateDefaultBufferSize(mNativeObject, w, h);
+ }
+
+ /**
+ * @hide
+ */
+ public int getTransformHint() {
+ checkNotReleased();
+ return nativeGetTransformHint(mNativeObject);
+ }
+
+ /**
+ * Update the transform hint of current SurfaceControl. Only affect if type is
+ * {@link #FX_SURFACE_BLAST}
+ *
+ * The transform hint is used to prevent allocating a buffer of different size when a
+ * layer is rotated. The producer can choose to consume the hint and allocate the buffer
+ * with the same size.
+ * @hide
+ */
+ public void setTransformHint(@Surface.Rotation int transformHint) {
+ nativeSetTransformHint(mNativeObject, transformHint);
+ }
+}
diff --git a/android/view/SurfaceControlFpsListener.java b/android/view/SurfaceControlFpsListener.java
new file mode 100644
index 0000000..20a511a
--- /dev/null
+++ b/android/view/SurfaceControlFpsListener.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+
+/**
+ * Listener for sampling the frames per second for a SurfaceControl and its children.
+ * This should only be used by a system component that needs to listen to a SurfaceControl's
+ * tree's FPS when it is not actively submitting transactions for that SurfaceControl.
+ * Otherwise, ASurfaceTransaction_OnComplete callbacks should be used.
+ *
+ * @hide
+ */
+public abstract class SurfaceControlFpsListener {
+ private long mNativeListener;
+
+ public SurfaceControlFpsListener() {
+ mNativeListener = nativeCreate(this);
+ }
+
+ protected void destroy() {
+ if (mNativeListener == 0) {
+ return;
+ }
+ unregister();
+ nativeDestroy(mNativeListener);
+ mNativeListener = 0;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Reports the fps from the registered SurfaceControl
+ */
+ public abstract void onFpsReported(float fps);
+
+ /**
+ * Registers the sampling listener for a particular task ID
+ */
+ public void register(int taskId) {
+ if (mNativeListener == 0) {
+ return;
+ }
+
+ nativeRegister(mNativeListener, taskId);
+ }
+
+ /**
+ * Unregisters the sampling listener.
+ */
+ public void unregister() {
+ if (mNativeListener == 0) {
+ return;
+ }
+ nativeUnregister(mNativeListener);
+ }
+
+ /**
+ * Dispatch the collected sample.
+ *
+ * Called from native code on a binder thread.
+ */
+ private static void dispatchOnFpsReported(
+ @NonNull SurfaceControlFpsListener listener, float fps) {
+ listener.onFpsReported(fps);
+ }
+
+ private static native long nativeCreate(SurfaceControlFpsListener thiz);
+ private static native void nativeDestroy(long ptr);
+ private static native void nativeRegister(long ptr, int taskId);
+ private static native void nativeUnregister(long ptr);
+}
diff --git a/android/view/SurfaceControlHdrLayerInfoListener.java b/android/view/SurfaceControlHdrLayerInfoListener.java
new file mode 100644
index 0000000..13d68d0
--- /dev/null
+++ b/android/view/SurfaceControlHdrLayerInfoListener.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.RequiresPermission;
+import android.os.IBinder;
+import android.util.ArrayMap;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.util.Objects;
+
+/**
+ * Allows for the monitoring of layers with HDR content
+ *
+ * @hide */
+public abstract class SurfaceControlHdrLayerInfoListener {
+ private static final NativeAllocationRegistry sRegistry =
+ NativeAllocationRegistry.createMalloced(
+ SurfaceControlHdrLayerInfoListener.class.getClassLoader(), nGetDestructor());
+
+ /**
+ * Callback when the HDR information about the given display has changed
+ *
+ * @param displayToken The display this callback is about
+ * @param numberOfHdrLayers How many HDR layers are visible on the display
+ * @param maxW The width of the HDR layer with the largest area
+ * @param maxH The height of the HDR layer with the largest area
+ * @param flags Additional metadata flags, currently always 0
+ * TODO(b/182312559): Add some flags
+ *
+ * @hide */
+ public abstract void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers,
+ int maxW, int maxH, int flags);
+
+ /**
+ * Registers this as an HDR info listener on the provided display
+ * @param displayToken
+ */
+ @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+ public void register(IBinder displayToken) {
+ Objects.requireNonNull(displayToken);
+ synchronized (this) {
+ if (mRegisteredListeners.containsKey(displayToken)) {
+ return;
+ }
+ long nativePtr = nRegister(displayToken);
+ Runnable destructor = sRegistry.registerNativeAllocation(this, nativePtr);
+ mRegisteredListeners.put(displayToken, destructor);
+ }
+ }
+
+ /**
+ * Unregisters this as an HDR info listener on the provided display
+ * @param displayToken
+ */
+ @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+ public void unregister(IBinder displayToken) {
+ Objects.requireNonNull(displayToken);
+ final Runnable destructor;
+ synchronized (this) {
+ destructor = mRegisteredListeners.remove(displayToken);
+ }
+ if (destructor != null) {
+ destructor.run();
+ }
+ }
+
+ /**
+ * Unregisters this on all previously registered displays
+ */
+ @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+ public void unregisterAll() {
+ final ArrayMap<IBinder, Runnable> toDestroy;
+ synchronized (this) {
+ toDestroy = mRegisteredListeners;
+ mRegisteredListeners = new ArrayMap<>();
+ }
+ for (Runnable destructor : toDestroy.values()) {
+ destructor.run();
+ }
+ }
+
+ private ArrayMap<IBinder, Runnable> mRegisteredListeners = new ArrayMap<>();
+
+ private static native long nGetDestructor();
+ private native long nRegister(IBinder displayToken);
+}
diff --git a/android/view/SurfaceControlViewHost.java b/android/view/SurfaceControlViewHost.java
new file mode 100644
index 0000000..11b161a
--- /dev/null
+++ b/android/view/SurfaceControlViewHost.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.accessibility.IAccessibilityEmbeddedConnection;
+
+import java.util.Objects;
+
+/**
+ * Utility class for adding a View hierarchy to a {@link SurfaceControl}. The View hierarchy
+ * will render in to a root SurfaceControl, and receive input based on the SurfaceControl's
+ * placement on-screen. The primary usage of this class is to embed a View hierarchy from
+ * one process in to another. After the SurfaceControlViewHost has been set up in the embedded
+ * content provider, we can send the {@link SurfaceControlViewHost.SurfacePackage}
+ * to the host process. The host process can then attach the hierarchy to a SurfaceView within
+ * its own by calling
+ * {@link SurfaceView#setChildSurfacePackage}.
+ */
+public class SurfaceControlViewHost {
+ private final ViewRootImpl mViewRoot;
+ private WindowlessWindowManager mWm;
+
+ private SurfaceControl mSurfaceControl;
+ private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
+
+ /**
+ * Package encapsulating a Surface hierarchy which contains interactive view
+ * elements. It's expected to get this object from
+ * {@link SurfaceControlViewHost#getSurfacePackage} afterwards it can be embedded within
+ * a SurfaceView by calling {@link SurfaceView#setChildSurfacePackage}.
+ *
+ * Note that each {@link SurfacePackage} must be released by calling
+ * {@link SurfacePackage#release}. However, if you use the recommended flow,
+ * the framework will automatically handle the lifetime for you.
+ *
+ * 1. When sending the package to the remote process, return it from an AIDL method
+ * or manually use FLAG_WRITE_RETURN_VALUE in writeToParcel. This will automatically
+ * release the package in the local process.
+ * 2. In the remote process, consume the package using SurfaceView. This way the
+ * SurfaceView will take over the lifetime and call {@link SurfacePackage#release}
+ * for the user.
+ *
+ * One final note: The {@link SurfacePackage} lifetime is totally de-coupled
+ * from the lifetime of the underlying {@link SurfaceControlViewHost}. Regardless
+ * of the lifetime of the package the user should still call
+ * {@link SurfaceControlViewHost#release} when finished.
+ */
+ public static final class SurfacePackage implements Parcelable {
+ private SurfaceControl mSurfaceControl;
+ private final IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
+ private final IBinder mInputToken;
+
+ SurfacePackage(SurfaceControl sc, IAccessibilityEmbeddedConnection connection,
+ IBinder inputToken) {
+ mSurfaceControl = sc;
+ mAccessibilityEmbeddedConnection = connection;
+ mInputToken = inputToken;
+ }
+
+ /**
+ * Constructs a copy of {@code SurfacePackage} with an independent lifetime.
+ *
+ * The caller can use this to create an independent copy in situations where ownership of
+ * the {@code SurfacePackage} would be transferred elsewhere, such as attaching to a
+ * {@code SurfaceView}, returning as {@code Binder} result value, etc. The caller is
+ * responsible for releasing this copy when its done.
+ *
+ * @param other {@code SurfacePackage} to create a copy of.
+ */
+ public SurfacePackage(@NonNull SurfacePackage other) {
+ SurfaceControl otherSurfaceControl = other.mSurfaceControl;
+ if (otherSurfaceControl != null && otherSurfaceControl.isValid()) {
+ mSurfaceControl = new SurfaceControl();
+ mSurfaceControl.copyFrom(otherSurfaceControl, "SurfacePackage");
+ }
+ mAccessibilityEmbeddedConnection = other.mAccessibilityEmbeddedConnection;
+ mInputToken = other.mInputToken;
+ }
+
+ private SurfacePackage(Parcel in) {
+ mSurfaceControl = new SurfaceControl();
+ mSurfaceControl.readFromParcel(in);
+ mAccessibilityEmbeddedConnection = IAccessibilityEmbeddedConnection.Stub.asInterface(
+ in.readStrongBinder());
+ mInputToken = in.readStrongBinder();
+ }
+
+ /**
+ * Use {@link SurfaceView#setChildSurfacePackage} or manually fix
+ * accessibility (see SurfaceView implementation).
+ * @hide
+ */
+ public @NonNull SurfaceControl getSurfaceControl() {
+ return mSurfaceControl;
+ }
+
+ /**
+ * Gets an accessibility embedded connection interface for this SurfaceControlViewHost.
+ *
+ * @return {@link IAccessibilityEmbeddedConnection} interface.
+ * @hide
+ */
+ public IAccessibilityEmbeddedConnection getAccessibilityEmbeddedConnection() {
+ return mAccessibilityEmbeddedConnection;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ mSurfaceControl.writeToParcel(out, flags);
+ out.writeStrongBinder(mAccessibilityEmbeddedConnection.asBinder());
+ out.writeStrongBinder(mInputToken);
+ }
+
+ /**
+ * Release the {@link SurfaceControl} associated with this package.
+ * It's not necessary to call this if you pass the package to
+ * {@link SurfaceView#setChildSurfacePackage} as {@link SurfaceView} will
+ * take ownership in that case.
+ */
+ public void release() {
+ if (mSurfaceControl != null) {
+ mSurfaceControl.release();
+ }
+ mSurfaceControl = null;
+ }
+
+ /**
+ * Returns an input token used which can be used to request focus on the embedded surface.
+ *
+ * @hide
+ */
+ public IBinder getInputToken() {
+ return mInputToken;
+ }
+
+ public static final @NonNull Creator<SurfacePackage> CREATOR
+ = new Creator<SurfacePackage>() {
+ public SurfacePackage createFromParcel(Parcel in) {
+ return new SurfacePackage(in);
+ }
+ public SurfacePackage[] newArray(int size) {
+ return new SurfacePackage[size];
+ }
+ };
+ }
+
+ /** @hide */
+ public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
+ @NonNull WindowlessWindowManager wwm) {
+ this(c, d, wwm, false /* useSfChoreographer */);
+ }
+
+ /** @hide */
+ public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
+ @NonNull WindowlessWindowManager wwm, boolean useSfChoreographer) {
+ mWm = wwm;
+ mViewRoot = new ViewRootImpl(c, d, mWm, useSfChoreographer);
+ mAccessibilityEmbeddedConnection = mViewRoot.getAccessibilityEmbeddedConnection();
+ }
+
+ /**
+ * Construct a new SurfaceControlViewHost. The root Surface will be
+ * allocated internally and is accessible via getSurfacePackage().
+ *
+ * The {@param hostToken} parameter, primarily used for ANR reporting,
+ * must be obtained from whomever will be hosting the embedded hierarchy.
+ * It's accessible from {@link SurfaceView#getHostToken}.
+ *
+ * @param context The Context object for your activity or application.
+ * @param display The Display the hierarchy will be placed on.
+ * @param hostToken The host token, as discussed above.
+ */
+ public SurfaceControlViewHost(@NonNull Context context, @NonNull Display display,
+ @Nullable IBinder hostToken) {
+ mSurfaceControl = new SurfaceControl.Builder()
+ .setContainerLayer()
+ .setName("SurfaceControlViewHost")
+ .setCallsite("SurfaceControlViewHost")
+ .build();
+ mWm = new WindowlessWindowManager(context.getResources().getConfiguration(),
+ mSurfaceControl, hostToken);
+ mViewRoot = new ViewRootImpl(context, display, mWm);
+ mAccessibilityEmbeddedConnection = mViewRoot.getAccessibilityEmbeddedConnection();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ // We aren't on the UI thread here so we need to pass false to
+ // doDie
+ mViewRoot.die(false /* immediate */);
+ }
+
+
+ /**
+ * Return a SurfacePackage for the root SurfaceControl of the embedded hierarchy.
+ * Rather than be directly reparented using {@link SurfaceControl.Transaction} this
+ * SurfacePackage should be passed to {@link SurfaceView#setChildSurfacePackage}
+ * which will not only reparent the Surface, but ensure the accessibility hierarchies
+ * are linked.
+ */
+ public @Nullable SurfacePackage getSurfacePackage() {
+ if (mSurfaceControl != null && mAccessibilityEmbeddedConnection != null) {
+ return new SurfacePackage(mSurfaceControl, mAccessibilityEmbeddedConnection,
+ mViewRoot.getInputToken());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the root view of the SurfaceControlViewHost. This view will render in to
+ * the SurfaceControl, and receive input based on the SurfaceControls positioning on
+ * screen. It will be laid as if it were in a window of the passed in width and height.
+ *
+ * @param view The View to add
+ * @param width The width to layout the View within, in pixels.
+ * @param height The height to layout the View within, in pixels.
+ */
+ public void setView(@NonNull View view, int width, int height) {
+ final WindowManager.LayoutParams lp =
+ new WindowManager.LayoutParams(width, height,
+ WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT);
+ setView(view, lp);
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public void setView(@NonNull View view, @NonNull WindowManager.LayoutParams attrs) {
+ Objects.requireNonNull(view);
+ attrs.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+ view.setLayoutParams(attrs);
+ mViewRoot.setView(view, attrs, null);
+ }
+
+ /**
+ * @return The view passed to setView, or null if none has been passed.
+ */
+ public @Nullable View getView() {
+ return mViewRoot.getView();
+ }
+
+ /**
+ * @return the ViewRootImpl wrapped by this host.
+ * @hide
+ */
+ public IWindow getWindowToken() {
+ return mViewRoot.mWindow;
+ }
+
+ /**
+ * @return the WindowlessWindowManager instance that this host is attached to.
+ * @hide
+ */
+ public @NonNull WindowlessWindowManager getWindowlessWM() {
+ return mWm;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public void relayout(WindowManager.LayoutParams attrs) {
+ mViewRoot.setLayoutParams(attrs, false);
+ mViewRoot.setReportNextDraw();
+ mWm.setCompletionCallback(mViewRoot.mWindow.asBinder(), (SurfaceControl.Transaction t) -> {
+ t.apply();
+ });
+ }
+
+ /**
+ * Modify the size of the root view.
+ *
+ * @param width Width in pixels
+ * @param height Height in pixels
+ */
+ public void relayout(int width, int height) {
+ final WindowManager.LayoutParams lp =
+ new WindowManager.LayoutParams(width, height,
+ WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT);
+ relayout(lp);
+ }
+
+ /**
+ * Trigger the tear down of the embedded view hierarchy and release the SurfaceControl.
+ * This will result in onDispatchedFromWindow being dispatched to the embedded view hierarchy
+ * and render the object unusable.
+ */
+ public void release() {
+ // ViewRoot will release mSurfaceControl for us.
+ mViewRoot.die(true /* immediate */);
+ }
+}
diff --git a/android/view/SurfaceControl_Delegate.java b/android/view/SurfaceControl_Delegate.java
new file mode 100644
index 0000000..24838b2
--- /dev/null
+++ b/android/view/SurfaceControl_Delegate.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+public class SurfaceControl_Delegate {
+
+ // ---- delegate manager ----
+ private static final DelegateManager<SurfaceControl_Delegate> sManager =
+ new DelegateManager<>(SurfaceControl_Delegate.class);
+ private static long sFinalizer = -1;
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeCreateTransaction() {
+ return sManager.addNewDelegate(new SurfaceControl_Delegate());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nativeGetNativeTransactionFinalizer() {
+ synchronized (SurfaceControl_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(sManager::removeJavaReferenceFor);
+ }
+ }
+ return sFinalizer;
+ }
+}
diff --git a/android/view/SurfaceHolder.java b/android/view/SurfaceHolder.java
new file mode 100644
index 0000000..c5d45c3
--- /dev/null
+++ b/android/view/SurfaceHolder.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+
+/**
+ * Abstract interface to someone holding a display surface. Allows you to
+ * control the surface size and format, edit the pixels in the surface, and
+ * monitor changes to the surface. This interface is typically available
+ * through the {@link SurfaceView} class.
+ *
+ * <p>When using this interface from a thread other than the one running
+ * its {@link SurfaceView}, you will want to carefully read the
+ * methods
+ * {@link #lockCanvas} and {@link Callback#surfaceCreated Callback.surfaceCreated()}.
+ */
+public interface SurfaceHolder {
+
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int SURFACE_TYPE_NORMAL = 0;
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int SURFACE_TYPE_HARDWARE = 1;
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int SURFACE_TYPE_GPU = 2;
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int SURFACE_TYPE_PUSH_BUFFERS = 3;
+
+ /**
+ * Exception that is thrown from {@link #lockCanvas} when called on a Surface
+ * whose type is SURFACE_TYPE_PUSH_BUFFERS.
+ */
+ public static class BadSurfaceTypeException extends RuntimeException {
+ public BadSurfaceTypeException() {
+ }
+
+ public BadSurfaceTypeException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * A client may implement this interface to receive information about
+ * changes to the surface. When used with a {@link SurfaceView}, the
+ * Surface being held is only available between calls to
+ * {@link #surfaceCreated(SurfaceHolder)} and
+ * {@link #surfaceDestroyed(SurfaceHolder)}. The Callback is set with
+ * {@link SurfaceHolder#addCallback SurfaceHolder.addCallback} method.
+ */
+ public interface Callback {
+ /**
+ * This is called immediately after the surface is first created.
+ * Implementations of this should start up whatever rendering code
+ * they desire. Note that only one thread can ever draw into
+ * a {@link Surface}, so you should not draw into the Surface here
+ * if your normal rendering will be in another thread.
+ *
+ * @param holder The SurfaceHolder whose surface is being created.
+ */
+ void surfaceCreated(@NonNull SurfaceHolder holder);
+
+ /**
+ * This is called immediately after any structural changes (format or
+ * size) have been made to the surface. You should at this point update
+ * the imagery in the surface. This method is always called at least
+ * once, after {@link #surfaceCreated}.
+ *
+ * @param holder The SurfaceHolder whose surface has changed.
+ * @param format The new {@link PixelFormat} of the surface.
+ * @param width The new width of the surface.
+ * @param height The new height of the surface.
+ */
+ void surfaceChanged(@NonNull SurfaceHolder holder, @PixelFormat.Format int format,
+ @IntRange(from = 0) int width, @IntRange(from = 0) int height);
+
+ /**
+ * This is called immediately before a surface is being destroyed. After
+ * returning from this call, you should no longer try to access this
+ * surface. If you have a rendering thread that directly accesses
+ * the surface, you must ensure that thread is no longer touching the
+ * Surface before returning from this function.
+ *
+ * @param holder The SurfaceHolder whose surface is being destroyed.
+ */
+ void surfaceDestroyed(@NonNull SurfaceHolder holder);
+ }
+
+ /**
+ * Additional callbacks that can be received for {@link Callback}.
+ */
+ public interface Callback2 extends Callback {
+ /**
+ * Called when the application needs to redraw the content of its
+ * surface, after it is resized or for some other reason. By not
+ * returning from here until the redraw is complete, you can ensure that
+ * the user will not see your surface in a bad state (at its new
+ * size before it has been correctly drawn that way). This will
+ * typically be preceeded by a call to {@link #surfaceChanged}.
+ *
+ * As of O, {@link #surfaceRedrawNeededAsync} may be implemented
+ * to provide a non-blocking implementation. If {@link #surfaceRedrawNeededAsync}
+ * is not implemented, then this will be called instead.
+ *
+ * @param holder The SurfaceHolder whose surface has changed.
+ */
+ void surfaceRedrawNeeded(@NonNull SurfaceHolder holder);
+
+ /**
+ * An alternative to surfaceRedrawNeeded where it is not required to block
+ * until the redraw is complete. You should initiate the redraw, and return,
+ * later invoking drawingFinished when your redraw is complete.
+ *
+ * This can be useful to avoid blocking your main application thread on rendering.
+ *
+ * As of O, if this is implemented {@link #surfaceRedrawNeeded} will not be called.
+ * However it is still recommended to implement {@link #surfaceRedrawNeeded} for
+ * compatibility with older versions of the platform.
+ *
+ * @param holder The SurfaceHolder which needs redrawing.
+ * @param drawingFinished A runnable to signal completion. This may be invoked
+ * from any thread.
+ *
+ */
+ default void surfaceRedrawNeededAsync(@NonNull SurfaceHolder holder,
+ @NonNull Runnable drawingFinished) {
+ surfaceRedrawNeeded(holder);
+ drawingFinished.run();
+ }
+ }
+
+ /**
+ * Add a Callback interface for this holder. There can several Callback
+ * interfaces associated with a holder.
+ *
+ * @param callback The new Callback interface.
+ */
+ public void addCallback(Callback callback);
+
+ /**
+ * Removes a previously added Callback interface from this holder.
+ *
+ * @param callback The Callback interface to remove.
+ */
+ public void removeCallback(Callback callback);
+
+ /**
+ * Use this method to find out if the surface is in the process of being
+ * created from Callback methods. This is intended to be used with
+ * {@link Callback#surfaceChanged}.
+ *
+ * @return true if the surface is in the process of being created.
+ */
+ public boolean isCreating();
+
+ /**
+ * Sets the surface's type.
+ *
+ * @deprecated this is ignored, this value is set automatically when needed.
+ */
+ @Deprecated
+ public void setType(int type);
+
+ /**
+ * Make the surface a fixed size. It will never change from this size.
+ * When working with a {@link SurfaceView}, this must be called from the
+ * same thread running the SurfaceView's window.
+ *
+ * @param width The surface's width.
+ * @param height The surface's height.
+ */
+ public void setFixedSize(int width, int height);
+
+ /**
+ * Allow the surface to resized based on layout of its container (this is
+ * the default). When this is enabled, you should monitor
+ * {@link Callback#surfaceChanged} for changes to the size of the surface.
+ * When working with a {@link SurfaceView}, this must be called from the
+ * same thread running the SurfaceView's window.
+ */
+ public void setSizeFromLayout();
+
+ /**
+ * Set the desired PixelFormat of the surface. The default is OPAQUE.
+ * When working with a {@link SurfaceView}, this must be called from the
+ * same thread running the SurfaceView's window.
+ *
+ * @param format A constant from PixelFormat.
+ *
+ * @see android.graphics.PixelFormat
+ */
+ public void setFormat(int format);
+
+ /**
+ * Enable or disable option to keep the screen turned on while this
+ * surface is displayed. The default is false, allowing it to turn off.
+ * This is safe to call from any thread.
+ *
+ * @param screenOn Set to true to force the screen to stay on, false
+ * to allow it to turn off.
+ */
+ public void setKeepScreenOn(boolean screenOn);
+
+ /**
+ * Start editing the pixels in the surface. The returned Canvas can be used
+ * to draw into the surface's bitmap. A null is returned if the surface has
+ * not been created or otherwise cannot be edited. You will usually need
+ * to implement {@link Callback#surfaceCreated Callback.surfaceCreated}
+ * to find out when the Surface is available for use.
+ *
+ * <p>The content of the Surface is never preserved between unlockCanvas() and
+ * lockCanvas(), for this reason, every pixel within the Surface area
+ * must be written. The only exception to this rule is when a dirty
+ * rectangle is specified, in which case, non-dirty pixels will be
+ * preserved.
+ *
+ * <p>If you call this repeatedly when the Surface is not ready (before
+ * {@link Callback#surfaceCreated Callback.surfaceCreated} or after
+ * {@link Callback#surfaceDestroyed Callback.surfaceDestroyed}), your calls
+ * will be throttled to a slow rate in order to avoid consuming CPU.
+ *
+ * <p>If null is not returned, this function internally holds a lock until
+ * the corresponding {@link #unlockCanvasAndPost} call, preventing
+ * {@link SurfaceView} from creating, destroying, or modifying the surface
+ * while it is being drawn. This can be more convenient than accessing
+ * the Surface directly, as you do not need to do special synchronization
+ * with a drawing thread in {@link Callback#surfaceDestroyed
+ * Callback.surfaceDestroyed}.
+ *
+ * @return Canvas Use to draw into the surface.
+ */
+ public Canvas lockCanvas();
+
+
+ /**
+ * Just like {@link #lockCanvas()} but allows specification of a dirty rectangle.
+ * Every
+ * pixel within that rectangle must be written; however pixels outside
+ * the dirty rectangle will be preserved by the next call to lockCanvas().
+ *
+ * @see android.view.SurfaceHolder#lockCanvas
+ *
+ * @param dirty Area of the Surface that will be modified.
+ * @return Canvas Use to draw into the surface.
+ */
+ public Canvas lockCanvas(Rect dirty);
+
+ /**
+ * <p>Just like {@link #lockCanvas()} but the returned canvas is hardware-accelerated.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/graphics/hardware-accel.html#unsupported">
+ * unsupported drawing operations</a> for a list of what is and isn't
+ * supported in a hardware-accelerated canvas.
+ *
+ * @return Canvas Use to draw into the surface.
+ * @throws IllegalStateException If the canvas cannot be locked.
+ */
+ default Canvas lockHardwareCanvas() {
+ throw new IllegalStateException("This SurfaceHolder doesn't support lockHardwareCanvas");
+ }
+
+ /**
+ * Finish editing pixels in the surface. After this call, the surface's
+ * current pixels will be shown on the screen, but its content is lost,
+ * in particular there is no guarantee that the content of the Surface
+ * will remain unchanged when lockCanvas() is called again.
+ *
+ * @see #lockCanvas()
+ *
+ * @param canvas The Canvas previously returned by lockCanvas().
+ */
+ public void unlockCanvasAndPost(Canvas canvas);
+
+ /**
+ * Retrieve the current size of the surface. Note: do not modify the
+ * returned Rect. This is only safe to call from the thread of
+ * {@link SurfaceView}'s window, or while inside of
+ * {@link #lockCanvas()}.
+ *
+ * @return Rect The surface's dimensions. The left and top are always 0.
+ */
+ public Rect getSurfaceFrame();
+
+ /**
+ * Direct access to the surface object. The Surface may not always be
+ * available -- for example when using a {@link SurfaceView} the holder's
+ * Surface is not created until the view has been attached to the window
+ * manager and performed a layout in order to determine the dimensions
+ * and screen position of the Surface. You will thus usually need
+ * to implement {@link Callback#surfaceCreated Callback.surfaceCreated}
+ * to find out when the Surface is available for use.
+ *
+ * <p>Note that if you directly access the Surface from another thread,
+ * it is critical that you correctly implement
+ * {@link Callback#surfaceCreated Callback.surfaceCreated} and
+ * {@link Callback#surfaceDestroyed Callback.surfaceDestroyed} to ensure
+ * that thread only accesses the Surface while it is valid, and that the
+ * Surface does not get destroyed while the thread is using it.
+ *
+ * <p>This method is intended to be used by frameworks which often need
+ * direct access to the Surface object (usually to pass it to native code).
+ *
+ * @return Surface The surface.
+ */
+ public Surface getSurface();
+}
diff --git a/android/view/SurfaceSession.java b/android/view/SurfaceSession.java
new file mode 100644
index 0000000..20f0598
--- /dev/null
+++ b/android/view/SurfaceSession.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+/**
+ * An instance of this class represents a connection to the surface
+ * flinger, from which you can create one or more Surface instances that will
+ * be composited to the screen.
+ * {@hide}
+ */
+public final class SurfaceSession {
+ // Note: This field is accessed by native code.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private long mNativeClient; // SurfaceComposerClient*
+
+ private static native long nativeCreate();
+ private static native void nativeDestroy(long ptr);
+
+ /** Create a new connection with the surface flinger. */
+ @UnsupportedAppUsage
+ public SurfaceSession() {
+ mNativeClient = nativeCreate();
+ }
+
+ /* no user serviceable parts here ... */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ kill();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Remove the reference to the native Session object. The native object may still exist if
+ * there are other references to it, but it cannot be accessed from this Java object anymore.
+ */
+ @UnsupportedAppUsage
+ public void kill() {
+ if (mNativeClient != 0) {
+ nativeDestroy(mNativeClient);
+ mNativeClient = 0;
+ }
+ }
+}
+
diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java
new file mode 100644
index 0000000..ebb2af4
--- /dev/null
+++ b/android/view/SurfaceView.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.layoutlib.bridge.MockView;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.AttributeSet;
+
+/**
+ * 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.
+ *
+ * TODO: generate automatically.
+ *
+ */
+public class SurfaceView extends MockView {
+
+ public SurfaceView(Context context) {
+ this(context, null);
+ }
+
+ public SurfaceView(Context context, AttributeSet attrs) {
+ this(context, attrs , 0);
+ }
+
+ public SurfaceView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public boolean gatherTransparentRegion(Region region) {
+ return false;
+ }
+
+ public void setZOrderMediaOverlay(boolean isMediaOverlay) {
+ }
+
+ public void setZOrderOnTop(boolean onTop) {
+ }
+
+ public void setSecure(boolean isSecure) {
+ }
+
+ public SurfaceHolder getHolder() {
+ return mSurfaceHolder;
+ }
+
+ private SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
+
+ @Override
+ public boolean isCreating() {
+ return false;
+ }
+
+ @Override
+ public void addCallback(Callback callback) {
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ }
+
+ @Override
+ public void setFixedSize(int width, int height) {
+ }
+
+ @Override
+ public void setSizeFromLayout() {
+ }
+
+ @Override
+ public void setFormat(int format) {
+ }
+
+ @Override
+ public void setType(int type) {
+ }
+
+ @Override
+ public void setKeepScreenOn(boolean screenOn) {
+ }
+
+ @Override
+ public Canvas lockCanvas() {
+ return null;
+ }
+
+ @Override
+ public Canvas lockCanvas(Rect dirty) {
+ return null;
+ }
+
+ @Override
+ public void unlockCanvasAndPost(Canvas canvas) {
+ }
+
+ @Override
+ public Surface getSurface() {
+ return null;
+ }
+
+ @Override
+ public Rect getSurfaceFrame() {
+ return null;
+ }
+ };
+}
+
diff --git a/android/view/SyncRtSurfaceTransactionApplier.java b/android/view/SyncRtSurfaceTransactionApplier.java
new file mode 100644
index 0000000..3e21103
--- /dev/null
+++ b/android/view/SyncRtSurfaceTransactionApplier.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.function.Consumer;
+
+/**
+ * Helper class to apply surface transactions in sync with RenderThread.
+ * @hide
+ */
+public class SyncRtSurfaceTransactionApplier {
+
+ public static final int FLAG_ALL = 0xffffffff;
+ public static final int FLAG_ALPHA = 1;
+ public static final int FLAG_MATRIX = 1 << 1;
+ public static final int FLAG_WINDOW_CROP = 1 << 2;
+ public static final int FLAG_LAYER = 1 << 3;
+ public static final int FLAG_CORNER_RADIUS = 1 << 4;
+ public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
+ public static final int FLAG_VISIBILITY = 1 << 6;
+ public static final int FLAG_TRANSACTION = 1 << 7;
+
+ private SurfaceControl mTargetSc;
+ private final ViewRootImpl mTargetViewRootImpl;
+ private final float[] mTmpFloat9 = new float[9];
+
+ /**
+ * @param targetView The view in the surface that acts as synchronization anchor.
+ */
+ public SyncRtSurfaceTransactionApplier(View targetView) {
+ mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
+ }
+
+ /**
+ * Schedules applying surface parameters on the next frame.
+ *
+ * @param params The surface parameters to apply.
+ */
+ public void scheduleApply(final SurfaceParams... params) {
+ if (mTargetViewRootImpl == null) {
+ return;
+ }
+ mTargetSc = mTargetViewRootImpl.getSurfaceControl();
+ final Transaction t = new Transaction();
+ applyParams(t, params);
+
+ mTargetViewRootImpl.registerRtFrameCallback(frame -> {
+ if (mTargetSc == null || !mTargetSc.isValid()) {
+ return;
+ }
+ applyTransaction(t, frame);
+ });
+
+ // Make sure a frame gets scheduled.
+ mTargetViewRootImpl.getView().invalidate();
+ }
+
+ /**
+ * Applies surface parameters on the next frame.
+ * @param t transaction to apply all parameters in.
+ * @param frame frame to synchronize to. Set -1 when sync is not required.
+ * @param params The surface parameters to apply.
+ */
+ void applyParams(Transaction t, final SurfaceParams... params) {
+ for (int i = params.length - 1; i >= 0; i--) {
+ SurfaceParams surfaceParams = params[i];
+ SurfaceControl surface = surfaceParams.surface;
+ applyParams(t, surfaceParams, mTmpFloat9);
+ }
+ }
+
+ void applyTransaction(Transaction t, long frame) {
+ if (mTargetViewRootImpl != null) {
+ mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
+ } else {
+ t.apply();
+ }
+ }
+
+ public static void applyParams(Transaction t, SurfaceParams params, float[] tmpFloat9) {
+ if ((params.flags & FLAG_TRANSACTION) != 0) {
+ t.merge(params.mergeTransaction);
+ }
+
+ if ((params.flags & FLAG_MATRIX) != 0) {
+ t.setMatrix(params.surface, params.matrix, tmpFloat9);
+ }
+ if ((params.flags & FLAG_WINDOW_CROP) != 0) {
+ t.setWindowCrop(params.surface, params.windowCrop);
+ }
+ if ((params.flags & FLAG_ALPHA) != 0) {
+ t.setAlpha(params.surface, params.alpha);
+ }
+ if ((params.flags & FLAG_LAYER) != 0) {
+ t.setLayer(params.surface, params.layer);
+ }
+ if ((params.flags & FLAG_CORNER_RADIUS) != 0) {
+ t.setCornerRadius(params.surface, params.cornerRadius);
+ }
+ if ((params.flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
+ t.setBackgroundBlurRadius(params.surface, params.backgroundBlurRadius);
+ }
+ if ((params.flags & FLAG_VISIBILITY) != 0) {
+ if (params.visible) {
+ t.show(params.surface);
+ } else {
+ t.hide(params.surface);
+ }
+ }
+ }
+
+ /**
+ * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
+ * attached if necessary.
+ */
+ public static void create(final View targetView,
+ final Consumer<SyncRtSurfaceTransactionApplier> callback) {
+ if (targetView == null) {
+ // No target view, no applier
+ callback.accept(null);
+ } else if (targetView.getViewRootImpl() != null) {
+ // Already attached, we're good to go
+ callback.accept(new SyncRtSurfaceTransactionApplier(targetView));
+ } else {
+ // Haven't been attached before we can get the view root
+ targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ targetView.removeOnAttachStateChangeListener(this);
+ callback.accept(new SyncRtSurfaceTransactionApplier(targetView));
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ // Do nothing
+ }
+ });
+ }
+ }
+
+ public static class SurfaceParams {
+
+ public static class Builder {
+ final SurfaceControl surface;
+ int flags;
+ float alpha;
+ float cornerRadius;
+ int backgroundBlurRadius;
+ Matrix matrix;
+ Rect windowCrop;
+ int layer;
+ boolean visible;
+ Transaction mergeTransaction;
+
+ /**
+ * @param surface The surface to modify.
+ */
+ public Builder(SurfaceControl surface) {
+ this.surface = surface;
+ }
+
+ /**
+ * @param alpha The alpha value to apply to the surface.
+ * @return this Builder
+ */
+ public Builder withAlpha(float alpha) {
+ this.alpha = alpha;
+ flags |= FLAG_ALPHA;
+ return this;
+ }
+
+ /**
+ * @param matrix The matrix to apply to the surface.
+ * @return this Builder
+ */
+ public Builder withMatrix(Matrix matrix) {
+ this.matrix = new Matrix(matrix);
+ flags |= FLAG_MATRIX;
+ return this;
+ }
+
+ /**
+ * @param windowCrop The window crop to apply to the surface.
+ * @return this Builder
+ */
+ public Builder withWindowCrop(Rect windowCrop) {
+ this.windowCrop = new Rect(windowCrop);
+ flags |= FLAG_WINDOW_CROP;
+ return this;
+ }
+
+ /**
+ * @param layer The layer to assign the surface.
+ * @return this Builder
+ */
+ public Builder withLayer(int layer) {
+ this.layer = layer;
+ flags |= FLAG_LAYER;
+ return this;
+ }
+
+ /**
+ * @param radius the Radius for rounded corners to apply to the surface.
+ * @return this Builder
+ */
+ public Builder withCornerRadius(float radius) {
+ this.cornerRadius = radius;
+ flags |= FLAG_CORNER_RADIUS;
+ return this;
+ }
+
+ /**
+ * @param radius the Radius for blur to apply to the background surfaces.
+ * @return this Builder
+ */
+ public Builder withBackgroundBlur(int radius) {
+ this.backgroundBlurRadius = radius;
+ flags |= FLAG_BACKGROUND_BLUR_RADIUS;
+ return this;
+ }
+
+ /**
+ * @param visible The visibility to apply to the surface.
+ * @return this Builder
+ */
+ public Builder withVisibility(boolean visible) {
+ this.visible = visible;
+ flags |= FLAG_VISIBILITY;
+ return this;
+ }
+
+ /**
+ * @param mergeTransaction The transaction to apply to the surface. Note this is applied
+ * first before all the other properties.
+ * @return this Builder
+ */
+ public Builder withMergeTransaction(Transaction mergeTransaction) {
+ this.mergeTransaction = mergeTransaction;
+ flags |= FLAG_TRANSACTION;
+ return this;
+ }
+
+ /**
+ * @return a new SurfaceParams instance
+ */
+ public SurfaceParams build() {
+ return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
+ cornerRadius, backgroundBlurRadius, visible, mergeTransaction);
+ }
+ }
+
+ private SurfaceParams(SurfaceControl surface, int params, float alpha, Matrix matrix,
+ Rect windowCrop, int layer, float cornerRadius,
+ int backgroundBlurRadius, boolean visible, Transaction mergeTransaction) {
+ this.flags = params;
+ this.surface = surface;
+ this.alpha = alpha;
+ this.matrix = matrix;
+ this.windowCrop = windowCrop;
+ this.layer = layer;
+ this.cornerRadius = cornerRadius;
+ this.backgroundBlurRadius = backgroundBlurRadius;
+ this.visible = visible;
+ this.mergeTransaction = mergeTransaction;
+ }
+
+ private final int flags;
+
+ @VisibleForTesting
+ public final SurfaceControl surface;
+
+ @VisibleForTesting
+ public final float alpha;
+
+ @VisibleForTesting
+ public final float cornerRadius;
+
+ @VisibleForTesting
+ public final int backgroundBlurRadius;
+
+ @VisibleForTesting
+ public final Matrix matrix;
+
+ @VisibleForTesting
+ public final Rect windowCrop;
+
+ @VisibleForTesting
+ public final int layer;
+
+ public final boolean visible;
+
+ public final Transaction mergeTransaction;
+ }
+}
diff --git a/android/view/TextureView.java b/android/view/TextureView.java
new file mode 100644
index 0000000..fc0ec4c
--- /dev/null
+++ b/android/view/TextureView.java
@@ -0,0 +1,864 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.TextureLayer;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * <p>A TextureView can be used to display a content stream. Such a content
+ * stream can for instance be a video or an OpenGL scene. The content stream
+ * can come from the application's process as well as a remote process.</p>
+ *
+ * <p>TextureView can only be used in a hardware accelerated window. When
+ * rendered in software, TextureView will draw nothing.</p>
+ *
+ * <p>Unlike {@link SurfaceView}, TextureView does not create a separate
+ * window but behaves as a regular View. This key difference allows a
+ * TextureView to be moved, transformed, animated, etc. For instance, you
+ * can make a TextureView semi-translucent by calling
+ * <code>myView.setAlpha(0.5f)</code>.</p>
+ *
+ * <p>Using a TextureView is simple: all you need to do is get its
+ * {@link SurfaceTexture}. The {@link SurfaceTexture} can then be used to
+ * render content. The following example demonstrates how to render the
+ * camera preview into a TextureView:</p>
+ *
+ * <pre>
+ * public class LiveCameraActivity extends Activity implements TextureView.SurfaceTextureListener {
+ * private Camera mCamera;
+ * private TextureView mTextureView;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * mTextureView = new TextureView(this);
+ * mTextureView.setSurfaceTextureListener(this);
+ *
+ * setContentView(mTextureView);
+ * }
+ *
+ * public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ * mCamera = Camera.open();
+ *
+ * try {
+ * mCamera.setPreviewTexture(surface);
+ * mCamera.startPreview();
+ * } catch (IOException ioe) {
+ * // Something bad happened
+ * }
+ * }
+ *
+ * public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ * // Ignored, Camera does all the work for us
+ * }
+ *
+ * public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ * mCamera.stopPreview();
+ * mCamera.release();
+ * return true;
+ * }
+ *
+ * public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ * // Invoked every time there's a new Camera preview frame
+ * }
+ * }
+ * </pre>
+ *
+ * <p>A TextureView's SurfaceTexture can be obtained either by invoking
+ * {@link #getSurfaceTexture()} or by using a {@link SurfaceTextureListener}.
+ * It is important to know that a SurfaceTexture is available only after the
+ * TextureView is attached to a window (and {@link #onAttachedToWindow()} has
+ * been invoked.) It is therefore highly recommended you use a listener to
+ * be notified when the SurfaceTexture becomes available.</p>
+ *
+ * <p>It is important to note that only one producer can use the TextureView.
+ * For instance, if you use a TextureView to display the camera preview, you
+ * cannot use {@link #lockCanvas()} to draw onto the TextureView at the same
+ * time.</p>
+ *
+ * @see SurfaceView
+ * @see SurfaceTexture
+ */
+public class TextureView extends View {
+ private static final String LOG_TAG = "TextureView";
+
+ @UnsupportedAppUsage
+ private TextureLayer mLayer;
+ @UnsupportedAppUsage
+ private SurfaceTexture mSurface;
+ private SurfaceTextureListener mListener;
+ private boolean mHadSurface;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private boolean mOpaque = true;
+
+ private final Matrix mMatrix = new Matrix();
+ private boolean mMatrixChanged;
+
+ private final Object[] mLock = new Object[0];
+ private boolean mUpdateLayer;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private boolean mUpdateSurface;
+
+ private Canvas mCanvas;
+ private int mSaveCount;
+
+ private final Object[] mNativeWindowLock = new Object[0];
+ // Set by native code, do not write!
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private long mNativeWindow;
+
+ /**
+ * Creates a new TextureView.
+ *
+ * @param context The context to associate this view with.
+ */
+ public TextureView(@NonNull Context context) {
+ super(context);
+ }
+
+ /**
+ * Creates a new TextureView.
+ *
+ * @param context The context to associate this view with.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public TextureView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Creates a new TextureView.
+ *
+ * @param context The context to associate this view with.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @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 TextureView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Creates a new TextureView.
+ *
+ * @param context The context to associate this view with.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @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 TextureView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ /**
+ * Indicates whether the content of this TextureView is opaque. The
+ * content is assumed to be opaque by default.
+ *
+ * @param opaque True if the content of this TextureView is opaque,
+ * false otherwise
+ */
+ public void setOpaque(boolean opaque) {
+ if (opaque != mOpaque) {
+ mOpaque = opaque;
+ if (mLayer != null) {
+ updateLayerAndInvalidate();
+ }
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (!isHardwareAccelerated()) {
+ Log.w(LOG_TAG, "A TextureView or a subclass can only be "
+ + "used with hardware acceleration enabled.");
+ }
+
+ if (mHadSurface) {
+ invalidate(true);
+ mHadSurface = false;
+ }
+ }
+
+ /** @hide */
+ @Override
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void onDetachedFromWindowInternal() {
+ destroyHardwareLayer();
+ releaseSurfaceTexture();
+ super.onDetachedFromWindowInternal();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @UnsupportedAppUsage
+ protected void destroyHardwareResources() {
+ super.destroyHardwareResources();
+ destroyHardwareLayer();
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void destroyHardwareLayer() {
+ if (mLayer != null) {
+ mLayer.detachSurfaceTexture();
+ mLayer.close();
+ mLayer = null;
+ mMatrixChanged = true;
+ }
+ }
+
+ private void releaseSurfaceTexture() {
+ if (mSurface != null) {
+ boolean shouldRelease = true;
+
+ if (mListener != null) {
+ shouldRelease = mListener.onSurfaceTextureDestroyed(mSurface);
+ }
+
+ synchronized (mNativeWindowLock) {
+ nDestroyNativeWindow();
+ }
+
+ if (shouldRelease) {
+ mSurface.release();
+ }
+ mSurface = null;
+ mHadSurface = true;
+ }
+ }
+
+ /**
+ * The layer type of a TextureView is ignored since a TextureView is always
+ * considered to act as a hardware layer. The optional paint supplied to this
+ * method will however be taken into account when rendering the content of
+ * this TextureView.
+ *
+ * @param layerType The type of layer to use with this view, must be one of
+ * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or
+ * {@link #LAYER_TYPE_HARDWARE}
+ * @param paint The paint used to compose the layer. This argument is optional
+ * and can be null. It is ignored when the layer type is
+ * {@link #LAYER_TYPE_NONE}
+ */
+ @Override
+ public void setLayerType(int layerType, @Nullable Paint paint) {
+ setLayerPaint(paint);
+ }
+
+ @Override
+ public void setLayerPaint(@Nullable Paint paint) {
+ if (paint != mLayerPaint) {
+ mLayerPaint = paint;
+ invalidate();
+ }
+ }
+
+ /**
+ * Always returns {@link #LAYER_TYPE_HARDWARE}.
+ */
+ @Override
+ public int getLayerType() {
+ return LAYER_TYPE_HARDWARE;
+ }
+
+ /**
+ * Calling this method has no effect.
+ */
+ @Override
+ public void buildLayer() {
+ }
+
+ @Override
+ public void setForeground(Drawable foreground) {
+ if (foreground != null && !sTextureViewIgnoresDrawableSetters) {
+ throw new UnsupportedOperationException(
+ "TextureView doesn't support displaying a foreground drawable");
+ }
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable background) {
+ if (background != null && !sTextureViewIgnoresDrawableSetters) {
+ throw new UnsupportedOperationException(
+ "TextureView doesn't support displaying a background drawable");
+ }
+ }
+
+ /**
+ * Subclasses of TextureView cannot do their own rendering
+ * with the {@link Canvas} object.
+ *
+ * @param canvas The Canvas to which the View is rendered.
+ */
+ @Override
+ public final void draw(Canvas canvas) {
+ // NOTE: Maintain this carefully (see View#draw)
+ mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
+
+ /* Simplify drawing to guarantee the layer is the only thing drawn - so e.g. no background,
+ scrolling, or fading edges. This guarantees all drawing is in the layer, so drawing
+ properties (alpha, layer paint) affect all of the content of a TextureView. */
+
+ if (canvas.isHardwareAccelerated()) {
+ RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
+
+ TextureLayer layer = getTextureLayer();
+ if (layer != null) {
+ applyUpdate();
+ applyTransformMatrix();
+
+ mLayer.setLayerPaint(mLayerPaint); // ensure layer paint is up to date
+ recordingCanvas.drawTextureLayer(layer);
+ }
+ }
+ }
+
+ /**
+ * Subclasses of TextureView cannot do their own rendering
+ * with the {@link Canvas} object.
+ *
+ * @param canvas The Canvas to which the View is rendered.
+ */
+ @Override
+ protected final void onDraw(Canvas canvas) {
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (mSurface != null) {
+ mSurface.setDefaultBufferSize(getWidth(), getHeight());
+ updateLayer();
+ if (mListener != null) {
+ mListener.onSurfaceTextureSizeChanged(mSurface, getWidth(), getHeight());
+ }
+ }
+ }
+
+ TextureLayer getTextureLayer() {
+ if (mLayer == null) {
+ if (mAttachInfo == null || mAttachInfo.mThreadedRenderer == null) {
+ return null;
+ }
+
+ mLayer = mAttachInfo.mThreadedRenderer.createTextureLayer();
+ boolean createNewSurface = (mSurface == null);
+ if (createNewSurface) {
+ // Create a new SurfaceTexture for the layer.
+ mSurface = new SurfaceTexture(false);
+ nCreateNativeWindow(mSurface);
+ }
+ mLayer.setSurfaceTexture(mSurface);
+ mSurface.setDefaultBufferSize(getWidth(), getHeight());
+ mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);
+
+ if (mListener != null && createNewSurface) {
+ mListener.onSurfaceTextureAvailable(mSurface, getWidth(), getHeight());
+ }
+ mLayer.setLayerPaint(mLayerPaint);
+ }
+
+ if (mUpdateSurface) {
+ // Someone has requested that we use a specific SurfaceTexture, so
+ // tell mLayer about it and set the SurfaceTexture to use the
+ // current view size.
+ mUpdateSurface = false;
+
+ // Since we are updating the layer, force an update to ensure its
+ // parameters are correct (width, height, transform, etc.)
+ updateLayer();
+ mMatrixChanged = true;
+
+ mLayer.setSurfaceTexture(mSurface);
+ mSurface.setDefaultBufferSize(getWidth(), getHeight());
+ }
+
+ return mLayer;
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ if (mSurface != null) {
+ // When the view becomes invisible, stop updating it, it's a waste of CPU
+ // To cancel updates, the easiest thing to do is simply to remove the
+ // updates listener
+ if (visibility == VISIBLE) {
+ if (mLayer != null) {
+ mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);
+ }
+ updateLayerAndInvalidate();
+ } else {
+ mSurface.setOnFrameAvailableListener(null);
+ }
+ }
+ }
+
+ private void updateLayer() {
+ synchronized (mLock) {
+ mUpdateLayer = true;
+ }
+ }
+
+ private void updateLayerAndInvalidate() {
+ synchronized (mLock) {
+ mUpdateLayer = true;
+ }
+ invalidate();
+ }
+
+ private void applyUpdate() {
+ if (mLayer == null) {
+ return;
+ }
+
+ synchronized (mLock) {
+ if (mUpdateLayer) {
+ mUpdateLayer = false;
+ } else {
+ return;
+ }
+ }
+
+ mLayer.prepare(getWidth(), getHeight(), mOpaque);
+ mLayer.updateSurfaceTexture();
+
+ if (mListener != null) {
+ mListener.onSurfaceTextureUpdated(mSurface);
+ }
+ }
+
+ /**
+ * <p>Sets the transform to associate with this texture view.
+ * The specified transform applies to the underlying surface
+ * texture and does not affect the size or position of the view
+ * itself, only of its content.</p>
+ *
+ * <p>Some transforms might prevent the content from drawing
+ * all the pixels contained within this view's bounds. In such
+ * situations, make sure this texture view is not marked opaque.</p>
+ *
+ * @param transform The transform to apply to the content of
+ * this view. If null the transform will be set to identity.
+ *
+ * @see #getTransform(android.graphics.Matrix)
+ * @see #isOpaque()
+ * @see #setOpaque(boolean)
+ */
+ public void setTransform(@Nullable Matrix transform) {
+ mMatrix.set(transform);
+ mMatrixChanged = true;
+ invalidateParentIfNeeded();
+ }
+
+ /**
+ * Returns the transform associated with this texture view.
+ *
+ * @param transform The {@link Matrix} in which to copy the current
+ * transform. Can be null.
+ *
+ * @return The specified matrix if not null or a new {@link Matrix}
+ * instance otherwise.
+ *
+ * @see #setTransform(android.graphics.Matrix)
+ */
+ public @NonNull Matrix getTransform(@Nullable Matrix transform) {
+ if (transform == null) {
+ transform = new Matrix();
+ }
+
+ transform.set(mMatrix);
+
+ return transform;
+ }
+
+ private void applyTransformMatrix() {
+ if (mMatrixChanged && mLayer != null) {
+ mLayer.setTransform(mMatrix);
+ mMatrixChanged = false;
+ }
+ }
+
+ /**
+ * <p>Returns a {@link android.graphics.Bitmap} representation of the content
+ * of the associated surface texture. If the surface texture is not available,
+ * this method returns null.</p>
+ *
+ * <p>The bitmap returned by this method uses the {@link Bitmap.Config#ARGB_8888}
+ * pixel format and its dimensions are the same as this view's.</p>
+ *
+ * <p><strong>Do not</strong> invoke this method from a drawing method
+ * ({@link #onDraw(android.graphics.Canvas)} for instance).</p>
+ *
+ * <p>If an error occurs during the copy, an empty bitmap will be returned.</p>
+ *
+ * @return A valid {@link Bitmap.Config#ARGB_8888} bitmap, or null if the surface
+ * texture is not available or the width <= 0 or the height <= 0
+ *
+ * @see #isAvailable()
+ * @see #getBitmap(android.graphics.Bitmap)
+ * @see #getBitmap(int, int)
+ */
+ public @Nullable Bitmap getBitmap() {
+ return getBitmap(getWidth(), getHeight());
+ }
+
+ /**
+ * <p>Returns a {@link android.graphics.Bitmap} representation of the content
+ * of the associated surface texture. If the surface texture is not available,
+ * this method returns null.</p>
+ *
+ * <p>The bitmap returned by this method uses the {@link Bitmap.Config#ARGB_8888}
+ * pixel format.</p>
+ *
+ * <p><strong>Do not</strong> invoke this method from a drawing method
+ * ({@link #onDraw(android.graphics.Canvas)} for instance).</p>
+ *
+ * <p>If an error occurs during the copy, an empty bitmap will be returned.</p>
+ *
+ * @param width The width of the bitmap to create
+ * @param height The height of the bitmap to create
+ *
+ * @return A valid {@link Bitmap.Config#ARGB_8888} bitmap, or null if the surface
+ * texture is not available or width is <= 0 or height is <= 0
+ *
+ * @see #isAvailable()
+ * @see #getBitmap(android.graphics.Bitmap)
+ * @see #getBitmap()
+ */
+ public @Nullable Bitmap getBitmap(int width, int height) {
+ if (isAvailable() && width > 0 && height > 0) {
+ return getBitmap(Bitmap.createBitmap(getResources().getDisplayMetrics(),
+ width, height, Bitmap.Config.ARGB_8888));
+ }
+ return null;
+ }
+
+ /**
+ * <p>Copies the content of this view's surface texture into the specified
+ * bitmap. If the surface texture is not available, the copy is not executed.
+ * The content of the surface texture will be scaled to fit exactly inside
+ * the specified bitmap.</p>
+ *
+ * <p><strong>Do not</strong> invoke this method from a drawing method
+ * ({@link #onDraw(android.graphics.Canvas)} for instance).</p>
+ *
+ * <p>If an error occurs, the bitmap is left unchanged.</p>
+ *
+ * @param bitmap The bitmap to copy the content of the surface texture into,
+ * cannot be null, all configurations are supported
+ *
+ * @return The bitmap specified as a parameter
+ *
+ * @see #isAvailable()
+ * @see #getBitmap(int, int)
+ * @see #getBitmap()
+ *
+ * @throws IllegalStateException if the hardware rendering context cannot be
+ * acquired to capture the bitmap
+ */
+ public @NonNull Bitmap getBitmap(@NonNull Bitmap bitmap) {
+ if (bitmap != null && isAvailable()) {
+ applyUpdate();
+ applyTransformMatrix();
+
+ // This case can happen if the app invokes setSurfaceTexture() before
+ // we are able to create the hardware layer. We can safely initialize
+ // the layer here thanks to the validate() call at the beginning of
+ // this method
+ if (mLayer == null && mUpdateSurface) {
+ getTextureLayer();
+ }
+
+ if (mLayer != null) {
+ mLayer.copyInto(bitmap);
+ }
+ }
+ return bitmap;
+ }
+
+ /**
+ * Returns true if the {@link SurfaceTexture} associated with this
+ * TextureView is available for rendering. When this method returns
+ * true, {@link #getSurfaceTexture()} returns a valid surface texture.
+ */
+ public boolean isAvailable() {
+ return mSurface != null;
+ }
+
+ /**
+ * <p>Start editing the pixels in the surface. The returned Canvas can be used
+ * to draw into the surface's bitmap. A null is returned if the surface has
+ * not been created or otherwise cannot be edited. You will usually need
+ * to implement
+ * {@link SurfaceTextureListener#onSurfaceTextureAvailable(android.graphics.SurfaceTexture, int, int)}
+ * to find out when the Surface is available for use.</p>
+ *
+ * <p>The content of the Surface is never preserved between unlockCanvas()
+ * and lockCanvas(), for this reason, every pixel within the Surface area
+ * must be written. The only exception to this rule is when a dirty
+ * rectangle is specified, in which case, non-dirty pixels will be
+ * preserved.</p>
+ *
+ * <p>This method can only be used if the underlying surface is not already
+ * owned by another producer. For instance, if the TextureView is being used
+ * to render the camera's preview you cannot invoke this method.</p>
+ *
+ * @return A Canvas used to draw into the surface, or null if the surface cannot be locked for
+ * drawing (see {@link #isAvailable()}).
+ *
+ * @see #lockCanvas(android.graphics.Rect)
+ * @see #unlockCanvasAndPost(android.graphics.Canvas)
+ */
+ public @Nullable Canvas lockCanvas() {
+ return lockCanvas(null);
+ }
+
+ /**
+ * Just like {@link #lockCanvas()} but allows specification of a dirty
+ * rectangle. Every pixel within that rectangle must be written; however
+ * pixels outside the dirty rectangle will be preserved by the next call
+ * to lockCanvas().
+ *
+ * This method can return null if the underlying surface texture is not
+ * available (see {@link #isAvailable()} or if the surface texture is
+ * already connected to an image producer (for instance: the camera,
+ * OpenGL, a media player, etc.)
+ *
+ * @param dirty Area of the surface that will be modified. If null the area of the entire
+ * surface is used.
+
+ * @return A Canvas used to draw into the surface, or null if the surface cannot be locked for
+ * drawing (see {@link #isAvailable()}).
+ *
+ * @see #lockCanvas()
+ * @see #unlockCanvasAndPost(android.graphics.Canvas)
+ * @see #isAvailable()
+ */
+ public @Nullable Canvas lockCanvas(@Nullable Rect dirty) {
+ if (!isAvailable()) return null;
+
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+
+ synchronized (mNativeWindowLock) {
+ if (!nLockCanvas(mNativeWindow, mCanvas, dirty)) {
+ return null;
+ }
+ }
+ mSaveCount = mCanvas.save();
+
+ return mCanvas;
+ }
+
+ /**
+ * Finish editing pixels in the surface. After this call, the surface's
+ * current pixels will be shown on the screen, but its content is lost,
+ * in particular there is no guarantee that the content of the Surface
+ * will remain unchanged when lockCanvas() is called again.
+ *
+ * @param canvas The Canvas previously returned by lockCanvas()
+ *
+ * @see #lockCanvas()
+ * @see #lockCanvas(android.graphics.Rect)
+ */
+ public void unlockCanvasAndPost(@NonNull Canvas canvas) {
+ if (mCanvas != null && canvas == mCanvas) {
+ canvas.restoreToCount(mSaveCount);
+ mSaveCount = 0;
+
+ synchronized (mNativeWindowLock) {
+ nUnlockCanvasAndPost(mNativeWindow, mCanvas);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link SurfaceTexture} used by this view. This method
+ * may return null if the view is not attached to a window or if the surface
+ * texture has not been initialized yet.
+ *
+ * @see #isAvailable()
+ */
+ public @Nullable SurfaceTexture getSurfaceTexture() {
+ return mSurface;
+ }
+
+ /**
+ * Set the {@link SurfaceTexture} for this view to use. If a {@link
+ * SurfaceTexture} is already being used by this view, it is immediately
+ * released and not usable any more. The {@link
+ * SurfaceTextureListener#onSurfaceTextureDestroyed} callback is <b>not</b>
+ * called for the previous {@link SurfaceTexture}. Similarly, the {@link
+ * SurfaceTextureListener#onSurfaceTextureAvailable} callback is <b>not</b>
+ * called for the {@link SurfaceTexture} passed to setSurfaceTexture.
+ *
+ * The {@link SurfaceTexture} object must be detached from all OpenGL ES
+ * contexts prior to calling this method.
+ *
+ * @param surfaceTexture The {@link SurfaceTexture} that the view should use.
+ * @see SurfaceTexture#detachFromGLContext()
+ */
+ public void setSurfaceTexture(@NonNull SurfaceTexture surfaceTexture) {
+ if (surfaceTexture == null) {
+ throw new NullPointerException("surfaceTexture must not be null");
+ }
+ if (surfaceTexture == mSurface) {
+ throw new IllegalArgumentException("Trying to setSurfaceTexture to " +
+ "the same SurfaceTexture that's already set.");
+ }
+ if (surfaceTexture.isReleased()) {
+ throw new IllegalArgumentException("Cannot setSurfaceTexture to a " +
+ "released SurfaceTexture");
+ }
+ if (mSurface != null) {
+ nDestroyNativeWindow();
+ mSurface.release();
+ }
+ mSurface = surfaceTexture;
+ nCreateNativeWindow(mSurface);
+
+ /*
+ * If the view is visible and we already made a layer, update the
+ * listener in the new surface to use the existing listener in the view.
+ * Otherwise this will be called when the view becomes visible or the
+ * layer is created
+ */
+ if (((mViewFlags & VISIBILITY_MASK) == VISIBLE) && mLayer != null) {
+ mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);
+ }
+ mUpdateSurface = true;
+ invalidateParentIfNeeded();
+ }
+
+ /**
+ * Returns the {@link SurfaceTextureListener} currently associated with this
+ * texture view.
+ *
+ * @see #setSurfaceTextureListener(android.view.TextureView.SurfaceTextureListener)
+ * @see SurfaceTextureListener
+ */
+ public @Nullable SurfaceTextureListener getSurfaceTextureListener() {
+ return mListener;
+ }
+
+ /**
+ * Sets the {@link SurfaceTextureListener} used to listen to surface
+ * texture events.
+ *
+ * @see #getSurfaceTextureListener()
+ * @see SurfaceTextureListener
+ */
+ public void setSurfaceTextureListener(@Nullable SurfaceTextureListener listener) {
+ mListener = listener;
+ }
+
+ @UnsupportedAppUsage
+ private final SurfaceTexture.OnFrameAvailableListener mUpdateListener =
+ surfaceTexture -> {
+ updateLayer();
+ invalidate();
+ };
+
+ /**
+ * This listener can be used to be notified when the surface texture
+ * associated with this texture view is available.
+ */
+ public interface SurfaceTextureListener {
+ /**
+ * Invoked when a {@link TextureView}'s SurfaceTexture is ready for use.
+ *
+ * @param surface The surface returned by
+ * {@link android.view.TextureView#getSurfaceTexture()}
+ * @param width The width of the surface
+ * @param height The height of the surface
+ */
+ void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height);
+
+ /**
+ * Invoked when the {@link SurfaceTexture}'s buffers size changed.
+ *
+ * @param surface The surface returned by
+ * {@link android.view.TextureView#getSurfaceTexture()}
+ * @param width The new width of the surface
+ * @param height The new height of the surface
+ */
+ void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height);
+
+ /**
+ * Invoked when the specified {@link SurfaceTexture} is about to be destroyed.
+ * If returns true, no rendering should happen inside the surface texture after this method
+ * is invoked. If returns false, the client needs to call {@link SurfaceTexture#release()}.
+ * Most applications should return true.
+ *
+ * @param surface The surface about to be destroyed
+ */
+ boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface);
+
+ /**
+ * Invoked when the specified {@link SurfaceTexture} is updated through
+ * {@link SurfaceTexture#updateTexImage()}.
+ *
+ * @param surface The surface just updated
+ */
+ void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private native void nCreateNativeWindow(SurfaceTexture surface);
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private native void nDestroyNativeWindow();
+
+ private static native boolean nLockCanvas(long nativeWindow, Canvas canvas, Rect dirty);
+ private static native void nUnlockCanvasAndPost(long nativeWindow, Canvas canvas);
+}
diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java
new file mode 100644
index 0000000..d839e35
--- /dev/null
+++ b/android/view/ThreadedRenderer.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.FrameInfo;
+import android.graphics.HardwareRenderer;
+import android.graphics.Picture;
+import android.graphics.Point;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.os.Trace;
+import android.util.Log;
+import android.view.Surface.OutOfResourcesException;
+import android.view.View.AttachInfo;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.R;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Threaded renderer that proxies the rendering to a render thread. Most calls
+ * are currently synchronous.
+ *
+ * The UI thread can block on the RenderThread, but RenderThread must never
+ * block on the UI thread.
+ *
+ * ThreadedRenderer creates an instance of RenderProxy. RenderProxy in turn creates
+ * and manages a CanvasContext on the RenderThread. The CanvasContext is fully managed
+ * by the lifecycle of the RenderProxy.
+ *
+ * Note that although currently the EGL context & surfaces are created & managed
+ * by the render thread, the goal is to move that into a shared structure that can
+ * be managed by both threads. EGLSurface creation & deletion should ideally be
+ * done on the UI thread and not the RenderThread to avoid stalling the
+ * RenderThread with surface buffer allocation.
+ *
+ * @hide
+ */
+public final class ThreadedRenderer extends HardwareRenderer {
+ /**
+ * System property used to enable or disable threaded rendering profiling.
+ * The default value of this property is assumed to be false.
+ *
+ * When profiling is enabled, the adb shell dumpsys gfxinfo command will
+ * output extra information about the time taken to execute by the last
+ * frames.
+ *
+ * Possible values:
+ * "true", to enable profiling
+ * "visual_bars", to enable profiling and visualize the results on screen
+ * "false", to disable profiling
+ *
+ * @see #PROFILE_PROPERTY_VISUALIZE_BARS
+ *
+ * @hide
+ */
+ public static final String PROFILE_PROPERTY = "debug.hwui.profile";
+
+ /**
+ * Value for {@link #PROFILE_PROPERTY}. When the property is set to this
+ * value, profiling data will be visualized on screen as a bar chart.
+ *
+ * @hide
+ */
+ public static final String PROFILE_PROPERTY_VISUALIZE_BARS = "visual_bars";
+
+ /**
+ * System property used to specify the number of frames to be used
+ * when doing threaded rendering profiling.
+ * The default value of this property is #PROFILE_MAX_FRAMES.
+ *
+ * When profiling is enabled, the adb shell dumpsys gfxinfo command will
+ * output extra information about the time taken to execute by the last
+ * frames.
+ *
+ * Possible values:
+ * "60", to set the limit of frames to 60
+ */
+ static final String PROFILE_MAXFRAMES_PROPERTY = "debug.hwui.profile.maxframes";
+
+ /**
+ * System property used to debug EGL configuration choice.
+ *
+ * Possible values:
+ * "choice", print the chosen configuration only
+ * "all", print all possible configurations
+ */
+ static final String PRINT_CONFIG_PROPERTY = "debug.hwui.print_config";
+
+ /**
+ * Turn on to draw dirty regions every other frame.
+ *
+ * Possible values:
+ * "true", to enable dirty regions debugging
+ * "false", to disable dirty regions debugging
+ *
+ * @hide
+ */
+ public static final String DEBUG_DIRTY_REGIONS_PROPERTY = "debug.hwui.show_dirty_regions";
+
+ /**
+ * Turn on to flash hardware layers when they update.
+ *
+ * Possible values:
+ * "true", to enable hardware layers updates debugging
+ * "false", to disable hardware layers updates debugging
+ *
+ * @hide
+ */
+ public static final String DEBUG_SHOW_LAYERS_UPDATES_PROPERTY =
+ "debug.hwui.show_layers_updates";
+
+ /**
+ * Controls overdraw debugging.
+ *
+ * Possible values:
+ * "false", to disable overdraw debugging
+ * "show", to show overdraw areas on screen
+ * "count", to display an overdraw counter
+ *
+ * @hide
+ */
+ public static final String DEBUG_OVERDRAW_PROPERTY = "debug.hwui.overdraw";
+
+ /**
+ * Value for {@link #DEBUG_OVERDRAW_PROPERTY}. When the property is set to this
+ * value, overdraw will be shown on screen by coloring pixels.
+ *
+ * @hide
+ */
+ public static final String OVERDRAW_PROPERTY_SHOW = "show";
+
+ /**
+ * Turn on to debug non-rectangular clip operations.
+ *
+ * Possible values:
+ * "hide", to disable this debug mode
+ * "highlight", highlight drawing commands tested against a non-rectangular clip
+ * "stencil", renders the clip region on screen when set
+ *
+ * @hide
+ */
+ public static final String DEBUG_SHOW_NON_RECTANGULAR_CLIP_PROPERTY =
+ "debug.hwui.show_non_rect_clip";
+
+ /**
+ * Sets the FPS devisor to lower the FPS.
+ *
+ * Sets a positive integer as a divisor. 1 (the default value) menas the full FPS, and 2
+ * means half the full FPS.
+ *
+ *
+ * @hide
+ */
+ public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor";
+
+ /**
+ * Forces smart-dark to be always on.
+ * @hide
+ */
+ public static final String DEBUG_FORCE_DARK = "debug.hwui.force_dark";
+
+ public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101;
+ public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102;
+ public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103;
+
+ /**
+ * Further threaded renderer disabling for the system process.
+ *
+ * @hide
+ */
+ public static boolean sRendererEnabled = true;
+
+ public static boolean sTrimForeground = false;
+
+ /**
+ * Controls whether or not the renderer should aggressively trim
+ * memory. Note that this must not be set for any process that uses
+ * WebView! This should be only used by system_process or similar
+ * that do not go into the background.
+ */
+ public static void enableForegroundTrimming() {
+ sTrimForeground = true;
+ }
+
+ /**
+ * Initialize HWUI for being in a system process like system_server
+ * Should not be called in non-system processes
+ */
+ public static void initForSystemProcess() {
+ // The system process on low-memory devices do not get to use hardware
+ // accelerated drawing, since this can add too much overhead to the
+ // process.
+ if (!ActivityManager.isHighEndGfx()) {
+ sRendererEnabled = false;
+ } else {
+ enableForegroundTrimming();
+ }
+ }
+
+ /**
+ * Creates a threaded renderer using OpenGL.
+ *
+ * @param translucent True if the surface is translucent, false otherwise
+ *
+ * @return A threaded renderer backed by OpenGL.
+ */
+ public static ThreadedRenderer create(Context context, boolean translucent, String name) {
+ return new ThreadedRenderer(context, translucent, name);
+ }
+
+ private static final String[] VISUALIZERS = {
+ PROFILE_PROPERTY_VISUALIZE_BARS,
+ };
+
+ // Size of the rendered content.
+ private int mWidth, mHeight;
+
+ // Actual size of the drawing surface.
+ private int mSurfaceWidth, mSurfaceHeight;
+
+ // Insets between the drawing surface and rendered content. These are
+ // applied as translation when updating the root render node.
+ private int mInsetTop, mInsetLeft;
+
+ // Light properties specified by the theme.
+ private final float mLightY;
+ private final float mLightZ;
+ private final float mLightRadius;
+
+ private boolean mInitialized = false;
+ private boolean mRootNodeNeedsUpdate;
+
+ private boolean mEnabled;
+ private boolean mRequested = true;
+
+ @Nullable
+ private ArrayList<FrameDrawingCallback> mNextRtFrameCallbacks;
+
+ ThreadedRenderer(Context context, boolean translucent, String name) {
+ super();
+ setName(name);
+ setOpaque(!translucent);
+
+ final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
+ mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
+ mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
+ mLightRadius = a.getDimension(R.styleable.Lighting_lightRadius, 0);
+ float ambientShadowAlpha = a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0);
+ float spotShadowAlpha = a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0);
+ a.recycle();
+ setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha);
+ }
+
+ @Override
+ public void destroy() {
+ mInitialized = false;
+ updateEnabledState(null);
+ super.destroy();
+ }
+
+ /**
+ * Indicates whether threaded rendering is currently enabled.
+ *
+ * @return True if threaded rendering is in use, false otherwise.
+ */
+ boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Indicates whether threaded rendering is currently enabled.
+ *
+ * @param enabled True if the threaded renderer is in use, false otherwise.
+ */
+ void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Indicates whether threaded rendering is currently request but not
+ * necessarily enabled yet.
+ *
+ * @return True if requested, false otherwise.
+ */
+ boolean isRequested() {
+ return mRequested;
+ }
+
+ /**
+ * Indicates whether threaded rendering is currently requested but not
+ * necessarily enabled yet.
+ */
+ void setRequested(boolean requested) {
+ mRequested = requested;
+ }
+
+ private void updateEnabledState(Surface surface) {
+ if (surface == null || !surface.isValid()) {
+ setEnabled(false);
+ } else {
+ setEnabled(mInitialized);
+ }
+ }
+
+ /**
+ * Initializes the threaded renderer for the specified surface.
+ *
+ * @param surface The surface to render
+ *
+ * @return True if the initialization was successful, false otherwise.
+ */
+ boolean initialize(Surface surface) throws OutOfResourcesException {
+ boolean status = !mInitialized;
+ mInitialized = true;
+ updateEnabledState(surface);
+ setSurface(surface);
+ return status;
+ }
+
+ /**
+ * Initializes the threaded renderer for the specified surface and setup the
+ * renderer for drawing, if needed. This is invoked when the ViewAncestor has
+ * potentially lost the threaded renderer. The threaded renderer should be
+ * reinitialized and setup when the render {@link #isRequested()} and
+ * {@link #isEnabled()}.
+ *
+ * @param width The width of the drawing surface.
+ * @param height The height of the drawing surface.
+ * @param attachInfo Information about the window.
+ * @param surface The surface to render
+ * @param surfaceInsets The drawing surface insets to apply
+ *
+ * @return true if the surface was initialized, false otherwise. Returning
+ * false might mean that the surface was already initialized.
+ */
+ boolean initializeIfNeeded(int width, int height, View.AttachInfo attachInfo,
+ Surface surface, Rect surfaceInsets) throws OutOfResourcesException {
+ if (isRequested()) {
+ // We lost the gl context, so recreate it.
+ if (!isEnabled()) {
+ if (initialize(surface)) {
+ setup(width, height, attachInfo, surfaceInsets);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Updates the threaded renderer for the specified surface.
+ *
+ * @param surface The surface to render
+ */
+ void updateSurface(Surface surface) throws OutOfResourcesException {
+ updateEnabledState(surface);
+ setSurface(surface);
+ }
+
+ @Override
+ public void setSurface(Surface surface) {
+ // TODO: Do we ever pass a non-null but isValid() = false surface?
+ // This is here to be super conservative for ViewRootImpl
+ if (surface != null && surface.isValid()) {
+ super.setSurface(surface);
+ } else {
+ super.setSurface(null);
+ }
+ }
+
+ /**
+ * Registers a callback to be executed when the next frame is being drawn on RenderThread. This
+ * callback will be executed on a RenderThread worker thread, and only used for the next frame
+ * and thus it will only fire once.
+ *
+ * @param callback The callback to register.
+ */
+ void registerRtFrameCallback(@NonNull FrameDrawingCallback callback) {
+ if (mNextRtFrameCallbacks == null) {
+ mNextRtFrameCallbacks = new ArrayList<>();
+ }
+ mNextRtFrameCallbacks.add(callback);
+ }
+
+ /**
+ * Destroys all hardware rendering resources associated with the specified
+ * view hierarchy.
+ *
+ * @param view The root of the view hierarchy
+ */
+ void destroyHardwareResources(View view) {
+ destroyResources(view);
+ clearContent();
+ }
+
+ private static void destroyResources(View view) {
+ view.destroyHardwareResources();
+ }
+
+ /**
+ * Sets up the renderer for drawing.
+ *
+ * @param width The width of the drawing surface.
+ * @param height The height of the drawing surface.
+ * @param attachInfo Information about the window.
+ * @param surfaceInsets The drawing surface insets to apply
+ */
+ void setup(int width, int height, AttachInfo attachInfo, Rect surfaceInsets) {
+ mWidth = width;
+ mHeight = height;
+
+ if (surfaceInsets != null && (surfaceInsets.left != 0 || surfaceInsets.right != 0
+ || surfaceInsets.top != 0 || surfaceInsets.bottom != 0)) {
+ mInsetLeft = surfaceInsets.left;
+ mInsetTop = surfaceInsets.top;
+ mSurfaceWidth = width + mInsetLeft + surfaceInsets.right;
+ mSurfaceHeight = height + mInsetTop + surfaceInsets.bottom;
+
+ // If the surface has insets, it can't be opaque.
+ setOpaque(false);
+ } else {
+ mInsetLeft = 0;
+ mInsetTop = 0;
+ mSurfaceWidth = width;
+ mSurfaceHeight = height;
+ }
+
+ mRootNode.setLeftTopRightBottom(-mInsetLeft, -mInsetTop, mSurfaceWidth, mSurfaceHeight);
+
+ setLightCenter(attachInfo);
+ }
+
+ /**
+ * Updates the light position based on the position of the window.
+ *
+ * @param attachInfo Information about the window.
+ */
+ void setLightCenter(AttachInfo attachInfo) {
+ // Adjust light position for window offsets.
+ final Point displaySize = attachInfo.mPoint;
+ attachInfo.mDisplay.getRealSize(displaySize);
+ final float lightX = displaySize.x / 2f - attachInfo.mWindowLeft;
+ final float lightY = mLightY - attachInfo.mWindowTop;
+ setLightSourceGeometry(lightX, lightY, mLightZ, mLightRadius);
+ }
+
+ /**
+ * Gets the current width of the surface. This is the width that the surface
+ * was last set to in a call to {@link #setup(int, int, View.AttachInfo, Rect)}.
+ *
+ * @return the current width of the surface
+ */
+ int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Gets the current height of the surface. This is the height that the surface
+ * was last set to in a call to {@link #setup(int, int, View.AttachInfo, Rect)}.
+ *
+ * @return the current width of the surface
+ */
+ int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Outputs extra debugging information in the specified file descriptor.
+ */
+ void dumpGfxInfo(PrintWriter pw, FileDescriptor fd, String[] args) {
+ pw.flush();
+ // If there's no arguments, eg 'dumpsys gfxinfo', then dump everything.
+ // If there's a targetted package, eg 'dumpsys gfxinfo com.android.systemui', then only
+ // dump the summary information
+ int flags = (args == null || args.length == 0) ? FLAG_DUMP_ALL : 0;
+ for (int i = 0; i < args.length; i++) {
+ switch (args[i]) {
+ case "framestats":
+ flags |= FLAG_DUMP_FRAMESTATS;
+ break;
+ case "reset":
+ flags |= FLAG_DUMP_RESET;
+ break;
+ case "-a": // magic option passed when dumping a bugreport.
+ flags = FLAG_DUMP_ALL;
+ break;
+ }
+ }
+ dumpProfileInfo(fd, flags);
+ }
+
+ Picture captureRenderingCommands() {
+ return null;
+ }
+
+ @Override
+ public boolean loadSystemProperties() {
+ boolean changed = super.loadSystemProperties();
+ if (changed) {
+ invalidateRoot();
+ }
+ return changed;
+ }
+
+ private void updateViewTreeDisplayList(View view) {
+ view.mPrivateFlags |= View.PFLAG_DRAWN;
+ view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
+ == View.PFLAG_INVALIDATED;
+ view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
+ view.updateDisplayListIfDirty();
+ view.mRecreateDisplayList = false;
+ }
+
+ private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
+ updateViewTreeDisplayList(view);
+
+ // Consume and set the frame callback after we dispatch draw to the view above, but before
+ // onPostDraw below which may reset the callback for the next frame. This ensures that
+ // updates to the frame callback during scroll handling will also apply in this frame.
+ if (mNextRtFrameCallbacks != null) {
+ final ArrayList<FrameDrawingCallback> frameCallbacks = mNextRtFrameCallbacks;
+ mNextRtFrameCallbacks = null;
+ setFrameCallback(frame -> {
+ for (int i = 0; i < frameCallbacks.size(); ++i) {
+ frameCallbacks.get(i).onFrameDraw(frame);
+ }
+ });
+ }
+
+ if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
+ RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
+ try {
+ final int saveCount = canvas.save();
+ canvas.translate(mInsetLeft, mInsetTop);
+ callbacks.onPreDraw(canvas);
+
+ canvas.enableZ();
+ canvas.drawRenderNode(view.updateDisplayListIfDirty());
+ canvas.disableZ();
+
+ callbacks.onPostDraw(canvas);
+ canvas.restoreToCount(saveCount);
+ mRootNodeNeedsUpdate = false;
+ } finally {
+ mRootNode.endRecording();
+ }
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ /**
+ * Interface used to receive callbacks whenever a view is drawn by
+ * a threaded renderer instance.
+ */
+ interface DrawCallbacks {
+ /**
+ * Invoked before a view is drawn by a threaded renderer.
+ * This method can be used to apply transformations to the
+ * canvas but no drawing command should be issued.
+ *
+ * @param canvas The Canvas used to render the view.
+ */
+ void onPreDraw(RecordingCanvas canvas);
+
+ /**
+ * Invoked after a view is drawn by a threaded renderer.
+ * It is safe to invoke drawing commands from this method.
+ *
+ * @param canvas The Canvas used to render the view.
+ */
+ void onPostDraw(RecordingCanvas canvas);
+ }
+
+ /**
+ * Indicates that the content drawn by DrawCallbacks needs to
+ * be updated, which will be done by the next call to draw()
+ */
+ void invalidateRoot() {
+ mRootNodeNeedsUpdate = true;
+ }
+
+ /**
+ * Draws the specified view.
+ *
+ * @param view The view to draw.
+ * @param attachInfo AttachInfo tied to the specified view.
+ */
+ void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
+ attachInfo.mViewRootImpl.mViewFrameInfo.markDrawStart();
+
+ updateRootDisplayList(view, callbacks);
+
+ // register animating rendernodes which started animating prior to renderer
+ // creation, which is typical for animators started prior to first draw
+ if (attachInfo.mPendingAnimatingRenderNodes != null) {
+ final int count = attachInfo.mPendingAnimatingRenderNodes.size();
+ for (int i = 0; i < count; i++) {
+ registerAnimatingRenderNode(
+ attachInfo.mPendingAnimatingRenderNodes.get(i));
+ }
+ attachInfo.mPendingAnimatingRenderNodes.clear();
+ // We don't need this anymore as subsequent calls to
+ // ViewRootImpl#attachRenderNodeAnimator will go directly to us.
+ attachInfo.mPendingAnimatingRenderNodes = null;
+ }
+
+ final FrameInfo frameInfo = attachInfo.mViewRootImpl.getUpdatedFrameInfo();
+
+ int syncResult = syncAndDrawFrame(frameInfo);
+ if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
+ Log.w("OpenGLRenderer", "Surface lost, forcing relayout");
+ // We lost our surface. For a relayout next frame which should give us a new
+ // surface from WindowManager, which hopefully will work.
+ attachInfo.mViewRootImpl.mForceNextWindowRelayout = true;
+ attachInfo.mViewRootImpl.requestLayout();
+ }
+ if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
+ attachInfo.mViewRootImpl.invalidate();
+ }
+ }
+
+ /** The root of everything */
+ public @NonNull RenderNode getRootNode() {
+ return mRootNode;
+ }
+
+ /**
+ * Basic synchronous renderer. Currently only used to render the Magnifier, so use with care.
+ * TODO: deduplicate against ThreadedRenderer.
+ *
+ * @hide
+ */
+ public static class SimpleRenderer extends HardwareRenderer {
+ private final float mLightY, mLightZ, mLightRadius;
+
+ public SimpleRenderer(final Context context, final String name, final Surface surface) {
+ super();
+ setName(name);
+ setOpaque(false);
+ setSurface(surface);
+ final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
+ mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
+ mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
+ mLightRadius = a.getDimension(R.styleable.Lighting_lightRadius, 0);
+ final float ambientShadowAlpha = a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0);
+ final float spotShadowAlpha = a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0);
+ a.recycle();
+ setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha);
+ }
+
+ /**
+ * Set the light center.
+ */
+ public void setLightCenter(final Display display,
+ final int windowLeft, final int windowTop) {
+ // Adjust light position for window offsets.
+ final Point displaySize = new Point();
+ display.getRealSize(displaySize);
+ final float lightX = displaySize.x / 2f - windowLeft;
+ final float lightY = mLightY - windowTop;
+
+ setLightSourceGeometry(lightX, lightY, mLightZ, mLightRadius);
+ }
+
+ public RenderNode getRootNode() {
+ return mRootNode;
+ }
+
+ /**
+ * Draw the surface.
+ */
+ public void draw(final FrameDrawingCallback callback) {
+ final long vsync = AnimationUtils.currentAnimationTimeMillis() * 1000000L;
+ if (callback != null) {
+ setFrameCallback(callback);
+ }
+ createRenderRequest()
+ .setVsyncTime(vsync)
+ .syncAndDraw();
+ }
+ }
+}
diff --git a/android/view/TouchDelegate.java b/android/view/TouchDelegate.java
new file mode 100644
index 0000000..de0f9e5
--- /dev/null
+++ b/android/view/TouchDelegate.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.ArrayMap;
+import android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo;
+
+/**
+ * Helper class to handle situations where you want a view to have a larger touch area than its
+ * actual view bounds. The view whose touch area is changed is called the delegate view. This
+ * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
+ * instance that specifies the bounds that should be mapped to the delegate and the delegate
+ * view itself.
+ * <p>
+ * The ancestor should then forward all of its touch events received in its
+ * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+public class TouchDelegate {
+
+ /**
+ * View that should receive forwarded touch events
+ */
+ private View mDelegateView;
+
+ /**
+ * Bounds in local coordinates of the containing view that should be mapped to the delegate
+ * view. This rect is used for initial hit testing.
+ */
+ private Rect mBounds;
+
+ /**
+ * mBounds inflated to include some slop. This rect is to track whether the motion events
+ * should be considered to be within the delegate view.
+ */
+ private Rect mSlopBounds;
+
+ /**
+ * True if the delegate had been targeted on a down event (intersected mBounds).
+ */
+ @UnsupportedAppUsage
+ private boolean mDelegateTargeted;
+
+ /**
+ * The touchable region of the View extends above its actual extent.
+ */
+ public static final int ABOVE = 1;
+
+ /**
+ * The touchable region of the View extends below its actual extent.
+ */
+ public static final int BELOW = 2;
+
+ /**
+ * The touchable region of the View extends to the left of its actual extent.
+ */
+ public static final int TO_LEFT = 4;
+
+ /**
+ * The touchable region of the View extends to the right of its actual extent.
+ */
+ public static final int TO_RIGHT = 8;
+
+ private int mSlop;
+
+ /**
+ * Touch delegate information for accessibility
+ */
+ private TouchDelegateInfo mTouchDelegateInfo;
+
+ /**
+ * Constructor
+ *
+ * @param bounds Bounds in local coordinates of the containing view that should be mapped to
+ * the delegate view
+ * @param delegateView The view that should receive motion events
+ */
+ public TouchDelegate(Rect bounds, View delegateView) {
+ mBounds = bounds;
+
+ mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
+ mSlopBounds = new Rect(bounds);
+ mSlopBounds.inset(-mSlop, -mSlop);
+ mDelegateView = delegateView;
+ }
+
+ /**
+ * Forward touch events to the delegate view if the event is within the bounds
+ * specified in the constructor.
+ *
+ * @param event The touch event to forward
+ * @return True if the event was consumed by the delegate, false otherwise.
+ */
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ int x = (int)event.getX();
+ int y = (int)event.getY();
+ boolean sendToDelegate = false;
+ boolean hit = true;
+ boolean handled = false;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mDelegateTargeted = mBounds.contains(x, y);
+ sendToDelegate = mDelegateTargeted;
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_MOVE:
+ sendToDelegate = mDelegateTargeted;
+ if (sendToDelegate) {
+ Rect slopBounds = mSlopBounds;
+ if (!slopBounds.contains(x, y)) {
+ hit = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ sendToDelegate = mDelegateTargeted;
+ mDelegateTargeted = false;
+ break;
+ }
+ if (sendToDelegate) {
+ if (hit) {
+ // Offset event coordinates to be inside the target view
+ event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
+ } else {
+ // Offset event coordinates to be outside the target view (in case it does
+ // something like tracking pressed state)
+ int slop = mSlop;
+ event.setLocation(-(slop * 2), -(slop * 2));
+ }
+ handled = mDelegateView.dispatchTouchEvent(event);
+ }
+ return handled;
+ }
+
+ /**
+ * Forward hover events to the delegate view if the event is within the bounds
+ * specified in the constructor and touch exploration is enabled.
+ *
+ * <p>This method is provided for accessibility purposes so touch exploration, which is
+ * commonly used by screen readers, can properly place accessibility focus on views that
+ * use touch delegates. Therefore, touch exploration must be enabled for hover events
+ * to be dispatched through the delegate.</p>
+ *
+ * @param event The hover event to forward
+ * @return True if the event was consumed by the delegate, false otherwise.
+ *
+ * @see android.view.accessibility.AccessibilityManager#isTouchExplorationEnabled
+ */
+ public boolean onTouchExplorationHoverEvent(@NonNull MotionEvent event) {
+ if (mBounds == null) {
+ return false;
+ }
+
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ boolean hit = true;
+ boolean handled = false;
+
+ final boolean isInbound = mBounds.contains(x, y);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ mDelegateTargeted = isInbound;
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if (isInbound) {
+ mDelegateTargeted = true;
+ } else {
+ // delegated previously
+ if (mDelegateTargeted && !mSlopBounds.contains(x, y)) {
+ hit = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ mDelegateTargeted = true;
+ break;
+ }
+ if (mDelegateTargeted) {
+ if (hit) {
+ event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
+ } else {
+ mDelegateTargeted = false;
+ }
+ handled = mDelegateView.dispatchHoverEvent(event);
+ }
+ return handled;
+ }
+
+ /**
+ * Return a {@link TouchDelegateInfo} mapping from regions (in view coordinates) to
+ * delegated views for accessibility usage.
+ *
+ * @return A TouchDelegateInfo.
+ */
+ @NonNull
+ public TouchDelegateInfo getTouchDelegateInfo() {
+ if (mTouchDelegateInfo == null) {
+ final ArrayMap<Region, View> targetMap = new ArrayMap<>(1);
+ Rect bounds = mBounds;
+ if (bounds == null) {
+ bounds = new Rect();
+ }
+ targetMap.put(new Region(bounds), mDelegateView);
+ mTouchDelegateInfo = new TouchDelegateInfo(targetMap);
+ }
+ return mTouchDelegateInfo;
+ }
+}
diff --git a/android/view/TunnelModeEnabledListener.java b/android/view/TunnelModeEnabledListener.java
new file mode 100644
index 0000000..c158da9
--- /dev/null
+++ b/android/view/TunnelModeEnabledListener.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Listens for tunnel mode enabled/disabled events from SurfaceFlinger.
+ * {@hide}
+ */
+public abstract class TunnelModeEnabledListener {
+
+ private long mNativeListener;
+ private final Executor mExecutor;
+
+ public TunnelModeEnabledListener(Executor executor) {
+ mExecutor = executor;
+ mNativeListener = nativeCreate(this);
+ }
+
+ /**
+ * Destroys the listener.
+ */
+ public void destroy() {
+ if (mNativeListener == 0) {
+ return;
+ }
+ unregister(this);
+ nativeDestroy(mNativeListener);
+ mNativeListener = 0;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Reports when tunnel mode has been enabled/disabled.
+ */
+ public abstract void onTunnelModeEnabledChanged(boolean tunnelModeEnabled);
+
+ /**
+ * Registers a listener.
+ */
+ public static void register(TunnelModeEnabledListener listener) {
+ if (listener.mNativeListener == 0) {
+ return;
+ }
+ nativeRegister(listener.mNativeListener);
+ }
+
+ /**
+ * Unregisters a listener.
+ */
+ public static void unregister(TunnelModeEnabledListener listener) {
+ if (listener.mNativeListener == 0) {
+ return;
+ }
+ nativeUnregister(listener.mNativeListener);
+ }
+
+ /**
+ * Dispatch tunnel mode enabled.
+ *
+ * Called from native code on a binder thread.
+ */
+ @VisibleForTesting
+ public static void dispatchOnTunnelModeEnabledChanged(TunnelModeEnabledListener listener,
+ boolean tunnelModeEnabled) {
+ listener.mExecutor.execute(() -> listener.onTunnelModeEnabledChanged(tunnelModeEnabled));
+ }
+
+ private static native long nativeCreate(TunnelModeEnabledListener thiz);
+ private static native void nativeDestroy(long ptr);
+ private static native void nativeRegister(long ptr);
+ private static native void nativeUnregister(long ptr);
+}
diff --git a/android/view/VelocityTracker.java b/android/view/VelocityTracker.java
new file mode 100644
index 0000000..e1c4305
--- /dev/null
+++ b/android/view/VelocityTracker.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.ArrayMap;
+import android.util.Pools.SynchronizedPool;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+
+/**
+ * Helper for tracking the velocity of touch events, for implementing
+ * flinging and other such gestures.
+ *
+ * Use {@link #obtain} to retrieve a new instance of the class when you are going
+ * to begin tracking. Put the motion events you receive into it with
+ * {@link #addMovement(MotionEvent)}. When you want to determine the velocity call
+ * {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)}
+ * and {@link #getYVelocity(int)} to retrieve the velocity for each pointer id.
+ */
+public final class VelocityTracker {
+ private static final SynchronizedPool<VelocityTracker> sPool =
+ new SynchronizedPool<VelocityTracker>(2);
+
+ private static final int ACTIVE_POINTER_ID = -1;
+
+ /**
+ * Velocity Tracker Strategy: Invalid.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_DEFAULT = -1;
+
+ /**
+ * Velocity Tracker Strategy: Impulse.
+ * Physical model of pushing an object. Quality: VERY GOOD.
+ * Works with duplicate coordinates, unclean finger liftoff.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_IMPULSE = 0;
+
+ /**
+ * Velocity Tracker Strategy: LSQ1.
+ * 1st order least squares. Quality: POOR.
+ * Frequently underfits the touch data especially when the finger accelerates
+ * or changes direction. Often underestimates velocity. The direction
+ * is overly influenced by historical touch points.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_LSQ1 = 1;
+
+ /**
+ * Velocity Tracker Strategy: LSQ2.
+ * 2nd order least squares. Quality: VERY GOOD.
+ * Pretty much ideal, but can be confused by certain kinds of touch data,
+ * particularly if the panel has a tendency to generate delayed,
+ * duplicate or jittery touch coordinates when the finger is released.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_LSQ2 = 2;
+
+ /**
+ * Velocity Tracker Strategy: LSQ3.
+ * 3rd order least squares. Quality: UNUSABLE.
+ * Frequently overfits the touch data yielding wildly divergent estimates
+ * of the velocity when the finger is released.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_LSQ3 = 3;
+
+ /**
+ * Velocity Tracker Strategy: WLSQ2_DELTA.
+ * 2nd order weighted least squares, delta weighting. Quality: EXPERIMENTAL
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_WLSQ2_DELTA = 4;
+
+ /**
+ * Velocity Tracker Strategy: WLSQ2_CENTRAL.
+ * 2nd order weighted least squares, central weighting. Quality: EXPERIMENTAL
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_WLSQ2_CENTRAL = 5;
+
+ /**
+ * Velocity Tracker Strategy: WLSQ2_RECENT.
+ * 2nd order weighted least squares, recent weighting. Quality: EXPERIMENTAL
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_WLSQ2_RECENT = 6;
+
+ /**
+ * Velocity Tracker Strategy: INT1.
+ * 1st order integrating filter. Quality: GOOD.
+ * Not as good as 'lsq2' because it cannot estimate acceleration but it is
+ * more tolerant of errors. Like 'lsq1', this strategy tends to underestimate
+ * the velocity of a fling but this strategy tends to respond to changes in
+ * direction more quickly and accurately.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_INT1 = 7;
+
+ /**
+ * Velocity Tracker Strategy: INT2.
+ * 2nd order integrating filter. Quality: EXPERIMENTAL.
+ * For comparison purposes only. Unlike 'int1' this strategy can compensate
+ * for acceleration but it typically overestimates the effect.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_INT2 = 8;
+
+ /**
+ * Velocity Tracker Strategy: Legacy.
+ * Legacy velocity tracker algorithm. Quality: POOR.
+ * For comparison purposes only. This algorithm is strongly influenced by
+ * old data points, consistently underestimates velocity and takes a very long
+ * time to adjust to changes in direction.
+ *
+ * @hide
+ */
+ public static final int VELOCITY_TRACKER_STRATEGY_LEGACY = 9;
+
+
+ /**
+ * Velocity Tracker Strategy look up table.
+ */
+ private static final Map<String, Integer> STRATEGIES = new ArrayMap<>();
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"VELOCITY_TRACKER_STRATEGY_"}, value = {
+ VELOCITY_TRACKER_STRATEGY_DEFAULT,
+ VELOCITY_TRACKER_STRATEGY_IMPULSE,
+ VELOCITY_TRACKER_STRATEGY_LSQ1,
+ VELOCITY_TRACKER_STRATEGY_LSQ2,
+ VELOCITY_TRACKER_STRATEGY_LSQ3,
+ VELOCITY_TRACKER_STRATEGY_WLSQ2_DELTA,
+ VELOCITY_TRACKER_STRATEGY_WLSQ2_CENTRAL,
+ VELOCITY_TRACKER_STRATEGY_WLSQ2_RECENT,
+ VELOCITY_TRACKER_STRATEGY_INT1,
+ VELOCITY_TRACKER_STRATEGY_INT2,
+ VELOCITY_TRACKER_STRATEGY_LEGACY
+ })
+ public @interface VelocityTrackerStrategy {}
+
+ private long mPtr;
+ @VelocityTrackerStrategy
+ private final int mStrategy;
+
+ private static native long nativeInitialize(int strategy);
+ private static native void nativeDispose(long ptr);
+ private static native void nativeClear(long ptr);
+ private static native void nativeAddMovement(long ptr, MotionEvent event);
+ private static native void nativeComputeCurrentVelocity(long ptr, int units, float maxVelocity);
+ private static native float nativeGetXVelocity(long ptr, int id);
+ private static native float nativeGetYVelocity(long ptr, int id);
+ private static native boolean nativeGetEstimator(long ptr, int id, Estimator outEstimator);
+
+ static {
+ // Strategy string and IDs mapping lookup.
+ STRATEGIES.put("impulse", VELOCITY_TRACKER_STRATEGY_IMPULSE);
+ STRATEGIES.put("lsq1", VELOCITY_TRACKER_STRATEGY_LSQ1);
+ STRATEGIES.put("lsq2", VELOCITY_TRACKER_STRATEGY_LSQ2);
+ STRATEGIES.put("lsq3", VELOCITY_TRACKER_STRATEGY_LSQ3);
+ STRATEGIES.put("wlsq2-delta", VELOCITY_TRACKER_STRATEGY_WLSQ2_DELTA);
+ STRATEGIES.put("wlsq2-central", VELOCITY_TRACKER_STRATEGY_WLSQ2_CENTRAL);
+ STRATEGIES.put("wlsq2-recent", VELOCITY_TRACKER_STRATEGY_WLSQ2_RECENT);
+ STRATEGIES.put("int1", VELOCITY_TRACKER_STRATEGY_INT1);
+ STRATEGIES.put("int2", VELOCITY_TRACKER_STRATEGY_INT2);
+ STRATEGIES.put("legacy", VELOCITY_TRACKER_STRATEGY_LEGACY);
+ }
+
+ /**
+ * Return a strategy ID from string.
+ */
+ private static int toStrategyId(String strStrategy) {
+ if (STRATEGIES.containsKey(strStrategy)) {
+ return STRATEGIES.get(strStrategy);
+ }
+ return VELOCITY_TRACKER_STRATEGY_DEFAULT;
+ }
+
+ /**
+ * Retrieve a new VelocityTracker object to watch the velocity of a
+ * motion. Be sure to call {@link #recycle} when done. You should
+ * generally only maintain an active object while tracking a movement,
+ * so that the VelocityTracker can be re-used elsewhere.
+ *
+ * @return Returns a new VelocityTracker.
+ */
+ static public VelocityTracker obtain() {
+ VelocityTracker instance = sPool.acquire();
+ return (instance != null) ? instance
+ : new VelocityTracker(VELOCITY_TRACKER_STRATEGY_DEFAULT);
+ }
+
+ /**
+ * Obtains a velocity tracker with the specified strategy as string.
+ * For testing and comparison purposes only.
+ * @deprecated Use {@link obtain(int strategy)} instead.
+ *
+ * @param strategy The strategy, or null to use the default.
+ * @return The velocity tracker.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @Deprecated
+ public static VelocityTracker obtain(String strategy) {
+ if (strategy == null) {
+ return obtain();
+ }
+ return new VelocityTracker(toStrategyId(strategy));
+ }
+
+ /**
+ * Obtains a velocity tracker with the specified strategy.
+ * For testing and comparison purposes only.
+ *
+ * @param strategy The strategy Id, VELOCITY_TRACKER_STRATEGY_DEFAULT to use the default.
+ * @return The velocity tracker.
+ *
+ * @hide
+ */
+ public static VelocityTracker obtain(int strategy) {
+ return new VelocityTracker(strategy);
+ }
+
+ /**
+ * Return a VelocityTracker object back to be re-used by others. You must
+ * not touch the object after calling this function.
+ */
+ public void recycle() {
+ if (mStrategy == VELOCITY_TRACKER_STRATEGY_DEFAULT) {
+ clear();
+ sPool.release(this);
+ }
+ }
+
+ /**
+ * Return strategy Id of VelocityTracker object.
+ * @return The velocity tracker strategy Id.
+ *
+ * @hide
+ */
+ public int getStrategyId() {
+ return mStrategy;
+ }
+
+ private VelocityTracker(@VelocityTrackerStrategy int strategy) {
+ // If user has not selected a specific strategy
+ if (strategy == VELOCITY_TRACKER_STRATEGY_DEFAULT) {
+ // Check if user specified strategy by overriding system property.
+ String strategyProperty =
+ SystemProperties.get("persist.input.velocitytracker.strategy");
+ if (strategyProperty == null || strategyProperty.isEmpty()) {
+ mStrategy = strategy;
+ } else {
+ mStrategy = toStrategyId(strategyProperty);
+ }
+ } else {
+ // User specified strategy
+ mStrategy = strategy;
+ }
+ mPtr = nativeInitialize(mStrategy);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mPtr != 0) {
+ nativeDispose(mPtr);
+ mPtr = 0;
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Reset the velocity tracker back to its initial state.
+ */
+ public void clear() {
+ nativeClear(mPtr);
+ }
+
+ /**
+ * Add a user's movement to the tracker. You should call this for the
+ * initial {@link MotionEvent#ACTION_DOWN}, the following
+ * {@link MotionEvent#ACTION_MOVE} events that you receive, and the
+ * final {@link MotionEvent#ACTION_UP}. You can, however, call this
+ * for whichever events you desire.
+ *
+ * @param event The MotionEvent you received and would like to track.
+ */
+ public void addMovement(MotionEvent event) {
+ if (event == null) {
+ throw new IllegalArgumentException("event must not be null");
+ }
+ nativeAddMovement(mPtr, event);
+ }
+
+ /**
+ * Equivalent to invoking {@link #computeCurrentVelocity(int, float)} with a maximum
+ * velocity of Float.MAX_VALUE.
+ *
+ * @see #computeCurrentVelocity(int, float)
+ */
+ public void computeCurrentVelocity(int units) {
+ nativeComputeCurrentVelocity(mPtr, units, Float.MAX_VALUE);
+ }
+
+ /**
+ * Compute the current velocity based on the points that have been
+ * collected. Only call this when you actually want to retrieve velocity
+ * information, as it is relatively expensive. You can then retrieve
+ * the velocity with {@link #getXVelocity()} and
+ * {@link #getYVelocity()}.
+ *
+ * @param units The units you would like the velocity in. A value of 1
+ * provides pixels per millisecond, 1000 provides pixels per second, etc.
+ * @param maxVelocity The maximum velocity that can be computed by this method.
+ * This value must be declared in the same unit as the units parameter. This value
+ * must be positive.
+ */
+ public void computeCurrentVelocity(int units, float maxVelocity) {
+ nativeComputeCurrentVelocity(mPtr, units, maxVelocity);
+ }
+
+ /**
+ * Retrieve the last computed X velocity. You must first call
+ * {@link #computeCurrentVelocity(int)} before calling this function.
+ *
+ * @return The previously computed X velocity.
+ */
+ public float getXVelocity() {
+ return nativeGetXVelocity(mPtr, ACTIVE_POINTER_ID);
+ }
+
+ /**
+ * Retrieve the last computed Y velocity. You must first call
+ * {@link #computeCurrentVelocity(int)} before calling this function.
+ *
+ * @return The previously computed Y velocity.
+ */
+ public float getYVelocity() {
+ return nativeGetYVelocity(mPtr, ACTIVE_POINTER_ID);
+ }
+
+ /**
+ * Retrieve the last computed X velocity. You must first call
+ * {@link #computeCurrentVelocity(int)} before calling this function.
+ *
+ * @param id Which pointer's velocity to return.
+ * @return The previously computed X velocity.
+ */
+ public float getXVelocity(int id) {
+ return nativeGetXVelocity(mPtr, id);
+ }
+
+ /**
+ * Retrieve the last computed Y velocity. You must first call
+ * {@link #computeCurrentVelocity(int)} before calling this function.
+ *
+ * @param id Which pointer's velocity to return.
+ * @return The previously computed Y velocity.
+ */
+ public float getYVelocity(int id) {
+ return nativeGetYVelocity(mPtr, id);
+ }
+
+ /**
+ * Get an estimator for the movements of a pointer using past movements of the
+ * pointer to predict future movements.
+ *
+ * It is not necessary to call {@link #computeCurrentVelocity(int)} before calling
+ * this method.
+ *
+ * @param id Which pointer's velocity to return.
+ * @param outEstimator The estimator to populate.
+ * @return True if an estimator was obtained, false if there is no information
+ * available about the pointer.
+ *
+ * @hide For internal use only. Not a final API.
+ */
+ public boolean getEstimator(int id, Estimator outEstimator) {
+ if (outEstimator == null) {
+ throw new IllegalArgumentException("outEstimator must not be null");
+ }
+ return nativeGetEstimator(mPtr, id, outEstimator);
+ }
+
+ /**
+ * An estimator for the movements of a pointer based on a polynomial model.
+ *
+ * The last recorded position of the pointer is at time zero seconds.
+ * Past estimated positions are at negative times and future estimated positions
+ * are at positive times.
+ *
+ * First coefficient is position (in pixels), second is velocity (in pixels per second),
+ * third is acceleration (in pixels per second squared).
+ *
+ * @hide For internal use only. Not a final API.
+ */
+ public static final class Estimator {
+ // Must match VelocityTracker::Estimator::MAX_DEGREE
+ private static final int MAX_DEGREE = 4;
+
+ /**
+ * Polynomial coefficients describing motion in X.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final float[] xCoeff = new float[MAX_DEGREE + 1];
+
+ /**
+ * Polynomial coefficients describing motion in Y.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final float[] yCoeff = new float[MAX_DEGREE + 1];
+
+ /**
+ * Polynomial degree, or zero if only position information is available.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int degree;
+
+ /**
+ * Confidence (coefficient of determination), between 0 (no fit) and 1 (perfect fit).
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public float confidence;
+
+ /**
+ * Gets an estimate of the X position of the pointer at the specified time point.
+ * @param time The time point in seconds, 0 is the last recorded time.
+ * @return The estimated X coordinate.
+ */
+ public float estimateX(float time) {
+ return estimate(time, xCoeff);
+ }
+
+ /**
+ * Gets an estimate of the Y position of the pointer at the specified time point.
+ * @param time The time point in seconds, 0 is the last recorded time.
+ * @return The estimated Y coordinate.
+ */
+ public float estimateY(float time) {
+ return estimate(time, yCoeff);
+ }
+
+ /**
+ * Gets the X coefficient with the specified index.
+ * @param index The index of the coefficient to return.
+ * @return The X coefficient, or 0 if the index is greater than the degree.
+ */
+ public float getXCoeff(int index) {
+ return index <= degree ? xCoeff[index] : 0;
+ }
+
+ /**
+ * Gets the Y coefficient with the specified index.
+ * @param index The index of the coefficient to return.
+ * @return The Y coefficient, or 0 if the index is greater than the degree.
+ */
+ public float getYCoeff(int index) {
+ return index <= degree ? yCoeff[index] : 0;
+ }
+
+ private float estimate(float time, float[] c) {
+ float a = 0;
+ float scale = 1;
+ for (int i = 0; i <= degree; i++) {
+ a += c[i] * scale;
+ scale *= time;
+ }
+ return a;
+ }
+ }
+}
diff --git a/android/view/VerifiedInputEvent.java b/android/view/VerifiedInputEvent.java
new file mode 100644
index 0000000..cc6fbe7
--- /dev/null
+++ b/android/view/VerifiedInputEvent.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+
+
+/**
+ * Base class for verified events.
+ * Verified events contain the subset of an InputEvent that the system can verify.
+ * Data contained inside VerifiedInputEvent's should be considered trusted and contain only
+ * the original event data that first came from the system.
+ *
+ * @see android.hardware.input.InputManager#verifyInputEvent(InputEvent)
+ */
+@SuppressLint("ParcelNotFinal")
+public abstract class VerifiedInputEvent implements Parcelable {
+ private static final String TAG = "VerifiedInputEvent";
+
+ /** @hide */
+ protected static final int VERIFIED_KEY = 1;
+ /** @hide */
+ protected static final int VERIFIED_MOTION = 2;
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef(prefix = "VERIFIED", value = {VERIFIED_KEY, VERIFIED_MOTION})
+ public @interface VerifiedInputEventType {};
+
+ @VerifiedInputEventType
+ private int mType;
+
+ private int mDeviceId;
+ private long mEventTimeNanos;
+ private int mSource;
+ private int mDisplayId;
+
+ /** @hide */
+ protected VerifiedInputEvent(int type, int deviceId, long eventTimeNanos, int source,
+ int displayId) {
+ mType = type;
+ mDeviceId = deviceId;
+ mEventTimeNanos = eventTimeNanos;
+ mSource = source;
+ mDisplayId = displayId;
+ }
+ /** @hide */
+ protected VerifiedInputEvent(@NonNull Parcel in, int expectedType) {
+ mType = in.readInt();
+ if (mType != expectedType) {
+ throw new IllegalArgumentException("Unexpected input event type token in parcel.");
+ }
+ mDeviceId = in.readInt();
+ mEventTimeNanos = in.readLong();
+ mSource = in.readInt();
+ mDisplayId = in.readInt();
+ }
+
+ /**
+ * Get the id of the device that generated this event.
+ *
+ * @see InputEvent#getDeviceId()
+ */
+ public int getDeviceId() {
+ return mDeviceId;
+ }
+
+ /**
+ * Get the time this event occurred, in the {@link android.os.SystemClock#uptimeMillis()}
+ * time base.
+ *
+ * @see InputEvent#getEventTime()
+ */
+ @SuppressLint("MethodNameUnits")
+ public long getEventTimeNanos() {
+ return mEventTimeNanos;
+ }
+
+ /**
+ * Get the source of the event.
+ *
+ * @see InputEvent#getSource()
+ */
+ public int getSource() {
+ return mSource;
+ }
+
+ /**
+ * Get the display id that is associated with this event.
+ *
+ * @see Display#getDisplayId()
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mDeviceId);
+ dest.writeLong(mEventTimeNanos);
+ dest.writeInt(mSource);
+ dest.writeInt(mDisplayId);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private static int peekInt(@NonNull Parcel parcel) {
+ final int initialDataPosition = parcel.dataPosition();
+ int data = parcel.readInt();
+ parcel.setDataPosition(initialDataPosition);
+ return data;
+ }
+
+ public static final @NonNull Parcelable.Creator<VerifiedInputEvent> CREATOR =
+ new Parcelable.Creator<VerifiedInputEvent>() {
+ @Override
+ public VerifiedInputEvent[] newArray(int size) {
+ return new VerifiedInputEvent[size];
+ }
+
+ @Override
+ public VerifiedInputEvent createFromParcel(@NonNull Parcel in) {
+ final int type = peekInt(in);
+ if (type == VERIFIED_KEY) {
+ return VerifiedKeyEvent.CREATOR.createFromParcel(in);
+ } else if (type == VERIFIED_MOTION) {
+ return VerifiedMotionEvent.CREATOR.createFromParcel(in);
+ }
+ throw new IllegalArgumentException("Unexpected input event type in parcel.");
+ }
+ };
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ VerifiedInputEvent that = (VerifiedInputEvent) o;
+ return mType == that.mType
+ && getDeviceId() == that.getDeviceId()
+ && getEventTimeNanos() == that.getEventTimeNanos()
+ && getSource() == that.getSource()
+ && getDisplayId() == that.getDisplayId();
+ }
+
+ @Override
+ public int hashCode() {
+ int _hash = 1;
+ _hash = 31 * _hash + mType;
+ _hash = 31 * _hash + getDeviceId();
+ _hash = 31 * _hash + Long.hashCode(getEventTimeNanos());
+ _hash = 31 * _hash + getSource();
+ _hash = 31 * _hash + getDisplayId();
+ return _hash;
+ }
+}
diff --git a/android/view/VerifiedKeyEvent.java b/android/view/VerifiedKeyEvent.java
new file mode 100644
index 0000000..fc357cc
--- /dev/null
+++ b/android/view/VerifiedKeyEvent.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.KeyEvent.FLAG_CANCELED;
+import static android.view.KeyEvent.FLAG_IS_ACCESSIBILITY_EVENT;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+
+/**
+ * KeyEvent that has been verified by the system.
+ * The data contained in this class is always a subset of a {@link KeyEvent}. Use this class to
+ * check which data has been confirmed by the system to be authentic.
+ *
+ * Most applications do not need to use this class.
+ *
+ * {@see android.hardware.input.InputManager#verifyInputEvent}
+ */
+@DataClass(genHiddenConstructor = true, genEqualsHashCode = true)
+public final class VerifiedKeyEvent extends VerifiedInputEvent implements Parcelable {
+ private static final String TAG = "VerifiedKeyEvent";
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef({KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP})
+ public @interface KeyEventAction {};
+
+ /**
+ * The action of this key event. May be either {@link KeyEvent#ACTION_DOWN} or
+ * {@link KeyEvent#ACTION_UP}.
+ *
+ * @see KeyEvent#getAction()
+ */
+ @KeyEventAction
+ private int mAction;
+
+ /**
+ * Retrieve the time of the most recent key down event, in the
+ * {@link android.os.SystemClock#uptimeMillis} time base, but in nanoseconds. If this
+ * is a down event, this will be the same as {@link VerifiedInputEvent#getEventTimeNanos()}.
+ *
+ * @see KeyEvent#getDownTime()
+ */
+ @SuppressLint({"MethodNameUnits"})
+ private long mDownTimeNanos;
+
+ /**
+ * Returns the flags for this key event.
+ *
+ * @see KeyEvent#getFlags()
+ * @see KeyEvent#FLAG_CANCELED
+ * @see KeyEvent#FLAG_IS_ACCESSIBILITY_EVENT
+ *
+ * @hide
+ */
+ private int mFlags;
+
+ /**
+ * Retrieve the key code of the key event.
+ *
+ * @see KeyEvent#getKeyCode()
+ */
+ private int mKeyCode;
+
+ /**
+ * Retrieve the hardware key id of this key event. These values are not reliable
+ * and vary from device to device.
+ *
+ * @see KeyEvent#getScanCode()
+ */
+ private int mScanCode;
+
+ /**
+ * <p>Returns the state of the meta keys.</p>
+ *
+ * @return an integer in which each bit set to 1 represents a pressed meta key
+ * @see KeyEvent#getMetaState()
+ */
+ private int mMetaState;
+
+ /**
+ * Retrieve the repeat count of the event. For key down events, this is the number of times
+ * the key has repeated with the first down starting at 0 and counting up from there.
+ * For key up events, this is always equal to zero. For multiple key events,
+ * this is the number of down/up pairs that have occurred.
+ */
+ private int mRepeatCount;
+
+ /**
+ * Get a specific flag of this key event, if possible. Return null if the flag value could
+ * not be checked.
+ *
+ * @param flag the flag of interest
+ * @return Boolean(true) if the key event has the requested flag
+ * Boolean(false) if the key event does not have the requested flag
+ * null if the flag value could not be checked
+ *
+ * @see KeyEvent#getFlags()
+ * @see KeyEvent#FLAG_CANCELED
+ */
+ public @Nullable Boolean getFlag(int flag) {
+ switch(flag) {
+ // InputDispatcher only verifies a subset of the KeyEvent flags.
+ // These values must be kept in sync with Input.cpp
+ case FLAG_CANCELED:
+ case FLAG_IS_ACCESSIBILITY_EVENT:
+ return (mFlags & flag) != 0;
+ }
+ return null;
+ }
+
+ // The codegen tool doesn't fully support subclasses, since it works on a per-file basis.
+ // To modify this file:
+ // 1. run codegen on this file
+ // 2. edit the constructor signature
+ // 3. add the "super" call for constructor that receives a Parcel
+ // 4. add the "super" call to the writeToParcel method
+ // 5. Update "equals" and "hashcode" methods to include VerifiedInputEvent fields
+
+
+
+ // Code below generated by codegen v1.0.20.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/VerifiedKeyEvent.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new VerifiedKeyEvent.
+ *
+ * @param action
+ * The action of this key event. May be either {@link KeyEvent#ACTION_DOWN} or
+ * {@link KeyEvent#ACTION_UP}.
+ * @param downTimeNanos
+ * Retrieve the time of the most recent key down event, in the
+ * {@link android.os.SystemClock#uptimeMillis} time base, but in nanoseconds. If this
+ * is a down event, this will be the same as {@link VerifiedInputEvent#getEventTimeNanos()}.
+ * @param flags
+ * Returns the flags for this key event.
+ * @param keyCode
+ * Retrieve the key code of the key event.
+ * @param scanCode
+ * Retrieve the hardware key id of this key event. These values are not reliable
+ * and vary from device to device.
+ * @param metaState
+ * <p>Returns the state of the meta keys.</p>
+ * @param repeatCount
+ * Retrieve the repeat count of the event. For key down events, this is the number of times
+ * the key has repeated with the first down starting at 0 and counting up from there.
+ * For key up events, this is always equal to zero. For multiple key events,
+ * this is the number of down/up pairs that have occurred.
+ * @hide
+ */
+ public VerifiedKeyEvent(
+ int deviceId,
+ long eventTimeNanos,
+ int source,
+ int displayId,
+ @KeyEventAction int action,
+ @SuppressLint({ "MethodNameUnits" }) long downTimeNanos,
+ int flags,
+ int keyCode,
+ int scanCode,
+ int metaState,
+ int repeatCount) {
+ super(VERIFIED_KEY, deviceId, eventTimeNanos, source, displayId);
+ this.mAction = action;
+ com.android.internal.util.AnnotationValidations.validate(
+ KeyEventAction.class, null, mAction);
+ this.mDownTimeNanos = downTimeNanos;
+ this.mFlags = flags;
+ this.mKeyCode = keyCode;
+ this.mScanCode = scanCode;
+ this.mMetaState = metaState;
+ this.mRepeatCount = repeatCount;
+ }
+
+ /**
+ * The action of this key event. May be either {@link KeyEvent#ACTION_DOWN} or
+ * {@link KeyEvent#ACTION_UP}.
+ *
+ * @see KeyEvent#getAction()
+ */
+ @DataClass.Generated.Member
+ public @KeyEventAction int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Retrieve the time of the most recent key down event, in the
+ * {@link android.os.SystemClock#uptimeMillis} time base, but in nanoseconds. If this
+ * is a down event, this will be the same as {@link VerifiedInputEvent#getEventTimeNanos()}.
+ *
+ * @see KeyEvent#getDownTime()
+ */
+ @DataClass.Generated.Member
+ public @SuppressLint({ "MethodNameUnits" }) long getDownTimeNanos() {
+ return mDownTimeNanos;
+ }
+
+ /**
+ * Returns the flags for this key event.
+ *
+ * @see KeyEvent#getFlags()
+ * @see KeyEvent#FLAG_CANCELED
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Retrieve the key code of the key event.
+ *
+ * @see KeyEvent#getKeyCode()
+ */
+ @DataClass.Generated.Member
+ public int getKeyCode() {
+ return mKeyCode;
+ }
+
+ /**
+ * Retrieve the hardware key id of this key event. These values are not reliable
+ * and vary from device to device.
+ *
+ * @see KeyEvent#getScanCode()
+ */
+ @DataClass.Generated.Member
+ public int getScanCode() {
+ return mScanCode;
+ }
+
+ /**
+ * <p>Returns the state of the meta keys.</p>
+ *
+ * @return an integer in which each bit set to 1 represents a pressed meta key
+ * @see KeyEvent#getMetaState()
+ */
+ @DataClass.Generated.Member
+ public int getMetaState() {
+ return mMetaState;
+ }
+
+ /**
+ * Retrieve the repeat count of the event. For key down events, this is the number of times
+ * the key has repeated with the first down starting at 0 and counting up from there.
+ * For key up events, this is always equal to zero. For multiple key events,
+ * this is the number of down/up pairs that have occurred.
+ */
+ @DataClass.Generated.Member
+ public int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(VerifiedKeyEvent other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ VerifiedKeyEvent that = (VerifiedKeyEvent) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && super.equals(that)
+ && mAction == that.mAction
+ && mDownTimeNanos == that.mDownTimeNanos
+ && mFlags == that.mFlags
+ && mKeyCode == that.mKeyCode
+ && mScanCode == that.mScanCode
+ && mMetaState == that.mMetaState
+ && mRepeatCount == that.mRepeatCount;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + super.hashCode();
+ _hash = 31 * _hash + mAction;
+ _hash = 31 * _hash + Long.hashCode(mDownTimeNanos);
+ _hash = 31 * _hash + mFlags;
+ _hash = 31 * _hash + mKeyCode;
+ _hash = 31 * _hash + mScanCode;
+ _hash = 31 * _hash + mMetaState;
+ _hash = 31 * _hash + mRepeatCount;
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@android.annotation.NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+ super.writeToParcel(dest, flags);
+
+ dest.writeInt(mAction);
+ dest.writeLong(mDownTimeNanos);
+ dest.writeInt(mFlags);
+ dest.writeInt(mKeyCode);
+ dest.writeInt(mScanCode);
+ dest.writeInt(mMetaState);
+ dest.writeInt(mRepeatCount);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ VerifiedKeyEvent(@android.annotation.NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+ super(in, VERIFIED_KEY);
+
+ int action = in.readInt();
+ long downTimeNanos = in.readLong();
+ int flags = in.readInt();
+ int keyCode = in.readInt();
+ int scanCode = in.readInt();
+ int metaState = in.readInt();
+ int repeatCount = in.readInt();
+
+ this.mAction = action;
+ com.android.internal.util.AnnotationValidations.validate(
+ KeyEventAction.class, null, mAction);
+ this.mDownTimeNanos = downTimeNanos;
+ this.mFlags = flags;
+ this.mKeyCode = keyCode;
+ this.mScanCode = scanCode;
+ this.mMetaState = metaState;
+ this.mRepeatCount = repeatCount;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @android.annotation.NonNull Parcelable.Creator<VerifiedKeyEvent> CREATOR
+ = new Parcelable.Creator<VerifiedKeyEvent>() {
+ @Override
+ public VerifiedKeyEvent[] newArray(int size) {
+ return new VerifiedKeyEvent[size];
+ }
+
+ @Override
+ public VerifiedKeyEvent createFromParcel(@android.annotation.NonNull Parcel in) {
+ return new VerifiedKeyEvent(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1604509197793L,
+ codegenVersion = "1.0.20",
+ sourceFile = "frameworks/base/core/java/android/view/VerifiedKeyEvent.java",
+ inputSignatures = "private static final java.lang.String TAG\nprivate @android.view.VerifiedKeyEvent.KeyEventAction int mAction\nprivate @android.annotation.SuppressLint long mDownTimeNanos\nprivate int mFlags\nprivate int mKeyCode\nprivate int mScanCode\nprivate int mMetaState\nprivate int mRepeatCount\npublic @android.annotation.Nullable java.lang.Boolean getFlag(int)\nclass VerifiedKeyEvent extends android.view.VerifiedInputEvent implements [android.os.Parcelable]\[email protected](genHiddenConstructor=true, genEqualsHashCode=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/VerifiedMotionEvent.java b/android/view/VerifiedMotionEvent.java
new file mode 100644
index 0000000..948575c
--- /dev/null
+++ b/android/view/VerifiedMotionEvent.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.MotionEvent.FLAG_IS_ACCESSIBILITY_EVENT;
+import static android.view.MotionEvent.FLAG_WINDOW_IS_OBSCURED;
+import static android.view.MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+
+/**
+ * MotionEvent that has been verified by the system.
+ * The data contained in this class is always a subset of a {@link MotionEvent}. Use this class to
+ * check which data has been confirmed by the system to be authentic.
+ *
+ * Most applications do not need to use this class.
+ *
+ * {@see android.hardware.input.InputManager#verifyInputEvent}
+ */
+@DataClass(genHiddenConstructor = true, genEqualsHashCode = true)
+public final class VerifiedMotionEvent extends VerifiedInputEvent implements Parcelable {
+ private static final String TAG = "VerifiedMotionEvent";
+
+ /**
+ * The raw X coordinate of the primary pointer.
+ * @see MotionEvent#getRawX()
+ */
+ private float mRawX;
+
+ /**
+ * The raw Y coordinate of the primary pointer.
+ * @see MotionEvent#getRawY()
+ */
+ private float mRawY;
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef({MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_CANCEL,
+ MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP})
+ public @interface MotionEventAction {};
+
+ /**
+ * The masked action being performed, without pointer index information.
+ *
+ * @see MotionEvent#getActionMasked()
+ */
+ @MotionEventAction
+ private int mActionMasked;
+
+ /**
+ * The time that the gesture started, in nanoseconds.
+ * Uses the same time base as {@link android.os.SystemClock#uptimeMillis()}
+ *
+ * @see MotionEvent#getDownTime()
+ */
+ @SuppressLint({"MethodNameUnits"})
+ private long mDownTimeNanos;
+
+ /**
+ * Returns the flags for this motion event.
+ *
+ * @see MotionEvent#getFlags()
+ * @see MotionEvent#FLAG_IS_ACCESSIBILITY_EVENT
+ * @see MotionEvent#FLAG_WINDOW_IS_OBSCURED
+ * @see MotionEvent#FLAG_WINDOW_IS_PARTIALLY_OBSCURED
+ * @hide
+ */
+ private int mFlags;
+
+ /**
+ * The state of any meta / modifier keys that were in effect when the event was generated.
+ *
+ * @see MotionEvent#getMetaState()
+ */
+ private int mMetaState;
+
+ /**
+ * The state of all buttons that are pressed such as a mouse or stylus button.
+ *
+ * @see MotionEvent#getButtonState()
+ */
+ private int mButtonState;
+
+ /**
+ * Get a specific flag of this motion event, if possible. Return null if the flag value could
+ * not be checked.
+ *
+ * @param flag the flag of interest
+ * @return Boolean(true) if the motion event has the requested flag
+ * Boolean(false) if the motion event does not have the requested flag
+ * null if the flag value could not be checked
+ *
+ * @see MotionEvent#FLAG_WINDOW_IS_OBSCURED
+ * @see MotionEvent#FLAG_WINDOW_IS_PARTIALLY_OBSCURED
+ */
+ public @Nullable Boolean getFlag(int flag) {
+ switch(flag) {
+ // InputDispatcher only verifies a subset of the MotionEvent flags.
+ // These values must be kept in sync with Input.cpp
+ case FLAG_IS_ACCESSIBILITY_EVENT:
+ case FLAG_WINDOW_IS_OBSCURED:
+ case FLAG_WINDOW_IS_PARTIALLY_OBSCURED:
+ return (mFlags & flag) != 0;
+ }
+ return null;
+ }
+
+ // The codegen tool doesn't fully support subclasses, since it works on a per-file basis.
+ // To modify this file:
+ // 1. run codegen on this file
+ // 2. edit the constructor signature
+ // 3. add the "super" call for constructor that receives a Parcel
+ // 4. add the "super" call to the writeToParcel method
+ // 5. Update "equals" and "hashcode" methods to include VerifiedInputEvent fields
+
+
+
+ // Code below generated by codegen v1.0.20.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/VerifiedMotionEvent.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new VerifiedMotionEvent.
+ *
+ * @param rawX
+ * The raw X coordinate of the primary pointer.
+ * @param rawY
+ * The raw Y coordinate of the primary pointer.
+ * @param actionMasked
+ * The masked action being performed, without pointer index information.
+ * @param downTimeNanos
+ * The time that the gesture started, in nanoseconds.
+ * Uses the same time base as {@link android.os.SystemClock#uptimeMillis()}
+ * @param flags
+ * Returns the flags for this motion event.
+ * @param metaState
+ * The state of any meta / modifier keys that were in effect when the event was generated.
+ * @param buttonState
+ * The state of all buttons that are pressed such as a mouse or stylus button.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public VerifiedMotionEvent(
+ int deviceId, long eventTimeNanos, int source, int displayId,
+ float rawX,
+ float rawY,
+ @MotionEventAction int actionMasked,
+ @SuppressLint({ "MethodNameUnits" }) long downTimeNanos,
+ int flags,
+ int metaState,
+ int buttonState) {
+ super(VERIFIED_MOTION, deviceId, eventTimeNanos, source, displayId);
+ this.mRawX = rawX;
+ this.mRawY = rawY;
+ this.mActionMasked = actionMasked;
+ com.android.internal.util.AnnotationValidations.validate(
+ MotionEventAction.class, null, mActionMasked);
+ this.mDownTimeNanos = downTimeNanos;
+ this.mFlags = flags;
+ this.mMetaState = metaState;
+ this.mButtonState = buttonState;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The raw X coordinate of the primary pointer.
+ *
+ * @see MotionEvent#getRawX()
+ */
+ @DataClass.Generated.Member
+ public float getRawX() {
+ return mRawX;
+ }
+
+ /**
+ * The raw Y coordinate of the primary pointer.
+ *
+ * @see MotionEvent#getRawY()
+ */
+ @DataClass.Generated.Member
+ public float getRawY() {
+ return mRawY;
+ }
+
+ /**
+ * The masked action being performed, without pointer index information.
+ *
+ * @see MotionEvent#getActionMasked()
+ */
+ @DataClass.Generated.Member
+ public @MotionEventAction int getActionMasked() {
+ return mActionMasked;
+ }
+
+ /**
+ * The time that the gesture started, in nanoseconds.
+ * Uses the same time base as {@link android.os.SystemClock#uptimeMillis()}
+ *
+ * @see MotionEvent#getDownTime()
+ */
+ @DataClass.Generated.Member
+ public @SuppressLint({ "MethodNameUnits" }) long getDownTimeNanos() {
+ return mDownTimeNanos;
+ }
+
+ /**
+ * Returns the flags for this motion event.
+ *
+ * @see MotionEvent#getFlags()
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * The state of any meta / modifier keys that were in effect when the event was generated.
+ *
+ * @see MotionEvent#getMetaState()
+ */
+ @DataClass.Generated.Member
+ public int getMetaState() {
+ return mMetaState;
+ }
+
+ /**
+ * The state of all buttons that are pressed such as a mouse or stylus button.
+ *
+ * @see MotionEvent#getButtonState()
+ */
+ @DataClass.Generated.Member
+ public int getButtonState() {
+ return mButtonState;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(VerifiedMotionEvent other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ VerifiedMotionEvent that = (VerifiedMotionEvent) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && super.equals(that)
+ && mRawX == that.mRawX
+ && mRawY == that.mRawY
+ && mActionMasked == that.mActionMasked
+ && mDownTimeNanos == that.mDownTimeNanos
+ && mFlags == that.mFlags
+ && mMetaState == that.mMetaState
+ && mButtonState == that.mButtonState;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + super.hashCode();
+ _hash = 31 * _hash + Float.hashCode(mRawX);
+ _hash = 31 * _hash + Float.hashCode(mRawY);
+ _hash = 31 * _hash + mActionMasked;
+ _hash = 31 * _hash + Long.hashCode(mDownTimeNanos);
+ _hash = 31 * _hash + mFlags;
+ _hash = 31 * _hash + mMetaState;
+ _hash = 31 * _hash + mButtonState;
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@android.annotation.NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+ super.writeToParcel(dest, flags);
+
+ dest.writeFloat(mRawX);
+ dest.writeFloat(mRawY);
+ dest.writeInt(mActionMasked);
+ dest.writeLong(mDownTimeNanos);
+ dest.writeInt(mFlags);
+ dest.writeInt(mMetaState);
+ dest.writeInt(mButtonState);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ VerifiedMotionEvent(@android.annotation.NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+ super(in, VERIFIED_MOTION);
+
+ float rawX = in.readFloat();
+ float rawY = in.readFloat();
+ int actionMasked = in.readInt();
+ long downTimeNanos = in.readLong();
+ int flags = in.readInt();
+ int metaState = in.readInt();
+ int buttonState = in.readInt();
+
+ this.mRawX = rawX;
+ this.mRawY = rawY;
+ this.mActionMasked = actionMasked;
+ com.android.internal.util.AnnotationValidations.validate(
+ MotionEventAction.class, null, mActionMasked);
+ this.mDownTimeNanos = downTimeNanos;
+ this.mFlags = flags;
+ this.mMetaState = metaState;
+ this.mButtonState = buttonState;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @android.annotation.NonNull Parcelable.Creator<VerifiedMotionEvent> CREATOR
+ = new Parcelable.Creator<VerifiedMotionEvent>() {
+ @Override
+ public VerifiedMotionEvent[] newArray(int size) {
+ return new VerifiedMotionEvent[size];
+ }
+
+ @Override
+ public VerifiedMotionEvent createFromParcel(@android.annotation.NonNull Parcel in) {
+ return new VerifiedMotionEvent(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1604509199368L,
+ codegenVersion = "1.0.20",
+ sourceFile = "frameworks/base/core/java/android/view/VerifiedMotionEvent.java",
+ inputSignatures = "private static final java.lang.String TAG\nprivate float mRawX\nprivate float mRawY\nprivate @android.view.VerifiedMotionEvent.MotionEventAction int mActionMasked\nprivate @android.annotation.SuppressLint long mDownTimeNanos\nprivate int mFlags\nprivate int mMetaState\nprivate int mButtonState\npublic @android.annotation.Nullable java.lang.Boolean getFlag(int)\nclass VerifiedMotionEvent extends android.view.VerifiedInputEvent implements [android.os.Parcelable]\[email protected](genHiddenConstructor=true, genEqualsHashCode=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/View.java b/android/view/View.java
new file mode 100644
index 0000000..f4223fb
--- /dev/null
+++ b/android/view/View.java
@@ -0,0 +1,31110 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.content.res.Resources.ID_NULL;
+import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
+import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN;
+import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN;
+import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH;
+import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
+
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
+import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
+
+import static java.lang.Math.max;
+
+import android.animation.AnimatorInflater;
+import android.animation.StateListAnimator;
+import android.annotation.AttrRes;
+import android.annotation.CallSuper;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.FloatRange;
+import android.annotation.IdRes;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.StyleRes;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.annotation.UiContext;
+import android.annotation.UiThread;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AutofillOptions;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BlendMode;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Interpolator;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.RenderEffect;
+import android.graphics.RenderNode;
+import android.graphics.Shader;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.hardware.display.DisplayManagerGlobal;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.sysprop.DisplayProperties;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.LayoutDirection;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.LongSparseLongArray;
+import android.util.Pair;
+import android.util.Pools.SynchronizedPool;
+import android.util.Property;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.StateSet;
+import android.util.SuperNotCalledException;
+import android.util.TypedValue;
+import android.view.AccessibilityIterators.CharacterTextSegmentIterator;
+import android.view.AccessibilityIterators.ParagraphTextSegmentIterator;
+import android.view.AccessibilityIterators.TextSegmentIterator;
+import android.view.AccessibilityIterators.WordTextSegmentIterator;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.InputDevice.InputSourceClass;
+import android.view.Window.OnContentApplyWindowInsetsListener;
+import android.view.WindowInsets.Type;
+import android.view.WindowInsetsAnimation.Bounds;
+import android.view.WindowManager.LayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityEventSource;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeIdManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Transformation;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.contentcapture.ContentCaptureContext;
+import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
+import android.view.displayhash.DisplayHash;
+import android.view.displayhash.DisplayHashManager;
+import android.view.displayhash.DisplayHashResultCallback;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inspector.InspectableProperty;
+import android.view.inspector.InspectableProperty.EnumEntry;
+import android.view.inspector.InspectableProperty.FlagEntry;
+import android.view.translation.TranslationCapability;
+import android.view.translation.TranslationSpec.DataFormat;
+import android.view.translation.ViewTranslationCallback;
+import android.view.translation.ViewTranslationRequest;
+import android.view.translation.ViewTranslationResponse;
+import android.widget.Checkable;
+import android.widget.FrameLayout;
+import android.widget.ScrollBarDrawable;
+
+import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.Preconditions;
+import com.android.internal.view.ScrollCaptureInternal;
+import com.android.internal.view.TooltipPopup;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.widget.ScrollBarUtils;
+
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+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;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * <p>
+ * This class represents the basic building block for user interface components. A View
+ * occupies a rectangular area on the screen and is responsible for drawing and
+ * event handling. View is the base class for <em>widgets</em>, which are
+ * used to create interactive UI components (buttons, text fields, etc.). The
+ * {@link android.view.ViewGroup} subclass is the base class for <em>layouts</em>, which
+ * are invisible containers that hold other Views (or other ViewGroups) and define
+ * their layout properties.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about using this class to develop your application's user interface,
+ * read the <a href="{@docRoot}guide/topics/ui/index.html">User Interface</a> developer guide.
+ * </div>
+ *
+ * <a name="Using"></a>
+ * <h3>Using Views</h3>
+ * <p>
+ * All of the views in a window are arranged in a single tree. You can add views
+ * either from code or by specifying a tree of views in one or more XML layout
+ * files. There are many specialized subclasses of views that act as controls or
+ * are capable of displaying text, images, or other content.
+ * </p>
+ * <p>
+ * Once you have created a tree of views, there are typically a few types of
+ * common operations you may wish to perform:
+ * <ul>
+ * <li><strong>Set properties:</strong> for example setting the text of a
+ * {@link android.widget.TextView}. The available properties and the methods
+ * that set them will vary among the different subclasses of views. Note that
+ * properties that are known at build time can be set in the XML layout
+ * files.</li>
+ * <li><strong>Set focus:</strong> The framework will handle moving focus in
+ * response to user input. To force focus to a specific view, call
+ * {@link #requestFocus}.</li>
+ * <li><strong>Set up listeners:</strong> Views allow clients to set listeners
+ * that will be notified when something interesting happens to the view. For
+ * example, all views will let you set a listener to be notified when the view
+ * gains or loses focus. You can register such a listener using
+ * {@link #setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}.
+ * Other view subclasses offer more specialized listeners. For example, a Button
+ * exposes a listener to notify clients when the button is clicked.</li>
+ * <li><strong>Set visibility:</strong> You can hide or show views using
+ * {@link #setVisibility(int)}.</li>
+ * </ul>
+ * </p>
+ * <p><em>
+ * Note: The Android framework is responsible for measuring, laying out and
+ * drawing views. You should not call methods that perform these actions on
+ * views yourself unless you are actually implementing a
+ * {@link android.view.ViewGroup}.
+ * </em></p>
+ *
+ * <a name="Lifecycle"></a>
+ * <h3>Implementing a Custom View</h3>
+ *
+ * <p>
+ * To implement a custom view, you will usually begin by providing overrides for
+ * some of the standard methods that the framework calls on all views. You do
+ * not need to override all of these methods. In fact, you can start by just
+ * overriding {@link #onDraw(android.graphics.Canvas)}.
+ * <table border="2" width="85%" align="center" cellpadding="5">
+ * <thead>
+ * <tr><th>Category</th> <th>Methods</th> <th>Description</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr>
+ * <td rowspan="2">Creation</td>
+ * <td>Constructors</td>
+ * <td>There is a form of the constructor that are called when the view
+ * is created from code and a form that is called when the view is
+ * inflated from a layout file. The second form should parse and apply
+ * any attributes defined in the layout file.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onFinishInflate()}</code></td>
+ * <td>Called after a view and all of its children has been inflated
+ * from XML.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="3">Layout</td>
+ * <td><code>{@link #onMeasure(int, int)}</code></td>
+ * <td>Called to determine the size requirements for this view and all
+ * of its children.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onLayout(boolean, int, int, int, int)}</code></td>
+ * <td>Called when this view should assign a size and position to all
+ * of its children.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onSizeChanged(int, int, int, int)}</code></td>
+ * <td>Called when the size of this view has changed.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td>Drawing</td>
+ * <td><code>{@link #onDraw(android.graphics.Canvas)}</code></td>
+ * <td>Called when the view should render its content.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="4">Event processing</td>
+ * <td><code>{@link #onKeyDown(int, KeyEvent)}</code></td>
+ * <td>Called when a new hardware key event occurs.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onKeyUp(int, KeyEvent)}</code></td>
+ * <td>Called when a hardware key up event occurs.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onTrackballEvent(MotionEvent)}</code></td>
+ * <td>Called when a trackball motion event occurs.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onTouchEvent(MotionEvent)}</code></td>
+ * <td>Called when a touch screen motion event occurs.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="2">Focus</td>
+ * <td><code>{@link #onFocusChanged(boolean, int, android.graphics.Rect)}</code></td>
+ * <td>Called when the view gains or loses focus.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td><code>{@link #onWindowFocusChanged(boolean)}</code></td>
+ * <td>Called when the window containing the view gains or loses focus.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="3">Attaching</td>
+ * <td><code>{@link #onAttachedToWindow()}</code></td>
+ * <td>Called when the view is attached to a window.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td><code>{@link #onDetachedFromWindow}</code></td>
+ * <td>Called when the view is detached from its window.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td><code>{@link #onWindowVisibilityChanged(int)}</code></td>
+ * <td>Called when the visibility of the window containing the view
+ * has changed.
+ * </td>
+ * </tr>
+ * </tbody>
+ *
+ * </table>
+ * </p>
+ *
+ * <a name="IDs"></a>
+ * <h3>IDs</h3>
+ * Views may have an integer id associated with them. These ids are typically
+ * assigned in the layout XML files, and are used to find specific views within
+ * the view tree. A common pattern is to:
+ * <ul>
+ * <li>Define a Button in the layout file and assign it a unique ID.
+ * <pre>
+ * <Button
+ * android:id="@+id/my_button"
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:text="@string/my_button_text"/>
+ * </pre></li>
+ * <li>From the onCreate method of an Activity, find the Button
+ * <pre class="prettyprint">
+ * Button myButton = findViewById(R.id.my_button);
+ * </pre></li>
+ * </ul>
+ * <p>
+ * View IDs need not be unique throughout the tree, but it is good practice to
+ * ensure that they are at least unique within the part of the tree you are
+ * searching.
+ * </p>
+ *
+ * <a name="Position"></a>
+ * <h3>Position</h3>
+ * <p>
+ * The geometry of a view is that of a rectangle. A view has a location,
+ * expressed as a pair of <em>left</em> and <em>top</em> coordinates, and
+ * two dimensions, expressed as a width and a height. The unit for location
+ * and dimensions is the pixel.
+ * </p>
+ *
+ * <p>
+ * It is possible to retrieve the location of a view by invoking the methods
+ * {@link #getLeft()} and {@link #getTop()}. The former returns the left, or X,
+ * coordinate of the rectangle representing the view. The latter returns the
+ * top, or Y, coordinate of the rectangle representing the view. These methods
+ * both return the location of the view relative to its parent. For instance,
+ * when getLeft() returns 20, that means the view is located 20 pixels to the
+ * right of the left edge of its direct parent.
+ * </p>
+ *
+ * <p>
+ * In addition, several convenience methods are offered to avoid unnecessary
+ * computations, namely {@link #getRight()} and {@link #getBottom()}.
+ * These methods return the coordinates of the right and bottom edges of the
+ * rectangle representing the view. For instance, calling {@link #getRight()}
+ * is similar to the following computation: <code>getLeft() + getWidth()</code>
+ * (see <a href="#SizePaddingMargins">Size</a> for more information about the width.)
+ * </p>
+ *
+ * <a name="SizePaddingMargins"></a>
+ * <h3>Size, padding and margins</h3>
+ * <p>
+ * The size of a view is expressed with a width and a height. A view actually
+ * possess two pairs of width and height values.
+ * </p>
+ *
+ * <p>
+ * The first pair is known as <em>measured width</em> and
+ * <em>measured height</em>. These dimensions define how big a view wants to be
+ * within its parent (see <a href="#Layout">Layout</a> for more details.) The
+ * measured dimensions can be obtained by calling {@link #getMeasuredWidth()}
+ * and {@link #getMeasuredHeight()}.
+ * </p>
+ *
+ * <p>
+ * The second pair is simply known as <em>width</em> and <em>height</em>, or
+ * sometimes <em>drawing width</em> and <em>drawing height</em>. These
+ * dimensions define the actual size of the view on screen, at drawing time and
+ * after layout. These values may, but do not have to, be different from the
+ * measured width and height. The width and height can be obtained by calling
+ * {@link #getWidth()} and {@link #getHeight()}.
+ * </p>
+ *
+ * <p>
+ * To measure its dimensions, a view takes into account its padding. The padding
+ * is expressed in pixels for the left, top, right and bottom parts of the view.
+ * Padding can be used to offset the content of the view by a specific amount of
+ * pixels. For instance, a left padding of 2 will push the view's content by
+ * 2 pixels to the right of the left edge. Padding can be set using the
+ * {@link #setPadding(int, int, int, int)} or {@link #setPaddingRelative(int, int, int, int)}
+ * method and queried by calling {@link #getPaddingLeft()}, {@link #getPaddingTop()},
+ * {@link #getPaddingRight()}, {@link #getPaddingBottom()}, {@link #getPaddingStart()},
+ * {@link #getPaddingEnd()}.
+ * </p>
+ *
+ * <p>
+ * Even though a view can define a padding, it does not provide any support for
+ * margins. However, view groups provide such a support. Refer to
+ * {@link android.view.ViewGroup} and
+ * {@link android.view.ViewGroup.MarginLayoutParams} for further information.
+ * </p>
+ *
+ * <a name="Layout"></a>
+ * <h3>Layout</h3>
+ * <p>
+ * Layout is a two pass process: a measure pass and a layout pass. The measuring
+ * pass is implemented in {@link #measure(int, int)} and is a top-down traversal
+ * of the view tree. Each view pushes dimension specifications down the tree
+ * during the recursion. At the end of the measure pass, every view has stored
+ * its measurements. The second pass happens in
+ * {@link #layout(int,int,int,int)} and is also top-down. During
+ * this pass each parent is responsible for positioning all of its children
+ * using the sizes computed in the measure pass.
+ * </p>
+ *
+ * <p>
+ * When a view's measure() method returns, its {@link #getMeasuredWidth()} and
+ * {@link #getMeasuredHeight()} values must be set, along with those for all of
+ * that view's descendants. A view's measured width and measured height values
+ * must respect the constraints imposed by the view's parents. This guarantees
+ * that at the end of the measure pass, all parents accept all of their
+ * children's measurements. A parent view may call measure() more than once on
+ * its children. For example, the parent may measure each child once with
+ * unspecified dimensions to find out how big they want to be, then call
+ * measure() on them again with actual numbers if the sum of all the children's
+ * unconstrained sizes is too big or too small.
+ * </p>
+ *
+ * <p>
+ * The measure pass uses two classes to communicate dimensions. The
+ * {@link MeasureSpec} class is used by views to tell their parents how they
+ * want to be measured and positioned. The base LayoutParams class just
+ * describes how big the view wants to be for both width and height. For each
+ * dimension, it can specify one of:
+ * <ul>
+ * <li> an exact number
+ * <li>MATCH_PARENT, which means the view wants to be as big as its parent
+ * (minus padding)
+ * <li> WRAP_CONTENT, which means that the view wants to be just big enough to
+ * enclose its content (plus padding).
+ * </ul>
+ * There are subclasses of LayoutParams for different subclasses of ViewGroup.
+ * For example, AbsoluteLayout has its own subclass of LayoutParams which adds
+ * an X and Y value.
+ * </p>
+ *
+ * <p>
+ * MeasureSpecs are used to push requirements down the tree from parent to
+ * child. A MeasureSpec can be in one of three modes:
+ * <ul>
+ * <li>UNSPECIFIED: This is used by a parent to determine the desired dimension
+ * of a child view. For example, a LinearLayout may call measure() on its child
+ * with the height set to UNSPECIFIED and a width of EXACTLY 240 to find out how
+ * tall the child view wants to be given a width of 240 pixels.
+ * <li>EXACTLY: This is used by the parent to impose an exact size on the
+ * child. The child must use this size, and guarantee that all of its
+ * descendants will fit within this size.
+ * <li>AT_MOST: This is used by the parent to impose a maximum size on the
+ * child. The child must guarantee that it and all of its descendants will fit
+ * within this size.
+ * </ul>
+ * </p>
+ *
+ * <p>
+ * To initiate a layout, call {@link #requestLayout}. This method is typically
+ * called by a view on itself when it believes that it can no longer fit within
+ * its current bounds.
+ * </p>
+ *
+ * <a name="Drawing"></a>
+ * <h3>Drawing</h3>
+ * <p>
+ * Drawing is handled by walking the tree and recording the drawing commands of
+ * any View that needs to update. After this, the drawing commands of the
+ * entire tree are issued to screen, clipped to the newly damaged area.
+ * </p>
+ *
+ * <p>
+ * The tree is largely recorded and drawn in order, with parents drawn before
+ * (i.e., behind) their children, with siblings drawn in the order they appear
+ * in the tree. If you set a background drawable for a View, then the View will
+ * draw it before calling back to its <code>onDraw()</code> method. The child
+ * drawing order can be overridden with
+ * {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean) custom child drawing order}
+ * in a ViewGroup, and with {@link #setZ(float)} custom Z values} set on Views.
+ * </p>
+ *
+ * <p>
+ * To force a view to draw, call {@link #invalidate()}.
+ * </p>
+ *
+ * <a name="EventHandlingThreading"></a>
+ * <h3>Event Handling and Threading</h3>
+ * <p>
+ * The basic cycle of a view is as follows:
+ * <ol>
+ * <li>An event comes in and is dispatched to the appropriate view. The view
+ * handles the event and notifies any listeners.</li>
+ * <li>If in the course of processing the event, the view's bounds may need
+ * to be changed, the view will call {@link #requestLayout()}.</li>
+ * <li>Similarly, if in the course of processing the event the view's appearance
+ * may need to be changed, the view will call {@link #invalidate()}.</li>
+ * <li>If either {@link #requestLayout()} or {@link #invalidate()} were called,
+ * the framework will take care of measuring, laying out, and drawing the tree
+ * as appropriate.</li>
+ * </ol>
+ * </p>
+ *
+ * <p><em>Note: The entire view tree is single threaded. You must always be on
+ * the UI thread when calling any method on any view.</em>
+ * If you are doing work on other threads and want to update the state of a view
+ * from that thread, you should use a {@link Handler}.
+ * </p>
+ *
+ * <a name="FocusHandling"></a>
+ * <h3>Focus Handling</h3>
+ * <p>
+ * The framework will handle routine focus movement in response to user input.
+ * This includes changing the focus as views are removed or hidden, or as new
+ * views become available. Views indicate their willingness to take focus
+ * through the {@link #isFocusable} method. To change whether a view can take
+ * focus, call {@link #setFocusable(boolean)}. When in touch mode (see notes below)
+ * views indicate whether they still would like focus via {@link #isFocusableInTouchMode}
+ * and can change this via {@link #setFocusableInTouchMode(boolean)}.
+ * </p>
+ * <p>
+ * Focus movement is based on an algorithm which finds the nearest neighbor in a
+ * given direction. In rare cases, the default algorithm may not match the
+ * intended behavior of the developer. In these situations, you can provide
+ * explicit overrides by using these XML attributes in the layout file:
+ * <pre>
+ * nextFocusDown
+ * nextFocusLeft
+ * nextFocusRight
+ * nextFocusUp
+ * </pre>
+ * </p>
+ *
+ *
+ * <p>
+ * To get a particular view to take focus, call {@link #requestFocus()}.
+ * </p>
+ *
+ * <a name="TouchMode"></a>
+ * <h3>Touch Mode</h3>
+ * <p>
+ * When a user is navigating a user interface via directional keys such as a D-pad, it is
+ * necessary to give focus to actionable items such as buttons so the user can see
+ * what will take input. If the device has touch capabilities, however, and the user
+ * begins interacting with the interface by touching it, it is no longer necessary to
+ * always highlight, or give focus to, a particular view. This motivates a mode
+ * for interaction named 'touch mode'.
+ * </p>
+ * <p>
+ * For a touch capable device, once the user touches the screen, the device
+ * will enter touch mode. From this point onward, only views for which
+ * {@link #isFocusableInTouchMode} is true will be focusable, such as text editing widgets.
+ * Other views that are touchable, like buttons, will not take focus when touched; they will
+ * only fire the on click listeners.
+ * </p>
+ * <p>
+ * Any time a user hits a directional key, such as a D-pad direction, the view device will
+ * exit touch mode, and find a view to take focus, so that the user may resume interacting
+ * with the user interface without touching the screen again.
+ * </p>
+ * <p>
+ * The touch mode state is maintained across {@link android.app.Activity}s. Call
+ * {@link #isInTouchMode} to see whether the device is currently in touch mode.
+ * </p>
+ *
+ * <a name="Scrolling"></a>
+ * <h3>Scrolling</h3>
+ * <p>
+ * The framework provides basic support for views that wish to internally
+ * scroll their content. This includes keeping track of the X and Y scroll
+ * offset as well as mechanisms for drawing scrollbars. See
+ * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)}, and
+ * {@link #awakenScrollBars()} for more details.
+ * </p>
+ *
+ * <a name="Tags"></a>
+ * <h3>Tags</h3>
+ * <p>
+ * Unlike IDs, tags are not used to identify views. Tags are essentially an
+ * extra piece of information that can be associated with a view. They are most
+ * often used as a convenience to store data related to views in the views
+ * themselves rather than by putting them in a separate structure.
+ * </p>
+ * <p>
+ * Tags may be specified with character sequence values in layout XML as either
+ * a single tag using the {@link android.R.styleable#View_tag android:tag}
+ * attribute or multiple tags using the {@code <tag>} child element:
+ * <pre>
+ * <View ...
+ * android:tag="@string/mytag_value" />
+ * <View ...>
+ * <tag android:id="@+id/mytag"
+ * android:value="@string/mytag_value" />
+ * </View>
+ * </pre>
+ * </p>
+ * <p>
+ * Tags may also be specified with arbitrary objects from code using
+ * {@link #setTag(Object)} or {@link #setTag(int, Object)}.
+ * </p>
+ *
+ * <a name="Themes"></a>
+ * <h3>Themes</h3>
+ * <p>
+ * By default, Views are created using the theme of the Context object supplied
+ * to their constructor; however, a different theme may be specified by using
+ * the {@link android.R.styleable#View_theme android:theme} attribute in layout
+ * XML or by passing a {@link ContextThemeWrapper} to the constructor from
+ * code.
+ * </p>
+ * <p>
+ * When the {@link android.R.styleable#View_theme android:theme} attribute is
+ * used in XML, the specified theme is applied on top of the inflation
+ * context's theme (see {@link LayoutInflater}) and used for the view itself as
+ * well as any child elements.
+ * </p>
+ * <p>
+ * In the following example, both views will be created using the Material dark
+ * color scheme; however, because an overlay theme is used which only defines a
+ * subset of attributes, the value of
+ * {@link android.R.styleable#Theme_colorAccent android:colorAccent} defined on
+ * the inflation context's theme (e.g. the Activity theme) will be preserved.
+ * <pre>
+ * <LinearLayout
+ * ...
+ * android:theme="@android:theme/ThemeOverlay.Material.Dark">
+ * <View ...>
+ * </LinearLayout>
+ * </pre>
+ * </p>
+ *
+ * <a name="Properties"></a>
+ * <h3>Properties</h3>
+ * <p>
+ * The View class exposes an {@link #ALPHA} property, as well as several transform-related
+ * properties, such as {@link #TRANSLATION_X} and {@link #TRANSLATION_Y}. These properties are
+ * available both in the {@link Property} form as well as in similarly-named setter/getter
+ * methods (such as {@link #setAlpha(float)} for {@link #ALPHA}). These properties can
+ * be used to set persistent state associated with these rendering-related properties on the view.
+ * The properties and methods can also be used in conjunction with
+ * {@link android.animation.Animator Animator}-based animations, described more in the
+ * <a href="#Animation">Animation</a> section.
+ * </p>
+ *
+ * <a name="Animation"></a>
+ * <h3>Animation</h3>
+ * <p>
+ * Starting with Android 3.0, the preferred way of animating views is to use the
+ * {@link android.animation} package APIs. These {@link android.animation.Animator Animator}-based
+ * classes change actual properties of the View object, such as {@link #setAlpha(float) alpha} and
+ * {@link #setTranslationX(float) translationX}. This behavior is contrasted to that of the pre-3.0
+ * {@link android.view.animation.Animation Animation}-based classes, which instead animate only
+ * how the view is drawn on the display. In particular, the {@link ViewPropertyAnimator} class
+ * makes animating these View properties particularly easy and efficient.
+ * </p>
+ * <p>
+ * Alternatively, you can use the pre-3.0 animation classes to animate how Views are rendered.
+ * You can attach an {@link Animation} object to a view using
+ * {@link #setAnimation(Animation)} or
+ * {@link #startAnimation(Animation)}. The animation can alter the scale,
+ * rotation, translation and alpha of a view over time. If the animation is
+ * attached to a view that has children, the animation will affect the entire
+ * subtree rooted by that node. When an animation is started, the framework will
+ * take care of redrawing the appropriate views until the animation completes.
+ * </p>
+ *
+ * <a name="Security"></a>
+ * <h3>Security</h3>
+ * <p>
+ * Sometimes it is essential that an application be able to verify that an action
+ * is being performed with the full knowledge and consent of the user, such as
+ * granting a permission request, making a purchase or clicking on an advertisement.
+ * Unfortunately, a malicious application could try to spoof the user into
+ * performing these actions, unaware, by concealing the intended purpose of the view.
+ * As a remedy, the framework offers a touch filtering mechanism that can be used to
+ * improve the security of views that provide access to sensitive functionality.
+ * </p><p>
+ * To enable touch filtering, call {@link #setFilterTouchesWhenObscured(boolean)} or set the
+ * android:filterTouchesWhenObscured layout attribute to true. When enabled, the framework
+ * will discard touches that are received whenever the view's window is obscured by
+ * another visible window. As a result, the view will not receive touches whenever a
+ * toast, dialog or other window appears above the view's window.
+ * </p><p>
+ * For more fine-grained control over security, consider overriding the
+ * {@link #onFilterTouchEventForSecurity(MotionEvent)} method to implement your own
+ * security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}.
+ * </p>
+ *
+ * @attr ref android.R.styleable#View_accessibilityHeading
+ * @attr ref android.R.styleable#View_allowClickWhenDisabled
+ * @attr ref android.R.styleable#View_alpha
+ * @attr ref android.R.styleable#View_background
+ * @attr ref android.R.styleable#View_clickable
+ * @attr ref android.R.styleable#View_clipToOutline
+ * @attr ref android.R.styleable#View_contentDescription
+ * @attr ref android.R.styleable#View_drawingCacheQuality
+ * @attr ref android.R.styleable#View_duplicateParentState
+ * @attr ref android.R.styleable#View_id
+ * @attr ref android.R.styleable#View_requiresFadingEdge
+ * @attr ref android.R.styleable#View_fadeScrollbars
+ * @attr ref android.R.styleable#View_fadingEdgeLength
+ * @attr ref android.R.styleable#View_filterTouchesWhenObscured
+ * @attr ref android.R.styleable#View_fitsSystemWindows
+ * @attr ref android.R.styleable#View_isScrollContainer
+ * @attr ref android.R.styleable#View_focusable
+ * @attr ref android.R.styleable#View_focusableInTouchMode
+ * @attr ref android.R.styleable#View_focusedByDefault
+ * @attr ref android.R.styleable#View_hapticFeedbackEnabled
+ * @attr ref android.R.styleable#View_keepScreenOn
+ * @attr ref android.R.styleable#View_keyboardNavigationCluster
+ * @attr ref android.R.styleable#View_layerType
+ * @attr ref android.R.styleable#View_layoutDirection
+ * @attr ref android.R.styleable#View_longClickable
+ * @attr ref android.R.styleable#View_minHeight
+ * @attr ref android.R.styleable#View_minWidth
+ * @attr ref android.R.styleable#View_nextClusterForward
+ * @attr ref android.R.styleable#View_nextFocusDown
+ * @attr ref android.R.styleable#View_nextFocusLeft
+ * @attr ref android.R.styleable#View_nextFocusRight
+ * @attr ref android.R.styleable#View_nextFocusUp
+ * @attr ref android.R.styleable#View_onClick
+ * @attr ref android.R.styleable#View_outlineSpotShadowColor
+ * @attr ref android.R.styleable#View_outlineAmbientShadowColor
+ * @attr ref android.R.styleable#View_padding
+ * @attr ref android.R.styleable#View_paddingHorizontal
+ * @attr ref android.R.styleable#View_paddingVertical
+ * @attr ref android.R.styleable#View_paddingBottom
+ * @attr ref android.R.styleable#View_paddingLeft
+ * @attr ref android.R.styleable#View_paddingRight
+ * @attr ref android.R.styleable#View_paddingTop
+ * @attr ref android.R.styleable#View_paddingStart
+ * @attr ref android.R.styleable#View_paddingEnd
+ * @attr ref android.R.styleable#View_saveEnabled
+ * @attr ref android.R.styleable#View_rotation
+ * @attr ref android.R.styleable#View_rotationX
+ * @attr ref android.R.styleable#View_rotationY
+ * @attr ref android.R.styleable#View_scaleX
+ * @attr ref android.R.styleable#View_scaleY
+ * @attr ref android.R.styleable#View_scrollX
+ * @attr ref android.R.styleable#View_scrollY
+ * @attr ref android.R.styleable#View_scrollbarSize
+ * @attr ref android.R.styleable#View_scrollbarStyle
+ * @attr ref android.R.styleable#View_scrollbars
+ * @attr ref android.R.styleable#View_scrollbarDefaultDelayBeforeFade
+ * @attr ref android.R.styleable#View_scrollbarFadeDuration
+ * @attr ref android.R.styleable#View_scrollbarTrackHorizontal
+ * @attr ref android.R.styleable#View_scrollbarThumbHorizontal
+ * @attr ref android.R.styleable#View_scrollbarThumbVertical
+ * @attr ref android.R.styleable#View_scrollbarTrackVertical
+ * @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack
+ * @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack
+ * @attr ref android.R.styleable#View_stateListAnimator
+ * @attr ref android.R.styleable#View_transitionName
+ * @attr ref android.R.styleable#View_soundEffectsEnabled
+ * @attr ref android.R.styleable#View_tag
+ * @attr ref android.R.styleable#View_textAlignment
+ * @attr ref android.R.styleable#View_textDirection
+ * @attr ref android.R.styleable#View_transformPivotX
+ * @attr ref android.R.styleable#View_transformPivotY
+ * @attr ref android.R.styleable#View_translationX
+ * @attr ref android.R.styleable#View_translationY
+ * @attr ref android.R.styleable#View_translationZ
+ * @attr ref android.R.styleable#View_visibility
+ * @attr ref android.R.styleable#View_theme
+ *
+ * @see android.view.ViewGroup
+ */
+@UiThread
+public class View implements Drawable.Callback, KeyEvent.Callback,
+ AccessibilityEventSource {
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final boolean DBG = false;
+
+ /** @hide */
+ public static boolean DEBUG_DRAW = false;
+
+ /**
+ * The logging tag used by this class with android.util.Log.
+ */
+ protected static final String VIEW_LOG_TAG = "View";
+
+ /**
+ * The logging tag used by this class when logging verbose, autofill-related messages.
+ */
+ // NOTE: We cannot use android.view.autofill.Helper.sVerbose because that variable is not
+ // set if a session is not started.
+ private static final String AUTOFILL_LOG_TAG = "View.Autofill";
+
+ /**
+ * The logging tag used by this class when logging content capture-related messages.
+ */
+ private static final String CONTENT_CAPTURE_LOG_TAG = "View.ContentCapture";
+
+ private static final boolean DEBUG_CONTENT_CAPTURE = false;
+
+ /**
+ * When set to true, this view will save its attribute data.
+ *
+ * @hide
+ */
+ public static boolean sDebugViewAttributes = false;
+
+ /**
+ * When set to this application package view will save its attribute data.
+ *
+ * @hide
+ */
+ public static String sDebugViewAttributesApplicationPackage;
+
+ /**
+ * Used to mark a View that has no ID.
+ */
+ public static final int NO_ID = -1;
+
+ /**
+ * Last ID that is given to Views that are no part of activities.
+ *
+ * {@hide}
+ */
+ public static final int LAST_APP_AUTOFILL_ID = Integer.MAX_VALUE / 2;
+
+ /**
+ * Attribute to find the autofilled highlight
+ *
+ * @see #getAutofilledDrawable()
+ */
+ private static final int[] AUTOFILL_HIGHLIGHT_ATTR =
+ new int[]{android.R.attr.autofilledHighlight};
+
+ /**
+ * Signals that compatibility booleans have been initialized according to
+ * target SDK versions.
+ */
+ private static boolean sCompatibilityDone = false;
+
+ /**
+ * Use the old (broken) way of building MeasureSpecs.
+ */
+ private static boolean sUseBrokenMakeMeasureSpec = false;
+
+ /**
+ * Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED
+ */
+ static boolean sUseZeroUnspecifiedMeasureSpec = false;
+
+ /**
+ * Ignore any optimizations using the measure cache.
+ */
+ private static boolean sIgnoreMeasureCache = false;
+
+ /**
+ * Ignore an optimization that skips unnecessary EXACTLY layout passes.
+ */
+ private static boolean sAlwaysRemeasureExactly = false;
+
+ /**
+ * Allow setForeground/setBackground to be called (and ignored) on a textureview,
+ * without throwing
+ */
+ static boolean sTextureViewIgnoresDrawableSetters = false;
+
+ /**
+ * Prior to N, some ViewGroups would not convert LayoutParams properly even though both extend
+ * MarginLayoutParams. For instance, converting LinearLayout.LayoutParams to
+ * RelativeLayout.LayoutParams would lose margin information. This is fixed on N but target API
+ * check is implemented for backwards compatibility.
+ *
+ * {@hide}
+ */
+ protected static boolean sPreserveMarginParamsInLayoutParamConversion;
+
+ /**
+ * Prior to N, when drag enters into child of a view that has already received an
+ * ACTION_DRAG_ENTERED event, the parent doesn't get a ACTION_DRAG_EXITED event.
+ * ACTION_DRAG_LOCATION and ACTION_DROP were delivered to the parent of a view that returned
+ * false from its event handler for these events.
+ * Starting from N, the parent will get ACTION_DRAG_EXITED event before the child gets its
+ * ACTION_DRAG_ENTERED. ACTION_DRAG_LOCATION and ACTION_DROP are never propagated to the parent.
+ * sCascadedDragDrop is true for pre-N apps for backwards compatibility implementation.
+ */
+ static boolean sCascadedDragDrop;
+
+ /**
+ * Prior to O, auto-focusable didn't exist and widgets such as ListView use hasFocusable
+ * to determine things like whether or not to permit item click events. We can't break
+ * apps that do this just because more things (clickable things) are now auto-focusable
+ * and they would get different results, so give old behavior to old apps.
+ */
+ static boolean sHasFocusableExcludeAutoFocusable;
+
+ /**
+ * Prior to O, auto-focusable didn't exist and views marked as clickable weren't implicitly
+ * made focusable by default. As a result, apps could (incorrectly) change the clickable
+ * setting of views off the UI thread. Now that clickable can effect the focusable state,
+ * changing the clickable attribute off the UI thread will cause an exception (since changing
+ * the focusable state checks). In order to prevent apps from crashing, we will handle this
+ * specific case and just not notify parents on new focusables resulting from marking views
+ * clickable from outside the UI thread.
+ */
+ private static boolean sAutoFocusableOffUIThreadWontNotifyParents;
+
+ /**
+ * Prior to P things like setScaleX() allowed passing float values that were bogus such as
+ * Float.NaN. If the app is targetting P or later then passing these values will result in an
+ * exception being thrown. If the app is targetting an earlier SDK version, then we will
+ * silently clamp these values to avoid crashes elsewhere when the rendering code hits
+ * these bogus values.
+ */
+ private static boolean sThrowOnInvalidFloatProperties;
+
+ /**
+ * Prior to P, {@code #startDragAndDrop} accepts a builder which produces an empty drag shadow.
+ * Currently zero size SurfaceControl cannot be created thus we create a 1x1 surface instead.
+ */
+ private static boolean sAcceptZeroSizeDragShadow;
+
+ /**
+ * Prior to R, {@link #dispatchApplyWindowInsets} had an issue:
+ * <p>The modified insets changed by {@link #onApplyWindowInsets} were passed to the
+ * entire view hierarchy in prefix order, including siblings as well as siblings of parents
+ * further down the hierarchy. This violates the basic concepts of the view hierarchy, and
+ * thus, the hierarchical dispatching mechanism was hard to use for apps.
+ * <p>
+ * In order to make window inset dispatching work properly, we dispatch window insets
+ * in the view hierarchy in a proper hierarchical manner if this flag is set to {@code false}.
+ */
+ static boolean sBrokenInsetsDispatch;
+
+ /**
+ * Prior to Q, calling
+ * {@link com.android.internal.policy.DecorView#setBackgroundDrawable(Drawable)}
+ * did not call update the window format so the opacity of the background was not correctly
+ * applied to the window. Some applications rely on this misbehavior to work properly.
+ * <p>
+ * From Q, {@link com.android.internal.policy.DecorView#setBackgroundDrawable(Drawable)} is
+ * the same as {@link com.android.internal.policy.DecorView#setWindowBackground(Drawable)}
+ * which updates the window format.
+ * @hide
+ */
+ protected static boolean sBrokenWindowBackground;
+
+ /**
+ * Prior to R, we were always forcing a layout of the entire hierarchy when insets changed from
+ * the server. This is inefficient and not all apps use it. Instead, we want to rely on apps
+ * calling {@link #requestLayout} when they need to relayout based on an insets change.
+ */
+ static boolean sForceLayoutWhenInsetsChanged;
+
+ /** @hide */
+ @IntDef({NOT_FOCUSABLE, FOCUSABLE, FOCUSABLE_AUTO})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Focusable {}
+
+ /**
+ * This view does not want keystrokes.
+ * <p>
+ * Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
+ * android:focusable}.
+ */
+ public static final int NOT_FOCUSABLE = 0x00000000;
+
+ /**
+ * This view wants keystrokes.
+ * <p>
+ * Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
+ * android:focusable}.
+ */
+ public static final int FOCUSABLE = 0x00000001;
+
+ /**
+ * This view determines focusability automatically. This is the default.
+ * <p>
+ * Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
+ * android:focusable}.
+ */
+ public static final int FOCUSABLE_AUTO = 0x00000010;
+
+ /**
+ * Mask for use with setFlags indicating bits used for focus.
+ */
+ private static final int FOCUSABLE_MASK = 0x00000011;
+
+ /**
+ * This view will adjust its padding to fit sytem windows (e.g. status bar)
+ */
+ private static final int FITS_SYSTEM_WINDOWS = 0x00000002;
+
+ /** @hide */
+ @IntDef({VISIBLE, INVISIBLE, GONE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Visibility {}
+
+ /**
+ * This view is visible.
+ * Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code
+ * android:visibility}.
+ */
+ public static final int VISIBLE = 0x00000000;
+
+ /**
+ * This view is invisible, but it still takes up space for layout purposes.
+ * Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code
+ * android:visibility}.
+ */
+ public static final int INVISIBLE = 0x00000004;
+
+ /**
+ * This view is invisible, and it doesn't take any space for layout
+ * purposes. Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code
+ * android:visibility}.
+ */
+ public static final int GONE = 0x00000008;
+
+ /**
+ * Mask for use with setFlags indicating bits used for visibility.
+ * {@hide}
+ */
+ static final int VISIBILITY_MASK = 0x0000000C;
+
+ private static final int[] VISIBILITY_FLAGS = {VISIBLE, INVISIBLE, GONE};
+
+ /**
+ * Hint indicating that this view can be autofilled with an email address.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
+
+ /**
+ * Hint indicating that this view can be autofilled with a user's real name.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_NAME}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_NAME = "name";
+
+ /**
+ * Hint indicating that this view can be autofilled with a username.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_USERNAME}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_USERNAME = "username";
+
+ /**
+ * Hint indicating that this view can be autofilled with a password.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_PASSWORD}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_PASSWORD = "password";
+
+ /**
+ * Hint indicating that this view can be autofilled with a phone number.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_PHONE}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_PHONE = "phone";
+
+ /**
+ * Hint indicating that this view can be autofilled with a postal address.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_POSTAL_ADDRESS}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_POSTAL_ADDRESS = "postalAddress";
+
+ /**
+ * Hint indicating that this view can be autofilled with a postal code.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_POSTAL_CODE}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_POSTAL_CODE = "postalCode";
+
+ /**
+ * Hint indicating that this view can be autofilled with a credit card number.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_NUMBER}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_CREDIT_CARD_NUMBER = "creditCardNumber";
+
+ /**
+ * Hint indicating that this view can be autofilled with a credit card security code.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
+
+ /**
+ * Hint indicating that this view can be autofilled with a credit card expiration date.
+ *
+ * <p>It should be used when the credit card expiration date is represented by just one view;
+ * if it is represented by more than one (for example, one view for the month and another view
+ * for the year), then each of these views should use the hint specific for the unit
+ * ({@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY},
+ * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH},
+ * or {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR}).
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <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 =
+ "creditCardExpirationDate";
+
+ /**
+ * Hint indicating that this view can be autofilled with a credit card expiration month.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <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 =
+ "creditCardExpirationMonth";
+
+ /**
+ * Hint indicating that this view can be autofilled with a credit card expiration year.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR =
+ "creditCardExpirationYear";
+
+ /**
+ * Hint indicating that this view can be autofilled with a credit card expiration day.
+ *
+ * <p>Can be used with either {@link #setAutofillHints(String[])} or
+ * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the
+ * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY}</code>).
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints.
+ */
+ public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY = "creditCardExpirationDay";
+
+ /**
+ * Hints for the autofill services that describes the content of the view.
+ */
+ private @Nullable String[] mAutofillHints;
+
+ /**
+ * Autofill id, lazily created on calls to {@link #getAutofillId()}.
+ */
+ private AutofillId mAutofillId;
+
+ /** @hide */
+ @IntDef(prefix = { "AUTOFILL_TYPE_" }, value = {
+ AUTOFILL_TYPE_NONE,
+ AUTOFILL_TYPE_TEXT,
+ AUTOFILL_TYPE_TOGGLE,
+ AUTOFILL_TYPE_LIST,
+ AUTOFILL_TYPE_DATE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AutofillType {}
+
+ /**
+ * Autofill type for views that cannot be autofilled.
+ *
+ * <p>Typically used when the view is read-only; for example, a text label.
+ *
+ * @see #getAutofillType()
+ */
+ public static final int AUTOFILL_TYPE_NONE = 0;
+
+ /**
+ * Autofill type for a text field, which is filled by a {@link CharSequence}.
+ *
+ * <p>{@link AutofillValue} instances for autofilling a {@link View} can be obtained through
+ * {@link AutofillValue#forText(CharSequence)}, and the value passed to autofill a
+ * {@link View} can be fetched through {@link AutofillValue#getTextValue()}.
+ *
+ * @see #getAutofillType()
+ */
+ public static final int AUTOFILL_TYPE_TEXT = 1;
+
+ /**
+ * Autofill type for a togglable field, which is filled by a {@code boolean}.
+ *
+ * <p>{@link AutofillValue} instances for autofilling a {@link View} can be obtained through
+ * {@link AutofillValue#forToggle(boolean)}, and the value passed to autofill a
+ * {@link View} can be fetched through {@link AutofillValue#getToggleValue()}.
+ *
+ * @see #getAutofillType()
+ */
+ public static final int AUTOFILL_TYPE_TOGGLE = 2;
+
+ /**
+ * Autofill type for a selection list field, which is filled by an {@code int}
+ * representing the element index inside the list (starting at {@code 0}).
+ *
+ * <p>{@link AutofillValue} instances for autofilling a {@link View} can be obtained through
+ * {@link AutofillValue#forList(int)}, and the value passed to autofill a
+ * {@link View} can be fetched through {@link AutofillValue#getListValue()}.
+ *
+ * <p>The available options in the selection list are typically provided by
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillOptions()}.
+ *
+ * @see #getAutofillType()
+ */
+ public static final int AUTOFILL_TYPE_LIST = 3;
+
+ /**
+ * Autofill type for a field that contains a date, which is represented by a long representing
+ * the number of milliseconds since the standard base time known as "the epoch", namely
+ * January 1, 1970, 00:00:00 GMT (see {@link java.util.Date#getTime()}.
+ *
+ * <p>{@link AutofillValue} instances for autofilling a {@link View} can be obtained through
+ * {@link AutofillValue#forDate(long)}, and the values passed to
+ * autofill a {@link View} can be fetched through {@link AutofillValue#getDateValue()}.
+ *
+ * @see #getAutofillType()
+ */
+ public static final int AUTOFILL_TYPE_DATE = 4;
+
+
+ /** @hide */
+ @IntDef(prefix = { "IMPORTANT_FOR_AUTOFILL_" }, value = {
+ IMPORTANT_FOR_AUTOFILL_AUTO,
+ IMPORTANT_FOR_AUTOFILL_YES,
+ IMPORTANT_FOR_AUTOFILL_NO,
+ IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS,
+ IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AutofillImportance {}
+
+ /**
+ * Automatically determine whether a view is important for autofill.
+ *
+ * @see #isImportantForAutofill()
+ * @see #setImportantForAutofill(int)
+ */
+ public static final int IMPORTANT_FOR_AUTOFILL_AUTO = 0x0;
+
+ /**
+ * The view is important for autofill, and its children (if any) will be traversed.
+ *
+ * @see #isImportantForAutofill()
+ * @see #setImportantForAutofill(int)
+ */
+ public static final int IMPORTANT_FOR_AUTOFILL_YES = 0x1;
+
+ /**
+ * The view is not important for autofill, but its children (if any) will be traversed.
+ *
+ * @see #isImportantForAutofill()
+ * @see #setImportantForAutofill(int)
+ */
+ public static final int IMPORTANT_FOR_AUTOFILL_NO = 0x2;
+
+ /**
+ * The view is important for autofill, but its children (if any) will not be traversed.
+ *
+ * @see #isImportantForAutofill()
+ * @see #setImportantForAutofill(int)
+ */
+ public static final int IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS = 0x4;
+
+ /**
+ * The view is not important for autofill, and its children (if any) will not be traversed.
+ *
+ * @see #isImportantForAutofill()
+ * @see #setImportantForAutofill(int)
+ */
+ public static final int IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS = 0x8;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "AUTOFILL_FLAG_" }, value = {
+ AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AutofillFlags {}
+
+ /**
+ * Flag requesting you to add views that are marked as not important for autofill
+ * (see {@link #setImportantForAutofill(int)}) to a {@link ViewStructure}.
+ */
+ public static final int AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x1;
+
+ /** @hide */
+ @IntDef(prefix = { "IMPORTANT_FOR_CONTENT_CAPTURE_" }, value = {
+ IMPORTANT_FOR_CONTENT_CAPTURE_AUTO,
+ IMPORTANT_FOR_CONTENT_CAPTURE_YES,
+ IMPORTANT_FOR_CONTENT_CAPTURE_NO,
+ IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS,
+ IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ContentCaptureImportance {}
+
+ /**
+ * Automatically determine whether a view is important for content capture.
+ *
+ * @see #isImportantForContentCapture()
+ * @see #setImportantForContentCapture(int)
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_AUTO = 0x0;
+
+ /**
+ * The view is important for content capture, and its children (if any) will be traversed.
+ *
+ * @see #isImportantForContentCapture()
+ * @see #setImportantForContentCapture(int)
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_YES = 0x1;
+
+ /**
+ * The view is not important for content capture, but its children (if any) will be traversed.
+ *
+ * @see #isImportantForContentCapture()
+ * @see #setImportantForContentCapture(int)
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO = 0x2;
+
+ /**
+ * The view is important for content capture, but its children (if any) will not be traversed.
+ *
+ * @see #isImportantForContentCapture()
+ * @see #setImportantForContentCapture(int)
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS = 0x4;
+
+ /**
+ * The view is not important for content capture, and its children (if any) will not be
+ * traversed.
+ *
+ * @see #isImportantForContentCapture()
+ * @see #setImportantForContentCapture(int)
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8;
+
+ /** {@hide} */
+ @IntDef(flag = true, prefix = {"SCROLL_CAPTURE_HINT_"},
+ value = {
+ SCROLL_CAPTURE_HINT_AUTO,
+ SCROLL_CAPTURE_HINT_EXCLUDE,
+ SCROLL_CAPTURE_HINT_INCLUDE,
+ SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScrollCaptureHint {}
+
+ /**
+ * The content of this view will be considered for scroll capture if scrolling is possible.
+ *
+ * @see #getScrollCaptureHint()
+ * @see #setScrollCaptureHint(int)
+ */
+ public static final int SCROLL_CAPTURE_HINT_AUTO = 0;
+
+ /**
+ * Explicitly exclude this view as a potential scroll capture target. The system will not
+ * consider it. Mutually exclusive with {@link #SCROLL_CAPTURE_HINT_INCLUDE}, which this flag
+ * takes precedence over.
+ *
+ * @see #getScrollCaptureHint()
+ * @see #setScrollCaptureHint(int)
+ */
+ public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1;
+
+ /**
+ * Explicitly include this view as a potential scroll capture target. When locating a scroll
+ * capture target, this view will be prioritized before others without this flag. Mutually
+ * exclusive with {@link #SCROLL_CAPTURE_HINT_EXCLUDE}, which takes precedence.
+ *
+ * @see #getScrollCaptureHint()
+ * @see #setScrollCaptureHint(int)
+ */
+ public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2;
+
+ /**
+ * Explicitly exclude all children of this view as potential scroll capture targets. This view
+ * is unaffected. Note: Excluded children are not considered, regardless of {@link
+ * #SCROLL_CAPTURE_HINT_INCLUDE}.
+ *
+ * @see #getScrollCaptureHint()
+ * @see #setScrollCaptureHint(int)
+ */
+ public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4;
+
+ /**
+ * This view is enabled. Interpretation varies by subclass.
+ * Use with ENABLED_MASK when calling setFlags.
+ * {@hide}
+ */
+ static final int ENABLED = 0x00000000;
+
+ /**
+ * This view is disabled. Interpretation varies by subclass.
+ * Use with ENABLED_MASK when calling setFlags.
+ * {@hide}
+ */
+ static final int DISABLED = 0x00000020;
+
+ /**
+ * Mask for use with setFlags indicating bits used for indicating whether
+ * this view is enabled
+ * {@hide}
+ */
+ static final int ENABLED_MASK = 0x00000020;
+
+ /**
+ * This view won't draw. {@link #onDraw(android.graphics.Canvas)} won't be
+ * called and further optimizations will be performed. It is okay to have
+ * this flag set and a background. Use with DRAW_MASK when calling setFlags.
+ * {@hide}
+ */
+ static final int WILL_NOT_DRAW = 0x00000080;
+
+ /**
+ * Mask for use with setFlags indicating bits used for indicating whether
+ * this view is will draw
+ * {@hide}
+ */
+ static final int DRAW_MASK = 0x00000080;
+
+ /**
+ * <p>This view doesn't show scrollbars.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_NONE = 0x00000000;
+
+ /**
+ * <p>This view shows horizontal scrollbars.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_HORIZONTAL = 0x00000100;
+
+ /**
+ * <p>This view shows vertical scrollbars.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_VERTICAL = 0x00000200;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for indicating which
+ * scrollbars are enabled.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_MASK = 0x00000300;
+
+ /**
+ * Indicates that the view should filter touches when its window is obscured.
+ * Refer to the class comments for more information about this security feature.
+ * {@hide}
+ */
+ static final int FILTER_TOUCHES_WHEN_OBSCURED = 0x00000400;
+
+ /**
+ * Set for framework elements that use FITS_SYSTEM_WINDOWS, to indicate
+ * that they are optional and should be skipped if the window has
+ * requested system UI flags that ignore those insets for layout.
+ * <p>
+ * This is only used for support library as of Android R. The framework now uses
+ * {@link #PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS} such that it can skip the legacy
+ * insets path that loses insets information.
+ */
+ static final int OPTIONAL_FITS_SYSTEM_WINDOWS = 0x00000800;
+
+ /**
+ * <p>This view doesn't show fading edges.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_NONE = 0x00000000;
+
+ /**
+ * <p>This view shows horizontal fading edges.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_HORIZONTAL = 0x00001000;
+
+ /**
+ * <p>This view shows vertical fading edges.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_VERTICAL = 0x00002000;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for indicating which
+ * fading edges are enabled.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_MASK = 0x00003000;
+
+ /**
+ * <p>Indicates this view can be clicked. When clickable, a View reacts
+ * to clicks by notifying the OnClickListener.<p>
+ * {@hide}
+ */
+ static final int CLICKABLE = 0x00004000;
+
+ /**
+ * <p>Indicates this view is caching its drawing into a bitmap.</p>
+ * {@hide}
+ */
+ static final int DRAWING_CACHE_ENABLED = 0x00008000;
+
+ /**
+ * <p>Indicates that no icicle should be saved for this view.<p>
+ * {@hide}
+ */
+ static final int SAVE_DISABLED = 0x000010000;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for the saveEnabled
+ * property.</p>
+ * {@hide}
+ */
+ static final int SAVE_DISABLED_MASK = 0x000010000;
+
+ /**
+ * <p>Indicates that no drawing cache should ever be created for this view.<p>
+ * {@hide}
+ */
+ static final int WILL_NOT_CACHE_DRAWING = 0x000020000;
+
+ /**
+ * <p>Indicates this view can take / keep focus when int touch mode.</p>
+ * {@hide}
+ */
+ static final int FOCUSABLE_IN_TOUCH_MODE = 0x00040000;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "DRAWING_CACHE_QUALITY_" }, value = {
+ DRAWING_CACHE_QUALITY_LOW,
+ DRAWING_CACHE_QUALITY_HIGH,
+ DRAWING_CACHE_QUALITY_AUTO
+ })
+ public @interface DrawingCacheQuality {}
+
+ /**
+ * <p>Enables low quality mode for the drawing cache.</p>
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int DRAWING_CACHE_QUALITY_LOW = 0x00080000;
+
+ /**
+ * <p>Enables high quality mode for the drawing cache.</p>
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int DRAWING_CACHE_QUALITY_HIGH = 0x00100000;
+
+ /**
+ * <p>Enables automatic quality mode for the drawing cache.</p>
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int DRAWING_CACHE_QUALITY_AUTO = 0x00000000;
+
+ private static final int[] DRAWING_CACHE_QUALITY_FLAGS = {
+ DRAWING_CACHE_QUALITY_AUTO, DRAWING_CACHE_QUALITY_LOW, DRAWING_CACHE_QUALITY_HIGH
+ };
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for the cache
+ * quality property.</p>
+ * {@hide}
+ */
+ static final int DRAWING_CACHE_QUALITY_MASK = 0x00180000;
+
+ /**
+ * <p>
+ * Indicates this view can be long clicked. When long clickable, a View
+ * reacts to long clicks by notifying the OnLongClickListener or showing a
+ * context menu.
+ * </p>
+ * {@hide}
+ */
+ static final int LONG_CLICKABLE = 0x00200000;
+
+ /**
+ * <p>Indicates that this view gets its drawable states from its direct parent
+ * and ignores its original internal states.</p>
+ *
+ * @hide
+ */
+ static final int DUPLICATE_PARENT_STATE = 0x00400000;
+
+ /**
+ * <p>
+ * Indicates this view can be context clicked. When context clickable, a View reacts to a
+ * context click (e.g. a primary stylus button press or right mouse click) by notifying the
+ * OnContextClickListener.
+ * </p>
+ * {@hide}
+ */
+ static final int CONTEXT_CLICKABLE = 0x00800000;
+
+ /** @hide */
+ @IntDef(prefix = { "SCROLLBARS_" }, value = {
+ SCROLLBARS_INSIDE_OVERLAY,
+ SCROLLBARS_INSIDE_INSET,
+ SCROLLBARS_OUTSIDE_OVERLAY,
+ SCROLLBARS_OUTSIDE_INSET
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScrollBarStyle {}
+
+ /**
+ * The scrollbar style to display the scrollbars inside the content area,
+ * without increasing the padding. The scrollbars will be overlaid with
+ * translucency on the view's content.
+ */
+ public static final int SCROLLBARS_INSIDE_OVERLAY = 0;
+
+ /**
+ * The scrollbar style to display the scrollbars inside the padded area,
+ * increasing the padding of the view. The scrollbars will not overlap the
+ * content area of the view.
+ */
+ public static final int SCROLLBARS_INSIDE_INSET = 0x01000000;
+
+ /**
+ * The scrollbar style to display the scrollbars at the edge of the view,
+ * without increasing the padding. The scrollbars will be overlaid with
+ * translucency.
+ */
+ public static final int SCROLLBARS_OUTSIDE_OVERLAY = 0x02000000;
+
+ /**
+ * The scrollbar style to display the scrollbars at the edge of the view,
+ * increasing the padding of the view. The scrollbars will only overlap the
+ * background, if any.
+ */
+ public static final int SCROLLBARS_OUTSIDE_INSET = 0x03000000;
+
+ /**
+ * Mask to check if the scrollbar style is overlay or inset.
+ * {@hide}
+ */
+ static final int SCROLLBARS_INSET_MASK = 0x01000000;
+
+ /**
+ * Mask to check if the scrollbar style is inside or outside.
+ * {@hide}
+ */
+ static final int SCROLLBARS_OUTSIDE_MASK = 0x02000000;
+
+ /**
+ * Mask for scrollbar style.
+ * {@hide}
+ */
+ static final int SCROLLBARS_STYLE_MASK = 0x03000000;
+
+ /**
+ * View flag indicating that the screen should remain on while the
+ * window containing this view is visible to the user. This effectively
+ * takes care of automatically setting the WindowManager's
+ * {@link WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON}.
+ */
+ public static final int KEEP_SCREEN_ON = 0x04000000;
+
+ /**
+ * View flag indicating whether this view should have sound effects enabled
+ * for events such as clicking and touching.
+ */
+ public static final int SOUND_EFFECTS_ENABLED = 0x08000000;
+
+ /**
+ * View flag indicating whether this view should have haptic feedback
+ * enabled for events such as long presses.
+ */
+ public static final int HAPTIC_FEEDBACK_ENABLED = 0x10000000;
+
+ /**
+ * <p>Indicates that the view hierarchy should stop saving state when
+ * it reaches this view. If state saving is initiated immediately at
+ * the view, it will be allowed.
+ * {@hide}
+ */
+ static final int PARENT_SAVE_DISABLED = 0x20000000;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for PARENT_SAVE_DISABLED.</p>
+ * {@hide}
+ */
+ static final int PARENT_SAVE_DISABLED_MASK = 0x20000000;
+
+ private static Paint sDebugPaint;
+
+ /**
+ * <p>Indicates this view can display a tooltip on hover or long press.</p>
+ * {@hide}
+ */
+ static final int TOOLTIP = 0x40000000;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = {
+ FOCUSABLES_ALL,
+ FOCUSABLES_TOUCH_MODE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FocusableMode {}
+
+ /**
+ * View flag indicating whether {@link #addFocusables(ArrayList, int, int)}
+ * should add all focusable Views regardless if they are focusable in touch mode.
+ */
+ public static final int FOCUSABLES_ALL = 0x00000000;
+
+ /**
+ * View flag indicating whether {@link #addFocusables(ArrayList, int, int)}
+ * should add only Views focusable in touch mode.
+ */
+ public static final int FOCUSABLES_TOUCH_MODE = 0x00000001;
+
+ /** @hide */
+ @IntDef(prefix = { "FOCUS_" }, value = {
+ FOCUS_BACKWARD,
+ FOCUS_FORWARD,
+ FOCUS_LEFT,
+ FOCUS_UP,
+ FOCUS_RIGHT,
+ FOCUS_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FocusDirection {}
+
+ /** @hide */
+ @IntDef(prefix = { "FOCUS_" }, value = {
+ FOCUS_LEFT,
+ FOCUS_UP,
+ FOCUS_RIGHT,
+ FOCUS_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FocusRealDirection {} // Like @FocusDirection, but without forward/backward
+
+ /**
+ * Use with {@link #focusSearch(int)}. Move focus to the previous selectable
+ * item.
+ */
+ public static final int FOCUS_BACKWARD = 0x00000001;
+
+ /**
+ * Use with {@link #focusSearch(int)}. Move focus to the next selectable
+ * item.
+ */
+ public static final int FOCUS_FORWARD = 0x00000002;
+
+ /**
+ * Use with {@link #focusSearch(int)}. Move focus to the left.
+ */
+ public static final int FOCUS_LEFT = 0x00000011;
+
+ /**
+ * Use with {@link #focusSearch(int)}. Move focus up.
+ */
+ public static final int FOCUS_UP = 0x00000021;
+
+ /**
+ * Use with {@link #focusSearch(int)}. Move focus to the right.
+ */
+ public static final int FOCUS_RIGHT = 0x00000042;
+
+ /**
+ * Use with {@link #focusSearch(int)}. Move focus down.
+ */
+ public static final int FOCUS_DOWN = 0x00000082;
+
+ /**
+ * Bits of {@link #getMeasuredWidthAndState()} and
+ * {@link #getMeasuredWidthAndState()} that provide the actual measured size.
+ */
+ public static final int MEASURED_SIZE_MASK = 0x00ffffff;
+
+ /**
+ * Bits of {@link #getMeasuredWidthAndState()} and
+ * {@link #getMeasuredWidthAndState()} that provide the additional state bits.
+ */
+ public static final int MEASURED_STATE_MASK = 0xff000000;
+
+ /**
+ * Bit shift of {@link #MEASURED_STATE_MASK} to get to the height bits
+ * for functions that combine both width and height into a single int,
+ * such as {@link #getMeasuredState()} and the childState argument of
+ * {@link #resolveSizeAndState(int, int, int)}.
+ */
+ public static final int MEASURED_HEIGHT_STATE_SHIFT = 16;
+
+ /**
+ * Bit of {@link #getMeasuredWidthAndState()} and
+ * {@link #getMeasuredWidthAndState()} that indicates the measured size
+ * is smaller that the space the view would like to have.
+ */
+ public static final int MEASURED_STATE_TOO_SMALL = 0x01000000;
+
+ /**
+ * Base View state sets
+ */
+ // Singles
+ /**
+ * Indicates the view has no states set. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] EMPTY_STATE_SET;
+ /**
+ * Indicates the view is enabled. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] ENABLED_STATE_SET;
+ /**
+ * Indicates the view is focused. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is selected. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] SELECTED_STATE_SET;
+ /**
+ * Indicates the view is pressed. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] PRESSED_STATE_SET;
+ /**
+ * Indicates the view's window has focus. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] WINDOW_FOCUSED_STATE_SET;
+ // Doubles
+ /**
+ * Indicates the view is enabled and has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is enabled and selected.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] ENABLED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view is enabled and that its window has focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is focused and selected.
+ *
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] FOCUSED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view has the focus and that its window has the focus.
+ *
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] FOCUSED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is selected and that its window has the focus.
+ *
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] SELECTED_WINDOW_FOCUSED_STATE_SET;
+ // Triples
+ /**
+ * Indicates the view is enabled, focused and selected.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view is enabled, focused and its window has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is enabled, selected and its window has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is focused, selected and its window has the focus.
+ *
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is enabled, focused, selected and its window
+ * has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] PRESSED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view is pressed, selected and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed and focused.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed, focused and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed, focused and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view is pressed, focused, selected and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed and enabled.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled, selected and its window has the
+ * focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled and focused.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled, focused and its window has the
+ * focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled, focused and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET;
+ /**
+ * Indicates the view is pressed, enabled, focused, selected and its window
+ * has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
+
+ static {
+ EMPTY_STATE_SET = StateSet.get(0);
+
+ WINDOW_FOCUSED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_WINDOW_FOCUSED);
+
+ SELECTED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_SELECTED);
+ SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED);
+
+ FOCUSED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_FOCUSED);
+ FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED);
+ FOCUSED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED);
+ FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_FOCUSED);
+
+ ENABLED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_ENABLED);
+ ENABLED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_ENABLED);
+ ENABLED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_ENABLED);
+ ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_ENABLED);
+ ENABLED_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_ENABLED);
+ ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED
+ | StateSet.VIEW_STATE_ENABLED);
+ ENABLED_FOCUSED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED
+ | StateSet.VIEW_STATE_ENABLED);
+ ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_FOCUSED| StateSet.VIEW_STATE_ENABLED);
+
+ PRESSED_STATE_SET = StateSet.get(StateSet.VIEW_STATE_PRESSED);
+ PRESSED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED
+ | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_FOCUSED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED
+ | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_ENABLED
+ | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_ENABLED
+ | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_FOCUSED | StateSet.VIEW_STATE_ENABLED
+ | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_FOCUSED
+ | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_SELECTED | StateSet.VIEW_STATE_FOCUSED
+ | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED);
+ PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET = StateSet.get(
+ StateSet.VIEW_STATE_WINDOW_FOCUSED | StateSet.VIEW_STATE_SELECTED
+ | StateSet.VIEW_STATE_FOCUSED| StateSet.VIEW_STATE_ENABLED
+ | StateSet.VIEW_STATE_PRESSED);
+ }
+
+ /**
+ * Accessibility event types that are dispatched for text population.
+ */
+ private static final int POPULATING_ACCESSIBILITY_EVENT_TYPES =
+ AccessibilityEvent.TYPE_VIEW_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_SELECTED
+ | AccessibilityEvent.TYPE_VIEW_FOCUSED
+ | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+ | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
+
+ static final int DEBUG_CORNERS_COLOR = Color.rgb(63, 127, 255);
+
+ static final int DEBUG_CORNERS_SIZE_DIP = 8;
+
+ /**
+ * Temporary Rect currently for use in setBackground(). This will probably
+ * be extended in the future to hold our own class with more than just
+ * a Rect. :)
+ */
+ static final ThreadLocal<Rect> sThreadLocal = ThreadLocal.withInitial(Rect::new);
+
+ /**
+ * Map used to store views' tags.
+ */
+ @UnsupportedAppUsage
+ private SparseArray<Object> mKeyedTags;
+
+ /**
+ * The next available accessibility id.
+ */
+ private static int sNextAccessibilityViewId;
+
+ /**
+ * The animation currently associated with this view.
+ * @hide
+ */
+ protected Animation mCurrentAnimation = null;
+
+ /**
+ * Width as measured during measure pass.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "measurement")
+ @UnsupportedAppUsage
+ int mMeasuredWidth;
+
+ /**
+ * Height as measured during measure pass.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "measurement")
+ @UnsupportedAppUsage
+ int mMeasuredHeight;
+
+ /**
+ * Flag to indicate that this view was marked INVALIDATED, or had its display list
+ * invalidated, prior to the current drawing iteration. If true, the view must re-draw
+ * its display list. This flag, used only when hw accelerated, allows us to clear the
+ * flag while retaining this information until it's needed (at getDisplayList() time and
+ * in drawChild(), when we decide to draw a view's children's display lists into our own).
+ *
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ boolean mRecreateDisplayList = false;
+
+ /**
+ * The view's identifier.
+ * {@hide}
+ *
+ * @see #setId(int)
+ * @see #getId()
+ */
+ @IdRes
+ @ViewDebug.ExportedProperty(resolveId = true)
+ int mID = NO_ID;
+
+ /** The ID of this view for autofill purposes.
+ * <ul>
+ * <li>== {@link #NO_ID}: ID has not been assigned yet
+ * <li>≤ {@link #LAST_APP_AUTOFILL_ID}: View is not part of a activity. The ID is
+ * unique in the process. This might change
+ * over activity lifecycle events.
+ * <li>> {@link #LAST_APP_AUTOFILL_ID}: View is part of a activity. The ID is
+ * unique in the activity. This stays the same
+ * over activity lifecycle events.
+ */
+ private int mAutofillViewId = NO_ID;
+
+ // ID for accessibility purposes. This ID must be unique for every window
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mAccessibilityViewId = NO_ID;
+
+ private int mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
+
+ /**
+ * The view's tag.
+ * {@hide}
+ *
+ * @see #setTag(Object)
+ * @see #getTag()
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected Object mTag = null;
+
+ /*
+ * Masks for mPrivateFlags, as generated by dumpFlags():
+ *
+ * |-------|-------|-------|-------|
+ * 1 PFLAG_WANTS_FOCUS
+ * 1 PFLAG_FOCUSED
+ * 1 PFLAG_SELECTED
+ * 1 PFLAG_IS_ROOT_NAMESPACE
+ * 1 PFLAG_HAS_BOUNDS
+ * 1 PFLAG_DRAWN
+ * 1 PFLAG_DRAW_ANIMATION
+ * 1 PFLAG_SKIP_DRAW
+ * 1 PFLAG_REQUEST_TRANSPARENT_REGIONS
+ * 1 PFLAG_DRAWABLE_STATE_DIRTY
+ * 1 PFLAG_MEASURED_DIMENSION_SET
+ * 1 PFLAG_FORCE_LAYOUT
+ * 1 PFLAG_LAYOUT_REQUIRED
+ * 1 PFLAG_PRESSED
+ * 1 PFLAG_DRAWING_CACHE_VALID
+ * 1 PFLAG_ANIMATION_STARTED
+ * 1 PFLAG_SAVE_STATE_CALLED
+ * 1 PFLAG_ALPHA_SET
+ * 1 PFLAG_SCROLL_CONTAINER
+ * 1 PFLAG_SCROLL_CONTAINER_ADDED
+ * 1 PFLAG_DIRTY
+ * 1 PFLAG_DIRTY_MASK
+ * 1 PFLAG_OPAQUE_BACKGROUND
+ * 1 PFLAG_OPAQUE_SCROLLBARS
+ * 11 PFLAG_OPAQUE_MASK
+ * 1 PFLAG_PREPRESSED
+ * 1 PFLAG_CANCEL_NEXT_UP_EVENT
+ * 1 PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH
+ * 1 PFLAG_HOVERED
+ * 1 PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK
+ * 1 PFLAG_ACTIVATED
+ * 1 PFLAG_INVALIDATED
+ * |-------|-------|-------|-------|
+ */
+ /** {@hide} */
+ static final int PFLAG_WANTS_FOCUS = 0x00000001;
+ /** {@hide} */
+ static final int PFLAG_FOCUSED = 0x00000002;
+ /** {@hide} */
+ static final int PFLAG_SELECTED = 0x00000004;
+ /** {@hide} */
+ static final int PFLAG_IS_ROOT_NAMESPACE = 0x00000008;
+ /** {@hide} */
+ static final int PFLAG_HAS_BOUNDS = 0x00000010;
+ /** {@hide} */
+ static final int PFLAG_DRAWN = 0x00000020;
+ /**
+ * When this flag is set, this view is running an animation on behalf of its
+ * children and should therefore not cancel invalidate requests, even if they
+ * lie outside of this view's bounds.
+ *
+ * {@hide}
+ */
+ static final int PFLAG_DRAW_ANIMATION = 0x00000040;
+ /** {@hide} */
+ static final int PFLAG_SKIP_DRAW = 0x00000080;
+ /** {@hide} */
+ static final int PFLAG_REQUEST_TRANSPARENT_REGIONS = 0x00000200;
+ /** {@hide} */
+ static final int PFLAG_DRAWABLE_STATE_DIRTY = 0x00000400;
+ /** {@hide} */
+ static final int PFLAG_MEASURED_DIMENSION_SET = 0x00000800;
+ /** {@hide} */
+ static final int PFLAG_FORCE_LAYOUT = 0x00001000;
+ /** {@hide} */
+ static final int PFLAG_LAYOUT_REQUIRED = 0x00002000;
+
+ private static final int PFLAG_PRESSED = 0x00004000;
+
+ /** {@hide} */
+ static final int PFLAG_DRAWING_CACHE_VALID = 0x00008000;
+ /**
+ * Flag used to indicate that this view should be drawn once more (and only once
+ * more) after its animation has completed.
+ * {@hide}
+ */
+ static final int PFLAG_ANIMATION_STARTED = 0x00010000;
+
+ private static final int PFLAG_SAVE_STATE_CALLED = 0x00020000;
+
+ /**
+ * Indicates that the View returned true when onSetAlpha() was called and that
+ * the alpha must be restored.
+ * {@hide}
+ */
+ static final int PFLAG_ALPHA_SET = 0x00040000;
+
+ /**
+ * Set by {@link #setScrollContainer(boolean)}.
+ */
+ static final int PFLAG_SCROLL_CONTAINER = 0x00080000;
+
+ /**
+ * Set by {@link #setScrollContainer(boolean)}.
+ */
+ static final int PFLAG_SCROLL_CONTAINER_ADDED = 0x00100000;
+
+ /**
+ * View flag indicating whether this view was invalidated (fully or partially.)
+ *
+ * @hide
+ */
+ static final int PFLAG_DIRTY = 0x00200000;
+
+ /**
+ * Mask for {@link #PFLAG_DIRTY}.
+ *
+ * @hide
+ */
+ static final int PFLAG_DIRTY_MASK = 0x00200000;
+
+ /**
+ * Indicates whether the background is opaque.
+ *
+ * @hide
+ */
+ static final int PFLAG_OPAQUE_BACKGROUND = 0x00800000;
+
+ /**
+ * Indicates whether the scrollbars are opaque.
+ *
+ * @hide
+ */
+ static final int PFLAG_OPAQUE_SCROLLBARS = 0x01000000;
+
+ /**
+ * Indicates whether the view is opaque.
+ *
+ * @hide
+ */
+ static final int PFLAG_OPAQUE_MASK = 0x01800000;
+
+ /**
+ * Indicates a prepressed state;
+ * the short time between ACTION_DOWN and recognizing
+ * a 'real' press. Prepressed is used to recognize quick taps
+ * even when they are shorter than ViewConfiguration.getTapTimeout().
+ *
+ * @hide
+ */
+ private static final int PFLAG_PREPRESSED = 0x02000000;
+
+ /**
+ * Indicates whether the view is temporarily detached.
+ *
+ * @hide
+ */
+ static final int PFLAG_CANCEL_NEXT_UP_EVENT = 0x04000000;
+
+ /**
+ * Indicates that we should awaken scroll bars once attached
+ *
+ * PLEASE NOTE: This flag is now unused as we now send onVisibilityChanged
+ * during window attachment and it is no longer needed. Feel free to repurpose it.
+ *
+ * @hide
+ */
+ private static final int PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH = 0x08000000;
+
+ /**
+ * Indicates that the view has received HOVER_ENTER. Cleared on HOVER_EXIT.
+ * @hide
+ */
+ private static final int PFLAG_HOVERED = 0x10000000;
+
+ /**
+ * Flag set by {@link AutofillManager} if it needs to be notified when this view is clicked.
+ */
+ private static final int PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK = 0x20000000;
+
+ /** {@hide} */
+ static final int PFLAG_ACTIVATED = 0x40000000;
+
+ /**
+ * Indicates that this view was specifically invalidated, not just dirtied because some
+ * child view was invalidated. The flag is used to determine when we need to recreate
+ * a view's display list (as opposed to just returning a reference to its existing
+ * display list).
+ *
+ * @hide
+ */
+ static final int PFLAG_INVALIDATED = 0x80000000;
+
+ /* End of masks for mPrivateFlags */
+
+ /*
+ * Masks for mPrivateFlags2, as generated by dumpFlags():
+ *
+ * |-------|-------|-------|-------|
+ * 1 PFLAG2_DRAG_CAN_ACCEPT
+ * 1 PFLAG2_DRAG_HOVERED
+ * 11 PFLAG2_LAYOUT_DIRECTION_MASK
+ * 1 PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL
+ * 1 PFLAG2_LAYOUT_DIRECTION_RESOLVED
+ * 11 PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK
+ * 1 PFLAG2_TEXT_DIRECTION_FLAGS[1]
+ * 1 PFLAG2_TEXT_DIRECTION_FLAGS[2]
+ * 11 PFLAG2_TEXT_DIRECTION_FLAGS[3]
+ * 1 PFLAG2_TEXT_DIRECTION_FLAGS[4]
+ * 1 1 PFLAG2_TEXT_DIRECTION_FLAGS[5]
+ * 11 PFLAG2_TEXT_DIRECTION_FLAGS[6]
+ * 111 PFLAG2_TEXT_DIRECTION_FLAGS[7]
+ * 111 PFLAG2_TEXT_DIRECTION_MASK
+ * 1 PFLAG2_TEXT_DIRECTION_RESOLVED
+ * 1 PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT
+ * 111 PFLAG2_TEXT_DIRECTION_RESOLVED_MASK
+ * 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[1]
+ * 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[2]
+ * 11 PFLAG2_TEXT_ALIGNMENT_FLAGS[3]
+ * 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[4]
+ * 1 1 PFLAG2_TEXT_ALIGNMENT_FLAGS[5]
+ * 11 PFLAG2_TEXT_ALIGNMENT_FLAGS[6]
+ * 111 PFLAG2_TEXT_ALIGNMENT_MASK
+ * 1 PFLAG2_TEXT_ALIGNMENT_RESOLVED
+ * 1 PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT
+ * 111 PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK
+ * 111 PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK
+ * 11 PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK
+ * 1 PFLAG2_ACCESSIBILITY_FOCUSED
+ * 1 PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED
+ * 1 PFLAG2_VIEW_QUICK_REJECTED
+ * 1 PFLAG2_PADDING_RESOLVED
+ * 1 PFLAG2_DRAWABLE_RESOLVED
+ * 1 PFLAG2_HAS_TRANSIENT_STATE
+ * |-------|-------|-------|-------|
+ */
+
+ /**
+ * Indicates that this view has reported that it can accept the current drag's content.
+ * Cleared when the drag operation concludes.
+ * @hide
+ */
+ static final int PFLAG2_DRAG_CAN_ACCEPT = 0x00000001;
+
+ /**
+ * Indicates that this view is currently directly under the drag location in a
+ * drag-and-drop operation involving content that it can accept. Cleared when
+ * the drag exits the view, or when the drag operation concludes.
+ * @hide
+ */
+ static final int PFLAG2_DRAG_HOVERED = 0x00000002;
+
+ /** @hide */
+ @IntDef(prefix = { "LAYOUT_DIRECTION_" }, value = {
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL,
+ LAYOUT_DIRECTION_INHERIT,
+ LAYOUT_DIRECTION_LOCALE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ // Not called LayoutDirection to avoid conflict with android.util.LayoutDirection
+ public @interface LayoutDir {}
+
+ /** @hide */
+ @IntDef(prefix = { "LAYOUT_DIRECTION_" }, value = {
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResolvedLayoutDir {}
+
+ /**
+ * A flag to indicate that the layout direction of this view has not been defined yet.
+ * @hide
+ */
+ public static final int LAYOUT_DIRECTION_UNDEFINED = LayoutDirection.UNDEFINED;
+
+ /**
+ * Horizontal layout direction of this view is from Left to Right.
+ * Use with {@link #setLayoutDirection}.
+ */
+ public static final int LAYOUT_DIRECTION_LTR = LayoutDirection.LTR;
+
+ /**
+ * Horizontal layout direction of this view is from Right to Left.
+ * Use with {@link #setLayoutDirection}.
+ */
+ public static final int LAYOUT_DIRECTION_RTL = LayoutDirection.RTL;
+
+ /**
+ * Horizontal layout direction of this view is inherited from its parent.
+ * Use with {@link #setLayoutDirection}.
+ */
+ public static final int LAYOUT_DIRECTION_INHERIT = LayoutDirection.INHERIT;
+
+ /**
+ * Horizontal layout direction of this view is from deduced from the default language
+ * script for the locale. Use with {@link #setLayoutDirection}.
+ */
+ public static final int LAYOUT_DIRECTION_LOCALE = LayoutDirection.LOCALE;
+
+ /**
+ * Bit shift to get the horizontal layout direction. (bits after DRAG_HOVERED)
+ * @hide
+ */
+ static final int PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT = 2;
+
+ /**
+ * Mask for use with private flags indicating bits used for horizontal layout direction.
+ * @hide
+ */
+ static final int PFLAG2_LAYOUT_DIRECTION_MASK = 0x00000003 << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT;
+
+ /**
+ * Indicates whether the view horizontal layout direction has been resolved and drawn to the
+ * right-to-left direction.
+ * @hide
+ */
+ static final int PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL = 4 << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT;
+
+ /**
+ * Indicates whether the view horizontal layout direction has been resolved.
+ * @hide
+ */
+ static final int PFLAG2_LAYOUT_DIRECTION_RESOLVED = 8 << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT;
+
+ /**
+ * Mask for use with private flags indicating bits used for resolved horizontal layout direction.
+ * @hide
+ */
+ static final int PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK = 0x0000000C
+ << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT;
+
+ /*
+ * Array of horizontal layout direction flags for mapping attribute "layoutDirection" to correct
+ * flag value.
+ * @hide
+ */
+ private static final int[] LAYOUT_DIRECTION_FLAGS = {
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL,
+ LAYOUT_DIRECTION_INHERIT,
+ LAYOUT_DIRECTION_LOCALE
+ };
+
+ /**
+ * Default horizontal layout direction.
+ */
+ private static final int LAYOUT_DIRECTION_DEFAULT = LAYOUT_DIRECTION_INHERIT;
+
+ /**
+ * Default horizontal layout direction.
+ * @hide
+ */
+ static final int LAYOUT_DIRECTION_RESOLVED_DEFAULT = LAYOUT_DIRECTION_LTR;
+
+ /**
+ * Text direction is inherited through {@link ViewGroup}
+ */
+ public static final int TEXT_DIRECTION_INHERIT = 0;
+
+ /**
+ * Text direction is using "first strong algorithm". The first strong directional character
+ * determines the paragraph direction. If there is no strong directional character, the
+ * paragraph direction is the view's resolved layout direction.
+ */
+ public static final int TEXT_DIRECTION_FIRST_STRONG = 1;
+
+ /**
+ * Text direction is using "any-RTL" algorithm. The paragraph direction is RTL if it contains
+ * any strong RTL character, otherwise it is LTR if it contains any strong LTR characters.
+ * If there are neither, the paragraph direction is the view's resolved layout direction.
+ */
+ public static final int TEXT_DIRECTION_ANY_RTL = 2;
+
+ /**
+ * Text direction is forced to LTR.
+ */
+ public static final int TEXT_DIRECTION_LTR = 3;
+
+ /**
+ * Text direction is forced to RTL.
+ */
+ public static final int TEXT_DIRECTION_RTL = 4;
+
+ /**
+ * Text direction is coming from the system Locale.
+ */
+ public static final int TEXT_DIRECTION_LOCALE = 5;
+
+ /**
+ * Text direction is using "first strong algorithm". The first strong directional character
+ * determines the paragraph direction. If there is no strong directional character, the
+ * paragraph direction is LTR.
+ */
+ public static final int TEXT_DIRECTION_FIRST_STRONG_LTR = 6;
+
+ /**
+ * Text direction is using "first strong algorithm". The first strong directional character
+ * determines the paragraph direction. If there is no strong directional character, the
+ * paragraph direction is RTL.
+ */
+ public static final int TEXT_DIRECTION_FIRST_STRONG_RTL = 7;
+
+ /**
+ * Default text direction is inherited
+ */
+ private static final int TEXT_DIRECTION_DEFAULT = TEXT_DIRECTION_INHERIT;
+
+ /**
+ * Default resolved text direction
+ * @hide
+ */
+ static final int TEXT_DIRECTION_RESOLVED_DEFAULT = TEXT_DIRECTION_FIRST_STRONG;
+
+ /**
+ * Bit shift to get the horizontal layout direction. (bits after LAYOUT_DIRECTION_RESOLVED)
+ * @hide
+ */
+ static final int PFLAG2_TEXT_DIRECTION_MASK_SHIFT = 6;
+
+ /**
+ * Mask for use with private flags indicating bits used for text direction.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_DIRECTION_MASK = 0x00000007
+ << PFLAG2_TEXT_DIRECTION_MASK_SHIFT;
+
+ /**
+ * Array of text direction flags for mapping attribute "textDirection" to correct
+ * flag value.
+ * @hide
+ */
+ private static final int[] PFLAG2_TEXT_DIRECTION_FLAGS = {
+ TEXT_DIRECTION_INHERIT << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_FIRST_STRONG << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_ANY_RTL << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_LTR << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_RTL << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_LOCALE << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_FIRST_STRONG_LTR << PFLAG2_TEXT_DIRECTION_MASK_SHIFT,
+ TEXT_DIRECTION_FIRST_STRONG_RTL << PFLAG2_TEXT_DIRECTION_MASK_SHIFT
+ };
+
+ /**
+ * Indicates whether the view text direction has been resolved.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_DIRECTION_RESOLVED = 0x00000008
+ << PFLAG2_TEXT_DIRECTION_MASK_SHIFT;
+
+ /**
+ * Bit shift to get the horizontal layout direction. (bits after DRAG_HOVERED)
+ * @hide
+ */
+ static final int PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT = 10;
+
+ /**
+ * Mask for use with private flags indicating bits used for resolved text direction.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_DIRECTION_RESOLVED_MASK = 0x00000007
+ << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT;
+
+ /**
+ * Indicates whether the view text direction has been resolved to the "first strong" heuristic.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT =
+ TEXT_DIRECTION_RESOLVED_DEFAULT << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT;
+
+ /** @hide */
+ @IntDef(prefix = { "TEXT_ALIGNMENT_" }, value = {
+ TEXT_ALIGNMENT_INHERIT,
+ TEXT_ALIGNMENT_GRAVITY,
+ TEXT_ALIGNMENT_CENTER,
+ TEXT_ALIGNMENT_TEXT_START,
+ TEXT_ALIGNMENT_TEXT_END,
+ TEXT_ALIGNMENT_VIEW_START,
+ TEXT_ALIGNMENT_VIEW_END
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TextAlignment {}
+
+ /**
+ * Default text alignment. The text alignment of this View is inherited from its parent.
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_INHERIT = 0;
+
+ /**
+ * Default for the root view. The gravity determines the text alignment, ALIGN_NORMAL,
+ * ALIGN_CENTER, or ALIGN_OPPOSITE, which are relative to each paragraph's text direction.
+ *
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_GRAVITY = 1;
+
+ /**
+ * Align to the start of the paragraph, e.g. ALIGN_NORMAL.
+ *
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_TEXT_START = 2;
+
+ /**
+ * Align to the end of the paragraph, e.g. ALIGN_OPPOSITE.
+ *
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_TEXT_END = 3;
+
+ /**
+ * Center the paragraph, e.g. ALIGN_CENTER.
+ *
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_CENTER = 4;
+
+ /**
+ * Align to the start of the view, which is ALIGN_LEFT if the view's resolved
+ * layoutDirection is LTR, and ALIGN_RIGHT otherwise.
+ *
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_VIEW_START = 5;
+
+ /**
+ * Align to the end of the view, which is ALIGN_RIGHT if the view's resolved
+ * layoutDirection is LTR, and ALIGN_LEFT otherwise.
+ *
+ * Use with {@link #setTextAlignment(int)}
+ */
+ public static final int TEXT_ALIGNMENT_VIEW_END = 6;
+
+ /**
+ * Default text alignment is inherited
+ */
+ private static final int TEXT_ALIGNMENT_DEFAULT = TEXT_ALIGNMENT_GRAVITY;
+
+ /**
+ * Default resolved text alignment
+ * @hide
+ */
+ static final int TEXT_ALIGNMENT_RESOLVED_DEFAULT = TEXT_ALIGNMENT_GRAVITY;
+
+ /**
+ * Bit shift to get the horizontal layout direction. (bits after DRAG_HOVERED)
+ * @hide
+ */
+ static final int PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT = 13;
+
+ /**
+ * Mask for use with private flags indicating bits used for text alignment.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_ALIGNMENT_MASK = 0x00000007 << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT;
+
+ /**
+ * Array of text direction flags for mapping attribute "textAlignment" to correct
+ * flag value.
+ * @hide
+ */
+ private static final int[] PFLAG2_TEXT_ALIGNMENT_FLAGS = {
+ TEXT_ALIGNMENT_INHERIT << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT,
+ TEXT_ALIGNMENT_GRAVITY << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT,
+ TEXT_ALIGNMENT_TEXT_START << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT,
+ TEXT_ALIGNMENT_TEXT_END << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT,
+ TEXT_ALIGNMENT_CENTER << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT,
+ TEXT_ALIGNMENT_VIEW_START << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT,
+ TEXT_ALIGNMENT_VIEW_END << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT
+ };
+
+ /**
+ * Indicates whether the view text alignment has been resolved.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_ALIGNMENT_RESOLVED = 0x00000008 << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT;
+
+ /**
+ * Bit shift to get the resolved text alignment.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT = 17;
+
+ /**
+ * Mask for use with private flags indicating bits used for text alignment.
+ * @hide
+ */
+ static final int PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK = 0x00000007
+ << PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT;
+
+ /**
+ * Indicates whether if the view text alignment has been resolved to gravity
+ */
+ private static final int PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT =
+ TEXT_ALIGNMENT_RESOLVED_DEFAULT << PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT;
+
+ // Accessiblity constants for mPrivateFlags2
+
+ /**
+ * Shift for the bits in {@link #mPrivateFlags2} related to the
+ * "importantForAccessibility" attribute.
+ */
+ static final int PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT = 20;
+
+ /**
+ * Automatically determine whether a view is important for accessibility.
+ */
+ public static final int IMPORTANT_FOR_ACCESSIBILITY_AUTO = 0x00000000;
+
+ /**
+ * The view is important for accessibility.
+ */
+ public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 0x00000001;
+
+ /**
+ * The view is not important for accessibility.
+ */
+ public static final int IMPORTANT_FOR_ACCESSIBILITY_NO = 0x00000002;
+
+ /**
+ * The view is not important for accessibility, nor are any of its
+ * descendant views.
+ */
+ public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004;
+
+ /**
+ * The default whether the view is important for accessibility.
+ */
+ static final int IMPORTANT_FOR_ACCESSIBILITY_DEFAULT = IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+
+ /**
+ * Mask for obtaining the bits which specify how to determine
+ * whether a view is important for accessibility.
+ */
+ static final int PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK = (IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ | IMPORTANT_FOR_ACCESSIBILITY_YES | IMPORTANT_FOR_ACCESSIBILITY_NO
+ | IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
+ << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
+
+ /**
+ * Shift for the bits in {@link #mPrivateFlags2} related to the
+ * "accessibilityLiveRegion" attribute.
+ */
+ static final int PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT = 23;
+
+ /**
+ * Live region mode specifying that accessibility services should not
+ * automatically announce changes to this view. This is the default live
+ * region mode for most views.
+ * <p>
+ * Use with {@link #setAccessibilityLiveRegion(int)}.
+ */
+ public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0x00000000;
+
+ /**
+ * Live region mode specifying that accessibility services should announce
+ * changes to this view.
+ * <p>
+ * Use with {@link #setAccessibilityLiveRegion(int)}.
+ */
+ public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001;
+
+ /**
+ * Live region mode specifying that accessibility services should interrupt
+ * ongoing speech to immediately announce changes to this view.
+ * <p>
+ * Use with {@link #setAccessibilityLiveRegion(int)}.
+ */
+ public static final int ACCESSIBILITY_LIVE_REGION_ASSERTIVE = 0x00000002;
+
+ /**
+ * The default whether the view is important for accessibility.
+ */
+ static final int ACCESSIBILITY_LIVE_REGION_DEFAULT = ACCESSIBILITY_LIVE_REGION_NONE;
+
+ /**
+ * Mask for obtaining the bits which specify a view's accessibility live
+ * region mode.
+ */
+ static final int PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK = (ACCESSIBILITY_LIVE_REGION_NONE
+ | ACCESSIBILITY_LIVE_REGION_POLITE | ACCESSIBILITY_LIVE_REGION_ASSERTIVE)
+ << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT;
+
+ /**
+ * Flag indicating whether a view has accessibility focus.
+ */
+ static final int PFLAG2_ACCESSIBILITY_FOCUSED = 0x04000000;
+
+ /**
+ * Flag whether the accessibility state of the subtree rooted at this view changed.
+ */
+ static final int PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED = 0x08000000;
+
+ /**
+ * Flag indicating whether a view failed the quickReject() check in draw(). This condition
+ * is used to check whether later changes to the view's transform should invalidate the
+ * view to force the quickReject test to run again.
+ */
+ static final int PFLAG2_VIEW_QUICK_REJECTED = 0x10000000;
+
+ /**
+ * Flag indicating that start/end padding has been resolved into left/right padding
+ * for use in measurement, layout, drawing, etc. This is set by {@link #resolvePadding()}
+ * and checked by {@link #measure(int, int)} to determine if padding needs to be resolved
+ * during measurement. In some special cases this is required such as when an adapter-based
+ * view measures prospective children without attaching them to a window.
+ */
+ static final int PFLAG2_PADDING_RESOLVED = 0x20000000;
+
+ /**
+ * Flag indicating that the start/end drawables has been resolved into left/right ones.
+ */
+ static final int PFLAG2_DRAWABLE_RESOLVED = 0x40000000;
+
+ /**
+ * Indicates that the view is tracking some sort of transient state
+ * that the app should not need to be aware of, but that the framework
+ * should take special care to preserve.
+ */
+ static final int PFLAG2_HAS_TRANSIENT_STATE = 0x80000000;
+
+ /**
+ * Group of bits indicating that RTL properties resolution is done.
+ */
+ static final int ALL_RTL_PROPERTIES_RESOLVED = PFLAG2_LAYOUT_DIRECTION_RESOLVED |
+ PFLAG2_TEXT_DIRECTION_RESOLVED |
+ PFLAG2_TEXT_ALIGNMENT_RESOLVED |
+ PFLAG2_PADDING_RESOLVED |
+ PFLAG2_DRAWABLE_RESOLVED;
+
+ // There are a couple of flags left in mPrivateFlags2
+
+ /* End of masks for mPrivateFlags2 */
+
+ /*
+ * Masks for mPrivateFlags3, as generated by dumpFlags():
+ *
+ * |-------|-------|-------|-------|
+ * 1 PFLAG3_VIEW_IS_ANIMATING_TRANSFORM
+ * 1 PFLAG3_VIEW_IS_ANIMATING_ALPHA
+ * 1 PFLAG3_IS_LAID_OUT
+ * 1 PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
+ * 1 PFLAG3_CALLED_SUPER
+ * 1 PFLAG3_APPLYING_INSETS
+ * 1 PFLAG3_FITTING_SYSTEM_WINDOWS
+ * 1 PFLAG3_NESTED_SCROLLING_ENABLED
+ * 1 PFLAG3_SCROLL_INDICATOR_TOP
+ * 1 PFLAG3_SCROLL_INDICATOR_BOTTOM
+ * 1 PFLAG3_SCROLL_INDICATOR_LEFT
+ * 1 PFLAG3_SCROLL_INDICATOR_RIGHT
+ * 1 PFLAG3_SCROLL_INDICATOR_START
+ * 1 PFLAG3_SCROLL_INDICATOR_END
+ * 1 PFLAG3_ASSIST_BLOCKED
+ * 1 PFLAG3_CLUSTER
+ * 1 PFLAG3_IS_AUTOFILLED
+ * 1 PFLAG3_FINGER_DOWN
+ * 1 PFLAG3_FOCUSED_BY_DEFAULT
+ * 1111 PFLAG3_IMPORTANT_FOR_AUTOFILL
+ * 1 PFLAG3_OVERLAPPING_RENDERING_FORCED_VALUE
+ * 1 PFLAG3_HAS_OVERLAPPING_RENDERING_FORCED
+ * 1 PFLAG3_TEMPORARY_DETACH
+ * 1 PFLAG3_NO_REVEAL_ON_FOCUS
+ * 1 PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT
+ * 1 PFLAG3_SCREEN_READER_FOCUSABLE
+ * 1 PFLAG3_AGGREGATED_VISIBLE
+ * 1 PFLAG3_AUTOFILLID_EXPLICITLY_SET
+ * 1 PFLAG3_ACCESSIBILITY_HEADING
+ * |-------|-------|-------|-------|
+ */
+
+ /**
+ * Flag indicating that view has a transform animation set on it. This is used to track whether
+ * an animation is cleared between successive frames, in order to tell the associated
+ * DisplayList to clear its animation matrix.
+ */
+ static final int PFLAG3_VIEW_IS_ANIMATING_TRANSFORM = 0x1;
+
+ /**
+ * Flag indicating that view has an alpha animation set on it. This is used to track whether an
+ * animation is cleared between successive frames, in order to tell the associated
+ * DisplayList to restore its alpha value.
+ */
+ static final int PFLAG3_VIEW_IS_ANIMATING_ALPHA = 0x2;
+
+ /**
+ * Flag indicating that the view has been through at least one layout since it
+ * was last attached to a window.
+ */
+ static final int PFLAG3_IS_LAID_OUT = 0x4;
+
+ /**
+ * Flag indicating that a call to measure() was skipped and should be done
+ * instead when layout() is invoked.
+ */
+ static final int PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT = 0x8;
+
+ /**
+ * Flag indicating that an overridden method correctly called down to
+ * the superclass implementation as required by the API spec.
+ */
+ static final int PFLAG3_CALLED_SUPER = 0x10;
+
+ /**
+ * Flag indicating that we're in the process of applying window insets.
+ */
+ static final int PFLAG3_APPLYING_INSETS = 0x20;
+
+ /**
+ * Flag indicating that we're in the process of fitting system windows using the old method.
+ */
+ static final int PFLAG3_FITTING_SYSTEM_WINDOWS = 0x40;
+
+ /**
+ * Flag indicating that nested scrolling is enabled for this view.
+ * The view will optionally cooperate with views up its parent chain to allow for
+ * integrated nested scrolling along the same axis.
+ */
+ static final int PFLAG3_NESTED_SCROLLING_ENABLED = 0x80;
+
+ /**
+ * Flag indicating that the bottom scroll indicator should be displayed
+ * when this view can scroll up.
+ */
+ static final int PFLAG3_SCROLL_INDICATOR_TOP = 0x0100;
+
+ /**
+ * Flag indicating that the bottom scroll indicator should be displayed
+ * when this view can scroll down.
+ */
+ static final int PFLAG3_SCROLL_INDICATOR_BOTTOM = 0x0200;
+
+ /**
+ * Flag indicating that the left scroll indicator should be displayed
+ * when this view can scroll left.
+ */
+ static final int PFLAG3_SCROLL_INDICATOR_LEFT = 0x0400;
+
+ /**
+ * Flag indicating that the right scroll indicator should be displayed
+ * when this view can scroll right.
+ */
+ static final int PFLAG3_SCROLL_INDICATOR_RIGHT = 0x0800;
+
+ /**
+ * Flag indicating that the start scroll indicator should be displayed
+ * when this view can scroll in the start direction.
+ */
+ static final int PFLAG3_SCROLL_INDICATOR_START = 0x1000;
+
+ /**
+ * Flag indicating that the end scroll indicator should be displayed
+ * when this view can scroll in the end direction.
+ */
+ static final int PFLAG3_SCROLL_INDICATOR_END = 0x2000;
+
+ static final int DRAG_MASK = PFLAG2_DRAG_CAN_ACCEPT | PFLAG2_DRAG_HOVERED;
+
+ static final int SCROLL_INDICATORS_NONE = 0x0000;
+
+ /**
+ * Mask for use with setFlags indicating bits used for indicating which
+ * scroll indicators are enabled.
+ */
+ static final int SCROLL_INDICATORS_PFLAG3_MASK = PFLAG3_SCROLL_INDICATOR_TOP
+ | PFLAG3_SCROLL_INDICATOR_BOTTOM | PFLAG3_SCROLL_INDICATOR_LEFT
+ | PFLAG3_SCROLL_INDICATOR_RIGHT | PFLAG3_SCROLL_INDICATOR_START
+ | PFLAG3_SCROLL_INDICATOR_END;
+
+ /**
+ * Left-shift required to translate between public scroll indicator flags
+ * and internal PFLAGS3 flags. When used as a right-shift, translates
+ * PFLAGS3 flags to public flags.
+ */
+ static final int SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT = 8;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "SCROLL_INDICATOR_" }, value = {
+ SCROLL_INDICATOR_TOP,
+ SCROLL_INDICATOR_BOTTOM,
+ SCROLL_INDICATOR_LEFT,
+ SCROLL_INDICATOR_RIGHT,
+ SCROLL_INDICATOR_START,
+ SCROLL_INDICATOR_END,
+ })
+ public @interface ScrollIndicators {}
+
+ /**
+ * Scroll indicator direction for the top edge of the view.
+ *
+ * @see #setScrollIndicators(int)
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ */
+ public static final int SCROLL_INDICATOR_TOP =
+ PFLAG3_SCROLL_INDICATOR_TOP >> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+
+ /**
+ * Scroll indicator direction for the bottom edge of the view.
+ *
+ * @see #setScrollIndicators(int)
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ */
+ public static final int SCROLL_INDICATOR_BOTTOM =
+ PFLAG3_SCROLL_INDICATOR_BOTTOM >> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+
+ /**
+ * Scroll indicator direction for the left edge of the view.
+ *
+ * @see #setScrollIndicators(int)
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ */
+ public static final int SCROLL_INDICATOR_LEFT =
+ PFLAG3_SCROLL_INDICATOR_LEFT >> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+
+ /**
+ * Scroll indicator direction for the right edge of the view.
+ *
+ * @see #setScrollIndicators(int)
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ */
+ public static final int SCROLL_INDICATOR_RIGHT =
+ PFLAG3_SCROLL_INDICATOR_RIGHT >> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+
+ /**
+ * Scroll indicator direction for the starting edge of the view.
+ * <p>
+ * Resolved according to the view's layout direction, see
+ * {@link #getLayoutDirection()} for more information.
+ *
+ * @see #setScrollIndicators(int)
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ */
+ public static final int SCROLL_INDICATOR_START =
+ PFLAG3_SCROLL_INDICATOR_START >> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+
+ /**
+ * Scroll indicator direction for the ending edge of the view.
+ * <p>
+ * Resolved according to the view's layout direction, see
+ * {@link #getLayoutDirection()} for more information.
+ *
+ * @see #setScrollIndicators(int)
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ */
+ public static final int SCROLL_INDICATOR_END =
+ PFLAG3_SCROLL_INDICATOR_END >> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+
+ /**
+ * <p>Indicates that we are allowing {@link ViewStructure} to traverse
+ * into this view.<p>
+ */
+ static final int PFLAG3_ASSIST_BLOCKED = 0x4000;
+
+ /**
+ * Flag indicating that the view is a root of a keyboard navigation cluster.
+ *
+ * @see #isKeyboardNavigationCluster()
+ * @see #setKeyboardNavigationCluster(boolean)
+ */
+ private static final int PFLAG3_CLUSTER = 0x8000;
+
+ /**
+ * Flag indicating that the view is autofilled
+ *
+ * @see #isAutofilled()
+ * @see #setAutofilled(boolean, boolean)
+ */
+ private static final int PFLAG3_IS_AUTOFILLED = 0x10000;
+
+ /**
+ * Indicates that the user is currently touching the screen.
+ * Currently used for the tooltip positioning only.
+ */
+ private static final int PFLAG3_FINGER_DOWN = 0x20000;
+
+ /**
+ * Flag indicating that this view is the default-focus view.
+ *
+ * @see #isFocusedByDefault()
+ * @see #setFocusedByDefault(boolean)
+ */
+ private static final int PFLAG3_FOCUSED_BY_DEFAULT = 0x40000;
+
+ /**
+ * Shift for the bits in {@link #mPrivateFlags3} related to the
+ * "importantForAutofill" attribute.
+ */
+ static final int PFLAG3_IMPORTANT_FOR_AUTOFILL_SHIFT = 19;
+
+ /**
+ * Mask for obtaining the bits which specify how to determine
+ * whether a view is important for autofill.
+ */
+ static final int PFLAG3_IMPORTANT_FOR_AUTOFILL_MASK = (IMPORTANT_FOR_AUTOFILL_AUTO
+ | IMPORTANT_FOR_AUTOFILL_YES | IMPORTANT_FOR_AUTOFILL_NO
+ | IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS
+ | IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS)
+ << PFLAG3_IMPORTANT_FOR_AUTOFILL_SHIFT;
+
+ /**
+ * Whether this view has rendered elements that overlap (see {@link
+ * #hasOverlappingRendering()}, {@link #forceHasOverlappingRendering(boolean)}, and
+ * {@link #getHasOverlappingRendering()} ). The value in this bit is only valid when
+ * PFLAG3_HAS_OVERLAPPING_RENDERING_FORCED has been set. Otherwise, the value is
+ * determined by whatever {@link #hasOverlappingRendering()} returns.
+ */
+ private static final int PFLAG3_OVERLAPPING_RENDERING_FORCED_VALUE = 0x800000;
+
+ /**
+ * Whether {@link #forceHasOverlappingRendering(boolean)} has been called. When true, value
+ * in PFLAG3_OVERLAPPING_RENDERING_FORCED_VALUE is valid.
+ */
+ private static final int PFLAG3_HAS_OVERLAPPING_RENDERING_FORCED = 0x1000000;
+
+ /**
+ * Flag indicating that the view is temporarily detached from the parent view.
+ *
+ * @see #onStartTemporaryDetach()
+ * @see #onFinishTemporaryDetach()
+ */
+ static final int PFLAG3_TEMPORARY_DETACH = 0x2000000;
+
+ /**
+ * Flag indicating that the view does not wish to be revealed within its parent
+ * hierarchy when it gains focus. Expressed in the negative since the historical
+ * default behavior is to reveal on focus; this flag suppresses that behavior.
+ *
+ * @see #setRevealOnFocusHint(boolean)
+ * @see #getRevealOnFocusHint()
+ */
+ private static final int PFLAG3_NO_REVEAL_ON_FOCUS = 0x4000000;
+
+ /**
+ * Flag indicating that when layout is completed we should notify
+ * that the view was entered for autofill purposes. To minimize
+ * showing autofill for views not visible to the user we evaluate
+ * user visibility which cannot be done until the view is laid out.
+ */
+ static final int PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT = 0x8000000;
+
+ /**
+ * Works like focusable for screen readers, but without the side effects on input focus.
+ * @see #setScreenReaderFocusable(boolean)
+ */
+ private static final int PFLAG3_SCREEN_READER_FOCUSABLE = 0x10000000;
+
+ /**
+ * The last aggregated visibility. Used to detect when it truly changes.
+ */
+ private static final int PFLAG3_AGGREGATED_VISIBLE = 0x20000000;
+
+ /**
+ * Used to indicate that {@link #mAutofillId} was explicitly set through
+ * {@link #setAutofillId(AutofillId)}.
+ */
+ private static final int PFLAG3_AUTOFILLID_EXPLICITLY_SET = 0x40000000;
+
+ /**
+ * Indicates if the View is a heading for accessibility purposes
+ */
+ private static final int PFLAG3_ACCESSIBILITY_HEADING = 0x80000000;
+
+ /* End of masks for mPrivateFlags3 */
+
+ /*
+ * Masks for mPrivateFlags4, as generated by dumpFlags():
+ *
+ * |-------|-------|-------|-------|
+ * 1111 PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK
+ * 1 PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED
+ * 1 PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED
+ * 1 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED
+ * 1 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE
+ * 11 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK
+ * 1 PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS
+ * 1 PFLAG4_AUTOFILL_HIDE_HIGHLIGHT
+ * 11 PFLAG4_SCROLL_CAPTURE_HINT_MASK
+ * 1 PFLAG4_ALLOW_CLICK_WHEN_DISABLED
+ * 1 PFLAG4_DETACHED
+ * 1 PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE
+ * |-------|-------|-------|-------|
+ */
+
+ /**
+ * Mask for obtaining the bits which specify how to determine
+ * whether a view is important for autofill.
+ *
+ * <p>NOTE: the important for content capture values were the first flags added and are set in
+ * the rightmost position, so we don't need to shift them
+ */
+ private static final int PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK =
+ IMPORTANT_FOR_CONTENT_CAPTURE_AUTO | IMPORTANT_FOR_CONTENT_CAPTURE_YES
+ | IMPORTANT_FOR_CONTENT_CAPTURE_NO
+ | IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS
+ | IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS;
+
+ /*
+ * Variables used to control when the IntelligenceManager.notifyNodeAdded()/removed() methods
+ * should be called.
+ *
+ * The idea is to call notifyAppeared() after the view is layout and visible, then call
+ * notifyDisappeared() when it's gone (without known when it was removed from the parent).
+ */
+ private static final int PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED = 0x10;
+ private static final int PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED = 0x20;
+
+ /*
+ * Flags used to cache the value returned by isImportantForContentCapture while the view
+ * hierarchy is being traversed.
+ */
+ private static final int PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED = 0x40;
+ private static final int PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE = 0x80;
+
+ private static final int PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK =
+ PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED
+ | PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE;
+
+ /**
+ * @see #OPTIONAL_FITS_SYSTEM_WINDOWS
+ */
+ static final int PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS = 0x000000100;
+
+ /**
+ * Flag indicating the field should not have yellow highlight when autofilled.
+ */
+ private static final int PFLAG4_AUTOFILL_HIDE_HIGHLIGHT = 0x200;
+
+ /**
+ * Shift for the bits in {@link #mPrivateFlags4} related to scroll capture.
+ */
+ static final int PFLAG4_SCROLL_CAPTURE_HINT_SHIFT = 10;
+
+ static final int PFLAG4_SCROLL_CAPTURE_HINT_MASK = (SCROLL_CAPTURE_HINT_INCLUDE
+ | SCROLL_CAPTURE_HINT_EXCLUDE | SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS)
+ << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
+
+ /**
+ * Indicates if the view can receive click events when disabled.
+ */
+ private static final int PFLAG4_ALLOW_CLICK_WHEN_DISABLED = 0x000001000;
+
+ /**
+ * Indicates if the view is just detached.
+ */
+ private static final int PFLAG4_DETACHED = 0x000002000;
+
+ /**
+ * Indicates that the view has transient state because the system is translating it.
+ */
+ private static final int PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE = 0x000004000;
+
+ /* End of masks for mPrivateFlags4 */
+
+ /** @hide */
+ protected static final int VIEW_STRUCTURE_FOR_ASSIST = 0;
+ /** @hide */
+ protected static final int VIEW_STRUCTURE_FOR_AUTOFILL = 1;
+ /** @hide */
+ protected static final int VIEW_STRUCTURE_FOR_CONTENT_CAPTURE = 2;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "VIEW_STRUCTURE_FOR" }, value = {
+ VIEW_STRUCTURE_FOR_ASSIST,
+ VIEW_STRUCTURE_FOR_AUTOFILL,
+ VIEW_STRUCTURE_FOR_CONTENT_CAPTURE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ViewStructureType {}
+
+ /**
+ * Always allow a user to over-scroll this view, provided it is a
+ * view that can scroll.
+ *
+ * @see #getOverScrollMode()
+ * @see #setOverScrollMode(int)
+ */
+ public static final int OVER_SCROLL_ALWAYS = 0;
+
+ /**
+ * Allow a user to over-scroll this view only if the content is large
+ * enough to meaningfully scroll, provided it is a view that can scroll.
+ *
+ * @see #getOverScrollMode()
+ * @see #setOverScrollMode(int)
+ */
+ public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1;
+
+ /**
+ * Never allow a user to over-scroll this view.
+ *
+ * @see #getOverScrollMode()
+ * @see #setOverScrollMode(int)
+ */
+ public static final int OVER_SCROLL_NEVER = 2;
+
+ /**
+ * Special constant for {@link #setSystemUiVisibility(int)}: View has
+ * requested the system UI (status bar) to be visible (the default).
+ *
+ * @see #setSystemUiVisibility(int)
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_VISIBLE = 0;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View has requested the
+ * system UI to enter an unobtrusive "low profile" mode.
+ *
+ * <p>This is for use in games, book readers, video players, or any other
+ * "immersive" application where the usual system chrome is deemed too distracting.
+ *
+ * <p>In low profile mode, the status bar and/or navigation icons may dim.
+ *
+ * @see #setSystemUiVisibility(int)
+ * @deprecated Low profile mode is deprecated. Hide the system bars instead if the application
+ * needs to be in a unobtrusive mode. Use {@link WindowInsetsController#hide(int)} with
+ * {@link Type#systemBars()}.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_LOW_PROFILE = 0x00000001;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View has requested that the
+ * system navigation be temporarily hidden.
+ *
+ * <p>This is an even less obtrusive state than that called for by
+ * {@link #SYSTEM_UI_FLAG_LOW_PROFILE}; on devices that draw essential navigation controls
+ * (Home, Back, and the like) on screen, <code>SYSTEM_UI_FLAG_HIDE_NAVIGATION</code> will cause
+ * those to disappear. This is useful (in conjunction with the
+ * {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN FLAG_FULLSCREEN} and
+ * {@link android.view.WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN FLAG_LAYOUT_IN_SCREEN}
+ * window flags) for displaying content using every last pixel on the display.
+ *
+ * <p>There is a limitation: because navigation controls are so important, the least user
+ * interaction will cause them to reappear immediately. When this happens, both
+ * this flag and {@link #SYSTEM_UI_FLAG_FULLSCREEN} will be cleared automatically,
+ * so that both elements reappear at the same time.
+ *
+ * @see #setSystemUiVisibility(int)
+ * @deprecated Use {@link WindowInsetsController#hide(int)} with {@link Type#navigationBars()}
+ * instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_HIDE_NAVIGATION = 0x00000002;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View has requested to go
+ * into the normal fullscreen mode so that its content can take over the screen
+ * while still allowing the user to interact with the application.
+ *
+ * <p>This has the same visual effect as
+ * {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN
+ * WindowManager.LayoutParams.FLAG_FULLSCREEN},
+ * meaning that non-critical screen decorations (such as the status bar) will be
+ * hidden while the user is in the View's window, focusing the experience on
+ * that content. Unlike the window flag, if you are using ActionBar in
+ * overlay mode with {@link Window#FEATURE_ACTION_BAR_OVERLAY
+ * Window.FEATURE_ACTION_BAR_OVERLAY}, then enabling this flag will also
+ * hide the action bar.
+ *
+ * <p>This approach to going fullscreen is best used over the window flag when
+ * it is a transient state -- that is, the application does this at certain
+ * points in its user interaction where it wants to allow the user to focus
+ * on content, but not as a continuous state. For situations where the application
+ * would like to simply stay full screen the entire time (such as a game that
+ * wants to take over the screen), the
+ * {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN window flag}
+ * is usually a better approach. The state set here will be removed by the system
+ * in various situations (such as the user moving to another application) like
+ * the other system UI states.
+ *
+ * <p>When using this flag, the application should provide some easy facility
+ * for the user to go out of it. A common example would be in an e-book
+ * reader, where tapping on the screen brings back whatever screen and UI
+ * decorations that had been hidden while the user was immersed in reading
+ * the book.
+ *
+ * @see #setSystemUiVisibility(int)
+ * @deprecated Use {@link WindowInsetsController#hide(int)} with {@link Type#statusBars()}
+ * instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: When using other layout
+ * flags, we would like a stable view of the content insets given to
+ * {@link #fitSystemWindows(Rect)}. This means that the insets seen there
+ * will always represent the worst case that the application can expect
+ * as a continuous state. In the stock Android UI this is the space for
+ * the system bar, nav bar, and status bar, but not more transient elements
+ * such as an input method.
+ *
+ * The stable layout your UI sees is based on the system UI modes you can
+ * switch to. That is, if you specify {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}
+ * then you will get a stable layout for changes of the
+ * {@link #SYSTEM_UI_FLAG_FULLSCREEN} mode; if you specify
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} and
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}, then you can transition
+ * to {@link #SYSTEM_UI_FLAG_FULLSCREEN} and {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}
+ * with a stable layout. (Note that you should avoid using
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION} by itself.)
+ *
+ * If you have set the window flag {@link WindowManager.LayoutParams#FLAG_FULLSCREEN}
+ * to hide the status bar (instead of using {@link #SYSTEM_UI_FLAG_FULLSCREEN}),
+ * then a hidden status bar will be considered a "stable" state for purposes
+ * here. This allows your UI to continually hide the status bar, while still
+ * using the system UI flags to hide the action bar while still retaining
+ * a stable layout. Note that changing the window fullscreen flag will never
+ * provide a stable layout for a clean transition.
+ *
+ * <p>If you are using ActionBar in
+ * overlay mode with {@link Window#FEATURE_ACTION_BAR_OVERLAY
+ * Window.FEATURE_ACTION_BAR_OVERLAY}, this flag will also impact the
+ * insets it adds to those given to the application.
+ *
+ * @deprecated Use {@link WindowInsets#getInsetsIgnoringVisibility(int)} instead to retrieve
+ * insets that don't change when system bars change visibility state.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View would like its window
+ * to be laid out as if it has requested
+ * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, even if it currently hasn't. This
+ * allows it to avoid artifacts when switching in and out of that mode, at
+ * the expense that some of its user interface may be covered by screen
+ * decorations when they are shown. You can perform layout of your inner
+ * UI elements to account for the navigation system UI through the
+ * {@link #fitSystemWindows(Rect)} method.
+ *
+ * @deprecated For floating windows, use {@link LayoutParams#setFitInsetsTypes(int)} with
+ * {@link Type#navigationBars()}. For non-floating windows that fill the screen, call
+ * {@link Window#setDecorFitsSystemWindows(boolean)} with {@code false}.
+ */
+ public static final int SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View would like its window
+ * to be laid out as if it has requested
+ * {@link #SYSTEM_UI_FLAG_FULLSCREEN}, even if it currently hasn't. This
+ * allows it to avoid artifacts when switching in and out of that mode, at
+ * the expense that some of its user interface may be covered by screen
+ * decorations when they are shown. You can perform layout of your inner
+ * UI elements to account for non-fullscreen system UI through the
+ * {@link #fitSystemWindows(Rect)} method.
+ *
+ * <p>Note: on displays that have a {@link DisplayCutout}, the window may still be placed
+ * differently than if {@link #SYSTEM_UI_FLAG_FULLSCREEN} was set, if the
+ * window's {@link WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ * layoutInDisplayCutoutMode} is
+ * {@link WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT}. To avoid this, use either of the other modes.
+ *
+ * @see WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+ *
+ * @deprecated For floating windows, use {@link LayoutParams#setFitInsetsTypes(int)} with
+ * {@link Type#statusBars()} ()}. For non-floating windows that fill the screen, call
+ * {@link Window#setDecorFitsSystemWindows(boolean)} with {@code false}.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View would like to remain interactive when
+ * hiding the navigation bar with {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}. If this flag is
+ * not set, {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION} will be force cleared by the system on any
+ * user interaction.
+ * <p>Since this flag is a modifier for {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, it only
+ * has an effect when used in combination with that flag.</p>
+ *
+ * @deprecated Use {@link WindowInsetsController#BEHAVIOR_DEFAULT} instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_IMMERSIVE = 0x00000800;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: View would like to remain interactive when
+ * hiding the status bar with {@link #SYSTEM_UI_FLAG_FULLSCREEN} and/or hiding the navigation
+ * bar with {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}. Use this flag to create an immersive
+ * experience while also hiding the system bars. If this flag is not set,
+ * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION} will be force cleared by the system on any user
+ * interaction, and {@link #SYSTEM_UI_FLAG_FULLSCREEN} will be force-cleared by the system
+ * if the user swipes from the top of the screen.
+ * <p>When system bars are hidden in immersive mode, they can be revealed temporarily with
+ * system gestures, such as swiping from the top of the screen. These transient system bars
+ * will overlay app's content, may have some degree of transparency, and will automatically
+ * hide after a short timeout.
+ * </p><p>Since this flag is a modifier for {@link #SYSTEM_UI_FLAG_FULLSCREEN} and
+ * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, it only has an effect when used in combination
+ * with one or both of those flags.</p>
+ *
+ * @deprecated Use {@link WindowInsetsController#BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE} instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: Requests the status bar to draw in a mode that
+ * is compatible with light status bar backgrounds.
+ *
+ * <p>For this to take effect, the window must request
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
+ * FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS
+ * FLAG_TRANSLUCENT_STATUS}.
+ *
+ * @see android.R.attr#windowLightStatusBar
+ * @deprecated Use {@link WindowInsetsController#APPEARANCE_LIGHT_STATUS_BARS} instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000;
+
+ /**
+ * This flag was previously used for a private API. DO NOT reuse it for a public API as it might
+ * trigger undefined behavior on older platforms with apps compiled against a new SDK.
+ */
+ private static final int SYSTEM_UI_RESERVED_LEGACY1 = 0x00004000;
+
+ /**
+ * This flag was previously used for a private API. DO NOT reuse it for a public API as it might
+ * trigger undefined behavior on older platforms with apps compiled against a new SDK.
+ */
+ private static final int SYSTEM_UI_RESERVED_LEGACY2 = 0x00010000;
+
+ /**
+ * Flag for {@link #setSystemUiVisibility(int)}: Requests the navigation bar to draw in a mode
+ * that is compatible with light navigation bar backgrounds.
+ *
+ * <p>For this to take effect, the window must request
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
+ * FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION
+ * FLAG_TRANSLUCENT_NAVIGATION}.
+ *
+ * @see android.R.attr#windowLightNavigationBar
+ * @deprecated Use {@link WindowInsetsController#APPEARANCE_LIGHT_NAVIGATION_BARS} instead.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR = 0x00000010;
+
+ /**
+ * @deprecated Use {@link #SYSTEM_UI_FLAG_LOW_PROFILE} instead.
+ */
+ @Deprecated
+ public static final int STATUS_BAR_HIDDEN = SYSTEM_UI_FLAG_LOW_PROFILE;
+
+ /**
+ * @deprecated Use {@link #SYSTEM_UI_FLAG_VISIBLE} instead.
+ */
+ @Deprecated
+ public static final int STATUS_BAR_VISIBLE = SYSTEM_UI_FLAG_VISIBLE;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to make the status bar not expandable. Unless you also
+ * set {@link #STATUS_BAR_DISABLE_NOTIFICATION_ICONS}, new notifications will continue to show.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int STATUS_BAR_DISABLE_EXPAND = 0x00010000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide notification icons and scrolling ticker text.
+ */
+ public static final int STATUS_BAR_DISABLE_NOTIFICATION_ICONS = 0x00020000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to disable incoming notification alerts. This will not block
+ * icons, but it will block sound, vibrating and other visual or aural notifications.
+ */
+ public static final int STATUS_BAR_DISABLE_NOTIFICATION_ALERTS = 0x00040000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide only the scrolling ticker. Note that
+ * {@link #STATUS_BAR_DISABLE_NOTIFICATION_ICONS} implies
+ * {@link #STATUS_BAR_DISABLE_NOTIFICATION_TICKER}.
+ */
+ public static final int STATUS_BAR_DISABLE_NOTIFICATION_TICKER = 0x00080000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide the center system info area.
+ */
+ public static final int STATUS_BAR_DISABLE_SYSTEM_INFO = 0x00100000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide only the home button. Don't use this
+ * unless you're a special part of the system UI (i.e., setup wizard, keyguard).
+ */
+ @UnsupportedAppUsage
+ public static final int STATUS_BAR_DISABLE_HOME = 0x00200000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide only the back button. Don't use this
+ * unless you're a special part of the system UI (i.e., setup wizard, keyguard).
+ */
+ @UnsupportedAppUsage
+ public static final int STATUS_BAR_DISABLE_BACK = 0x00400000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide only the clock. You might use this if your activity has
+ * its own clock making the status bar's clock redundant.
+ */
+ public static final int STATUS_BAR_DISABLE_CLOCK = 0x00800000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to hide only the recent apps button. Don't use this
+ * unless you're a special part of the system UI (i.e., setup wizard, keyguard).
+ */
+ @UnsupportedAppUsage
+ public static final int STATUS_BAR_DISABLE_RECENT = 0x01000000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to disable the global search gesture. Don't use this
+ * unless you're a special part of the system UI (i.e., setup wizard, keyguard).
+ */
+ public static final int STATUS_BAR_DISABLE_SEARCH = 0x02000000;
+
+ /**
+ * @hide
+ *
+ * NOTE: This flag may only be used in subtreeSystemUiVisibility. It is masked
+ * out of the public fields to keep the undefined bits out of the developer's way.
+ *
+ * Flag to disable the ongoing call chip.
+ */
+ public static final int STATUS_BAR_DISABLE_ONGOING_CALL_CHIP = 0x04000000;
+
+ /**
+ * @hide
+ */
+ public static final int PUBLIC_STATUS_BAR_VISIBILITY_MASK = 0x00003FF7;
+
+ /**
+ * These are the system UI flags that can be cleared by events outside
+ * of an application. Currently this is just the ability to tap on the
+ * screen while hiding the navigation bar to have it return.
+ * @hide
+ */
+ public static final int SYSTEM_UI_CLEARABLE_FLAGS =
+ SYSTEM_UI_FLAG_LOW_PROFILE | SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | SYSTEM_UI_FLAG_FULLSCREEN;
+
+ /**
+ * Flags that can impact the layout in relation to system UI.
+ *
+ * @deprecated System UI layout flags are deprecated.
+ */
+ @Deprecated
+ public static final int SYSTEM_UI_LAYOUT_FLAGS =
+ SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FIND_VIEWS_" }, value = {
+ FIND_VIEWS_WITH_TEXT,
+ FIND_VIEWS_WITH_CONTENT_DESCRIPTION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FindViewFlags {}
+
+ /**
+ * Find views that render the specified text.
+ *
+ * @see #findViewsWithText(ArrayList, CharSequence, int)
+ */
+ public static final int FIND_VIEWS_WITH_TEXT = 0x00000001;
+
+ /**
+ * Find find views that contain the specified content description.
+ *
+ * @see #findViewsWithText(ArrayList, CharSequence, int)
+ */
+ public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 0x00000002;
+
+ /**
+ * Find views that contain {@link AccessibilityNodeProvider}. Such
+ * a View is a root of virtual view hierarchy and may contain the searched
+ * text. If this flag is set Views with providers are automatically
+ * added and it is a responsibility of the client to call the APIs of
+ * the provider to determine whether the virtual tree rooted at this View
+ * contains the text, i.e. getting the list of {@link AccessibilityNodeInfo}s
+ * representing the virtual views with this text.
+ *
+ * @see #findViewsWithText(ArrayList, CharSequence, int)
+ *
+ * @hide
+ */
+ public static final int FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS = 0x00000004;
+
+ /**
+ * The undefined cursor position.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_CURSOR_POSITION_UNDEFINED = -1;
+
+ /**
+ * Indicates that the screen has changed state and is now off.
+ *
+ * @see #onScreenStateChanged(int)
+ */
+ public static final int SCREEN_STATE_OFF = 0x0;
+
+ /**
+ * Indicates that the screen has changed state and is now on.
+ *
+ * @see #onScreenStateChanged(int)
+ */
+ public static final int SCREEN_STATE_ON = 0x1;
+
+ /**
+ * Indicates no axis of view scrolling.
+ */
+ public static final int SCROLL_AXIS_NONE = 0;
+
+ /**
+ * Indicates scrolling along the horizontal axis.
+ */
+ public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0;
+
+ /**
+ * Indicates scrolling along the vertical axis.
+ */
+ public static final int SCROLL_AXIS_VERTICAL = 1 << 1;
+
+ /**
+ * Controls the over-scroll mode for this view.
+ * See {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)},
+ * {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS},
+ * and {@link #OVER_SCROLL_NEVER}.
+ */
+ private int mOverScrollMode;
+
+ /**
+ * The parent this view is attached to.
+ * {@hide}
+ *
+ * @see #getParent()
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected ViewParent mParent;
+
+ /**
+ * {@hide}
+ *
+ * Not available for general use. If you need help, hang up and then dial one of the following
+ * public APIs:
+ *
+ * @see #isAttachedToWindow() for current attach state
+ * @see #onAttachedToWindow() for subclasses performing work when becoming attached
+ * @see #onDetachedFromWindow() for subclasses performing work when becoming detached
+ * @see OnAttachStateChangeListener for other code performing work on attach/detach
+ * @see #getHandler() for posting messages to this view's UI thread/looper
+ * @see #getParent() for interacting with the parent chain
+ * @see #getWindowToken() for the current window token
+ * @see #getRootView() for the view at the root of the attached hierarchy
+ * @see #getDisplay() for the Display this view is presented on
+ * @see #getRootWindowInsets() for the current insets applied to the whole attached window
+ * @see #hasWindowFocus() for whether the attached window is currently focused
+ * @see #getWindowVisibility() for checking the visibility of the attached window
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ AttachInfo mAttachInfo;
+
+ /**
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(mask = PFLAG_FORCE_LAYOUT, equals = PFLAG_FORCE_LAYOUT,
+ name = "FORCE_LAYOUT"),
+ @ViewDebug.FlagToString(mask = PFLAG_LAYOUT_REQUIRED, equals = PFLAG_LAYOUT_REQUIRED,
+ name = "LAYOUT_REQUIRED"),
+ @ViewDebug.FlagToString(mask = PFLAG_DRAWING_CACHE_VALID, equals = PFLAG_DRAWING_CACHE_VALID,
+ name = "DRAWING_CACHE_INVALID", outputIf = false),
+ @ViewDebug.FlagToString(mask = PFLAG_DRAWN, equals = PFLAG_DRAWN, name = "DRAWN", outputIf = true),
+ @ViewDebug.FlagToString(mask = PFLAG_DRAWN, equals = PFLAG_DRAWN, name = "NOT_DRAWN", outputIf = false),
+ @ViewDebug.FlagToString(mask = PFLAG_DIRTY_MASK, equals = PFLAG_DIRTY, name = "DIRTY")
+ }, formatToHexString = true)
+
+ /* @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769414)
+ public int mPrivateFlags;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768943)
+ int mPrivateFlags2;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 129147060)
+ int mPrivateFlags3;
+
+ private int mPrivateFlags4;
+
+ /**
+ * This view's request for the visibility of the status bar.
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @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_DISABLE_ONGOING_CALL_CHIP,
+ equals = STATUS_BAR_DISABLE_ONGOING_CALL_CHIP,
+ name = "STATUS_BAR_DISABLE_ONGOING_CALL_CHIP")
+ }, formatToHexString = true)
+ @SystemUiVisibility
+ int mSystemUiVisibility;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix = "", value = {
+ SYSTEM_UI_FLAG_LOW_PROFILE,
+ SYSTEM_UI_FLAG_HIDE_NAVIGATION,
+ SYSTEM_UI_FLAG_FULLSCREEN,
+ SYSTEM_UI_FLAG_LAYOUT_STABLE,
+ SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,
+ SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,
+ SYSTEM_UI_FLAG_IMMERSIVE,
+ SYSTEM_UI_FLAG_IMMERSIVE_STICKY,
+ SYSTEM_UI_FLAG_LIGHT_STATUS_BAR,
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ STATUS_BAR_DISABLE_EXPAND,
+ STATUS_BAR_DISABLE_NOTIFICATION_ICONS,
+ STATUS_BAR_DISABLE_NOTIFICATION_ALERTS,
+ STATUS_BAR_DISABLE_NOTIFICATION_TICKER,
+ STATUS_BAR_DISABLE_SYSTEM_INFO,
+ STATUS_BAR_DISABLE_HOME,
+ STATUS_BAR_DISABLE_BACK,
+ STATUS_BAR_DISABLE_CLOCK,
+ STATUS_BAR_DISABLE_RECENT,
+ STATUS_BAR_DISABLE_SEARCH,
+ STATUS_BAR_DISABLE_ONGOING_CALL_CHIP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SystemUiVisibility {}
+
+ /**
+ * Reference count for transient state.
+ * @see #setHasTransientState(boolean)
+ */
+ int mTransientStateCount = 0;
+
+ /**
+ * Count of how many windows this view has been attached to.
+ */
+ int mWindowAttachCount;
+
+ /**
+ * The layout parameters associated with this view and used by the parent
+ * {@link android.view.ViewGroup} to determine how this view should be
+ * laid out.
+ *
+ * The field should not be used directly. Instead {@link #getLayoutParams()} and {@link
+ * #setLayoutParams(ViewGroup.LayoutParams)} should be used. The setter guarantees internal
+ * state correctness of the class.
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected ViewGroup.LayoutParams mLayoutParams;
+
+ /**
+ * The view flags hold various views states.
+ *
+ * Use {@link #setTransitionVisibility(int)} to change the visibility of this view without
+ * triggering updates.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(formatToHexString = true)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ int mViewFlags;
+
+ static class TransformationInfo {
+ /**
+ * The transform matrix for the View. This transform is calculated internally
+ * based on the translation, rotation, and scale properties.
+ *
+ * Do *not* use this variable directly; instead call getMatrix(), which will
+ * load the value from the View's RenderNode.
+ */
+ private final Matrix mMatrix = new Matrix();
+
+ /**
+ * The inverse transform matrix for the View. This transform is calculated
+ * internally based on the translation, rotation, and scale properties.
+ *
+ * Do *not* use this variable directly; instead call getInverseMatrix(),
+ * which will load the value from the View's RenderNode.
+ */
+ private Matrix mInverseMatrix;
+
+ /**
+ * The opacity of the View. This is a value from 0 to 1, where 0 means
+ * completely transparent and 1 means completely opaque.
+ */
+ @ViewDebug.ExportedProperty
+ private float mAlpha = 1f;
+
+ /**
+ * The opacity of the view as manipulated by the Fade transition. This is a
+ * property only used by transitions, which is composited with the other alpha
+ * values to calculate the final visual alpha value.
+ */
+ float mTransitionAlpha = 1f;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public TransformationInfo mTransformationInfo;
+
+ /**
+ * Current clip bounds. to which all drawing of this view are constrained.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ Rect mClipBounds = null;
+
+ private boolean mLastIsOpaque;
+
+ /**
+ * The distance in pixels from the left edge of this view's parent
+ * to the left edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected int mLeft;
+ /**
+ * The distance in pixels from the left edge of this view's parent
+ * to the right edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected int mRight;
+ /**
+ * The distance in pixels from the top edge of this view's parent
+ * to the top edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected int mTop;
+ /**
+ * The distance in pixels from the top edge of this view's parent
+ * to the bottom edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected int mBottom;
+
+ /**
+ * The offset, in pixels, by which the content of this view is scrolled
+ * horizontally.
+ * Please use {@link View#getScrollX()} and {@link View#setScrollX(int)} instead of
+ * accessing these directly.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "scrolling")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected int mScrollX;
+ /**
+ * The offset, in pixels, by which the content of this view is scrolled
+ * vertically.
+ * Please use {@link View#getScrollY()} and {@link View#setScrollY(int)} instead of
+ * accessing these directly.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "scrolling")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected int mScrollY;
+
+ /**
+ * The final computed left padding in pixels that is used for drawing. This is the distance in
+ * pixels between the left edge of this view and the left edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ @UnsupportedAppUsage
+ protected int mPaddingLeft = 0;
+ /**
+ * The final computed right padding in pixels that is used for drawing. This is the distance in
+ * pixels between the right edge of this view and the right edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ @UnsupportedAppUsage
+ protected int mPaddingRight = 0;
+ /**
+ * The final computed top padding in pixels that is used for drawing. This is the distance in
+ * pixels between the top edge of this view and the top edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ @UnsupportedAppUsage
+ protected int mPaddingTop;
+ /**
+ * The final computed bottom padding in pixels that is used for drawing. This is the distance in
+ * pixels between the bottom edge of this view and the bottom edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ @UnsupportedAppUsage
+ protected int mPaddingBottom;
+
+ /**
+ * The layout insets in pixels, that is the distance in pixels between the
+ * visible edges of this view its bounds.
+ */
+ private Insets mLayoutInsets;
+
+ /**
+ * Briefly describes the state of the view and is primarily used for accessibility support.
+ */
+ private CharSequence mStateDescription;
+
+ /**
+ * Briefly describes the view and is primarily used for accessibility support.
+ */
+ private CharSequence mContentDescription;
+
+ /**
+ * If this view represents a distinct part of the window, it can have a title that labels the
+ * area.
+ */
+ private CharSequence mAccessibilityPaneTitle;
+
+ /**
+ * Specifies the id of a view for which this view serves as a label for
+ * accessibility purposes.
+ */
+ private int mLabelForId = View.NO_ID;
+
+ /**
+ * Predicate for matching labeled view id with its label for
+ * accessibility purposes.
+ */
+ private MatchLabelForPredicate mMatchLabelForPredicate;
+
+ /**
+ * Specifies a view before which this one is visited in accessibility traversal.
+ */
+ private int mAccessibilityTraversalBeforeId = NO_ID;
+
+ /**
+ * Specifies a view after which this one is visited in accessibility traversal.
+ */
+ private int mAccessibilityTraversalAfterId = NO_ID;
+
+ /**
+ * Predicate for matching a view by its id.
+ */
+ private MatchIdPredicate mMatchIdPredicate;
+
+ /**
+ * The right padding after RTL resolution, but before taking account of scroll bars.
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ protected int mUserPaddingRight;
+
+ /**
+ * The resolved bottom padding before taking account of scroll bars.
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ protected int mUserPaddingBottom;
+
+ /**
+ * The left padding after RTL resolution, but before taking account of scroll bars.
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ protected int mUserPaddingLeft;
+
+ /**
+ * Cache the paddingStart set by the user to append to the scrollbar's size.
+ *
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ int mUserPaddingStart;
+
+ /**
+ * Cache the paddingEnd set by the user to append to the scrollbar's size.
+ *
+ */
+ @ViewDebug.ExportedProperty(category = "padding")
+ int mUserPaddingEnd;
+
+ /**
+ * The left padding as set by a setter method, a background's padding, or via XML property
+ * resolution. This value is the padding before LTR resolution or taking account of scrollbars.
+ *
+ * @hide
+ */
+ int mUserPaddingLeftInitial;
+
+ /**
+ * The right padding as set by a setter method, a background's padding, or via XML property
+ * resolution. This value is the padding before LTR resolution or taking account of scrollbars.
+ *
+ * @hide
+ */
+ int mUserPaddingRightInitial;
+
+ /**
+ * Default undefined padding
+ */
+ private static final int UNDEFINED_PADDING = Integer.MIN_VALUE;
+
+ /**
+ * Cache if a left padding has been defined explicitly via padding, horizontal padding,
+ * or leftPadding in XML, or by setPadding(...) or setRelativePadding(...)
+ */
+ private boolean mLeftPaddingDefined = false;
+
+ /**
+ * Cache if a right padding has been defined explicitly via padding, horizontal padding,
+ * or rightPadding in XML, or by setPadding(...) or setRelativePadding(...)
+ */
+ private boolean mRightPaddingDefined = false;
+
+ /**
+ * @hide
+ */
+ int mOldWidthMeasureSpec = Integer.MIN_VALUE;
+ /**
+ * @hide
+ */
+ int mOldHeightMeasureSpec = Integer.MIN_VALUE;
+
+ private LongSparseLongArray mMeasureCache;
+
+ @ViewDebug.ExportedProperty(deepExport = true, prefix = "bg_")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Drawable mBackground;
+ private TintInfo mBackgroundTint;
+
+ @ViewDebug.ExportedProperty(deepExport = true, prefix = "fg_")
+ private ForegroundInfo mForegroundInfo;
+
+ private Drawable mScrollIndicatorDrawable;
+
+ /**
+ * RenderNode used for backgrounds.
+ * <p>
+ * When non-null and valid, this is expected to contain an up-to-date copy
+ * of the background drawable. It is cleared on temporary detach, and reset
+ * on cleanup.
+ * @hide
+ */
+ RenderNode mBackgroundRenderNode;
+
+ @UnsupportedAppUsage
+ private int mBackgroundResource;
+ private boolean mBackgroundSizeChanged;
+
+ /** The default focus highlight.
+ * @see #mDefaultFocusHighlightEnabled
+ * @see Drawable#hasFocusStateSpecified()
+ */
+ private Drawable mDefaultFocusHighlight;
+ private Drawable mDefaultFocusHighlightCache;
+ private boolean mDefaultFocusHighlightSizeChanged;
+ /**
+ * True if the default focus highlight is needed on the target device.
+ */
+ private static boolean sUseDefaultFocusHighlight;
+
+ /**
+ * True if zero-sized views can be focused.
+ */
+ private static boolean sCanFocusZeroSized;
+
+ /**
+ * Always assign focus if a focusable View is available.
+ */
+ private static boolean sAlwaysAssignFocus;
+
+ private String mTransitionName;
+
+ static class TintInfo {
+ ColorStateList mTintList;
+ BlendMode mBlendMode;
+ boolean mHasTintMode;
+ boolean mHasTintList;
+ }
+
+ private static class ForegroundInfo {
+ private Drawable mDrawable;
+ private TintInfo mTintInfo;
+ private int mGravity = Gravity.FILL;
+ private boolean mInsidePadding = true;
+ private boolean mBoundsChanged = true;
+ private final Rect mSelfBounds = new Rect();
+ private final Rect mOverlayBounds = new Rect();
+ }
+
+ static class ListenerInfo {
+
+ @UnsupportedAppUsage
+ ListenerInfo() {
+ }
+
+ /**
+ * Listener used to dispatch focus change events.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ protected OnFocusChangeListener mOnFocusChangeListener;
+
+ /**
+ * Listeners for layout change events.
+ */
+ private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
+
+ protected OnScrollChangeListener mOnScrollChangeListener;
+
+ /**
+ * Listeners for attach events.
+ */
+ private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
+
+ /**
+ * Listener used to dispatch click events.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ public OnClickListener mOnClickListener;
+
+ /**
+ * Listener used to dispatch long click events.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ protected OnLongClickListener mOnLongClickListener;
+
+ /**
+ * Listener used to dispatch context click events. This field should be made private, so it
+ * is hidden from the SDK.
+ * {@hide}
+ */
+ protected OnContextClickListener mOnContextClickListener;
+
+ /**
+ * Listener used to build the context menu.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected OnCreateContextMenuListener mOnCreateContextMenuListener;
+
+ @UnsupportedAppUsage
+ private OnKeyListener mOnKeyListener;
+
+ @UnsupportedAppUsage
+ private OnTouchListener mOnTouchListener;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private OnHoverListener mOnHoverListener;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private OnGenericMotionListener mOnGenericMotionListener;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private OnDragListener mOnDragListener;
+
+ private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
+
+ OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
+
+ OnCapturedPointerListener mOnCapturedPointerListener;
+
+ private ArrayList<OnUnhandledKeyEventListener> mUnhandledKeyListeners;
+
+ WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback;
+
+ /**
+ * This lives here since it's only valid for interactive views. This list is null until the
+ * first use.
+ */
+ private List<Rect> mSystemGestureExclusionRects = null;
+
+ /**
+ * Used to track {@link #mSystemGestureExclusionRects}
+ */
+ public RenderNode.PositionUpdateListener mPositionUpdateListener;
+
+ /**
+ * Allows the application to implement custom scroll capture support.
+ */
+ ScrollCaptureCallback mScrollCaptureCallback;
+
+ @Nullable
+ private OnReceiveContentListener mOnReceiveContentListener;
+ }
+
+ @UnsupportedAppUsage
+ ListenerInfo mListenerInfo;
+
+ private static class TooltipInfo {
+ /**
+ * Text to be displayed in a tooltip popup.
+ */
+ @Nullable
+ CharSequence mTooltipText;
+
+ /**
+ * View-relative position of the tooltip anchor point.
+ */
+ int mAnchorX;
+ int mAnchorY;
+
+ /**
+ * The tooltip popup.
+ */
+ @Nullable
+ TooltipPopup mTooltipPopup;
+
+ /**
+ * Set to true if the tooltip was shown as a result of a long click.
+ */
+ boolean mTooltipFromLongClick;
+
+ /**
+ * Keep these Runnables so that they can be used to reschedule.
+ */
+ Runnable mShowTooltipRunnable;
+ Runnable mHideTooltipRunnable;
+
+ /**
+ * Hover move is ignored if it is within this distance in pixels from the previous one.
+ */
+ int mHoverSlop;
+
+ /**
+ * Update the anchor position if it significantly (that is by at least mHoverSlop)
+ * different from the previously stored position. Ignoring insignificant changes
+ * filters out the jitter which is typical for such input sources as stylus.
+ *
+ * @return True if the position has been updated.
+ */
+ private boolean updateAnchorPos(MotionEvent event) {
+ final int newAnchorX = (int) event.getX();
+ final int newAnchorY = (int) event.getY();
+ if (Math.abs(newAnchorX - mAnchorX) <= mHoverSlop
+ && Math.abs(newAnchorY - mAnchorY) <= mHoverSlop) {
+ return false;
+ }
+ mAnchorX = newAnchorX;
+ mAnchorY = newAnchorY;
+ return true;
+ }
+
+ /**
+ * Clear the anchor position to ensure that the next change is considered significant.
+ */
+ private void clearAnchorPos() {
+ mAnchorX = Integer.MAX_VALUE;
+ mAnchorY = Integer.MAX_VALUE;
+ }
+ }
+
+ TooltipInfo mTooltipInfo;
+
+ // Temporary values used to hold (x,y) coordinates when delegating from the
+ // two-arg performLongClick() method to the legacy no-arg version.
+ private float mLongClickX = Float.NaN;
+ private float mLongClickY = Float.NaN;
+
+ /**
+ * The application environment this view lives in.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(deepExport = true)
+ @UnsupportedAppUsage
+ @UiContext
+ protected Context mContext;
+
+ @UnsupportedAppUsage
+ private final Resources mResources;
+
+ @UnsupportedAppUsage
+ private ScrollabilityCache mScrollCache;
+
+ private int[] mDrawableState = null;
+
+ ViewOutlineProvider mOutlineProvider = ViewOutlineProvider.BACKGROUND;
+
+ /**
+ * Animator that automatically runs based on state changes.
+ */
+ private StateListAnimator mStateListAnimator;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_LEFT},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusLeftId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_RIGHT},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusRightId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_UP},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusUpId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_DOWN},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusDownId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_FORWARD},
+ * the user may specify which view to go to next.
+ */
+ int mNextFocusForwardId = View.NO_ID;
+
+ /**
+ * User-specified next keyboard navigation cluster in the {@link #FOCUS_FORWARD} direction.
+ *
+ * @see #findUserSetNextKeyboardNavigationCluster(View, int)
+ */
+ int mNextClusterForwardId = View.NO_ID;
+
+ /**
+ * Whether this View should use a default focus highlight when it gets focused but doesn't
+ * have {@link android.R.attr#state_focused} defined in its background.
+ */
+ boolean mDefaultFocusHighlightEnabled = true;
+
+ private CheckForLongPress mPendingCheckForLongPress;
+ @UnsupportedAppUsage
+ private CheckForTap mPendingCheckForTap = null;
+ private PerformClick mPerformClick;
+ private SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
+ private SendAccessibilityEventThrottle mSendStateChangedAccessibilityEvent;
+ private UnsetPressedState mUnsetPressedState;
+
+ /**
+ * Whether the long press's action has been invoked. The tap's action is invoked on the
+ * up event while a long press is invoked as soon as the long press duration is reached, so
+ * a long press could be performed before the tap is checked, in which case the tap's action
+ * should not be invoked.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private boolean mHasPerformedLongPress;
+
+ /**
+ * Whether a context click button is currently pressed down. This is true when the stylus is
+ * touching the screen and the primary button has been pressed, or if a mouse's right button is
+ * pressed. This is false once the button is released or if the stylus has been lifted.
+ */
+ private boolean mInContextButtonPress;
+
+ /**
+ * Whether the next up event should be ignored for the purposes of gesture recognition. This is
+ * true after a stylus button press has occured, when the next up event should not be recognized
+ * as a tap.
+ */
+ private boolean mIgnoreNextUpEvent;
+
+ /**
+ * The minimum height of the view. We'll try our best to have the height
+ * of this view to at least this amount.
+ */
+ @ViewDebug.ExportedProperty(category = "measurement")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private int mMinHeight;
+
+ /**
+ * The minimum width of the view. We'll try our best to have the width
+ * of this view to at least this amount.
+ */
+ @ViewDebug.ExportedProperty(category = "measurement")
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private int mMinWidth;
+
+ /**
+ * The delegate to handle touch events that are physically in this view
+ * but should be handled by another view.
+ */
+ private TouchDelegate mTouchDelegate = null;
+
+ /**
+ * While touch exploration is in use, set to true when hovering across boundaries and
+ * inside the touch area of the delegate at receiving {@link MotionEvent#ACTION_HOVER_ENTER}
+ * or {@link MotionEvent#ACTION_HOVER_MOVE}. False when leaving boundaries or receiving a
+ * {@link MotionEvent#ACTION_HOVER_EXIT}.
+ * Note that children of view group are excluded in the touch area.
+ * @see #dispatchTouchExplorationHoverEvent
+ */
+ private boolean mHoveringTouchDelegate = false;
+
+ /**
+ * Solid color to use as a background when creating the drawing cache. Enables
+ * the cache to use 16 bit bitmaps instead of 32 bit.
+ */
+ private int mDrawingCacheBackgroundColor = 0;
+
+ /**
+ * Special tree observer used when mAttachInfo is null.
+ */
+ private ViewTreeObserver mFloatingTreeObserver;
+
+ /**
+ * Cache the touch slop from the context that created the view.
+ */
+ private int mTouchSlop;
+
+ /**
+ * Cache the ambiguous gesture multiplier from the context that created the view.
+ */
+ private float mAmbiguousGestureMultiplier;
+
+ /**
+ * Object that handles automatic animation of view properties.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private ViewPropertyAnimator mAnimator = null;
+
+ /**
+ * List of registered FrameMetricsObservers.
+ */
+ private ArrayList<FrameMetricsObserver> mFrameMetricsObservers;
+
+ /**
+ * Flag indicating that a drag can cross window boundaries. When
+ * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called
+ * with this flag set, all visible applications with targetSdkVersion >=
+ * {@link android.os.Build.VERSION_CODES#N API 24} will be able to participate
+ * in the drag operation and receive the dragged content.
+ *
+ * <p>If this is the only flag set, then the drag recipient will only have access to text data
+ * and intents contained in the {@link ClipData} object. Access to URIs contained in the
+ * {@link ClipData} is determined by other DRAG_FLAG_GLOBAL_* flags</p>
+ */
+ public static final int DRAG_FLAG_GLOBAL = 1 << 8; // 256
+
+ /**
+ * When this flag is used with {@link #DRAG_FLAG_GLOBAL}, the drag recipient will be able to
+ * request read access to the content URI(s) contained in the {@link ClipData} object.
+ * @see android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION
+ */
+ public static final int DRAG_FLAG_GLOBAL_URI_READ = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+ /**
+ * When this flag is used with {@link #DRAG_FLAG_GLOBAL}, the drag recipient will be able to
+ * request write access to the content URI(s) contained in the {@link ClipData} object.
+ * @see android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ */
+ public static final int DRAG_FLAG_GLOBAL_URI_WRITE = Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+
+ /**
+ * When this flag is used with {@link #DRAG_FLAG_GLOBAL_URI_READ} and/or {@link
+ * #DRAG_FLAG_GLOBAL_URI_WRITE}, the URI permission grant can be persisted across device
+ * reboots until explicitly revoked with
+ * {@link android.content.Context#revokeUriPermission(Uri, int)} Context.revokeUriPermission}.
+ * @see android.content.Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ */
+ public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION =
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
+
+ /**
+ * When this flag is used with {@link #DRAG_FLAG_GLOBAL_URI_READ} and/or {@link
+ * #DRAG_FLAG_GLOBAL_URI_WRITE}, the URI permission grant applies to any URI that is a prefix
+ * match against the original granted URI.
+ * @see android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION
+ */
+ public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION =
+ Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ /**
+ * Flag indicating that the drag shadow will be opaque. When
+ * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called
+ * with this flag set, the drag shadow will be opaque, otherwise, it will be semitransparent.
+ */
+ public static final int DRAG_FLAG_OPAQUE = 1 << 9;
+
+ /**
+ * Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
+ */
+ private float mVerticalScrollFactor;
+
+ /**
+ * Position of the vertical scroll bar.
+ */
+ @UnsupportedAppUsage
+ private int mVerticalScrollbarPosition;
+
+ /**
+ * Position the scroll bar at the default position as determined by the system.
+ */
+ public static final int SCROLLBAR_POSITION_DEFAULT = 0;
+
+ /**
+ * Position the scroll bar along the left edge.
+ */
+ public static final int SCROLLBAR_POSITION_LEFT = 1;
+
+ /**
+ * Position the scroll bar along the right edge.
+ */
+ public static final int SCROLLBAR_POSITION_RIGHT = 2;
+
+ /**
+ * Indicates that the view does not have a layer.
+ *
+ * @see #getLayerType()
+ * @see #setLayerType(int, android.graphics.Paint)
+ * @see #LAYER_TYPE_SOFTWARE
+ * @see #LAYER_TYPE_HARDWARE
+ */
+ public static final int LAYER_TYPE_NONE = 0;
+
+ /**
+ * <p>Indicates that the view has a software layer. A software layer is backed
+ * by a bitmap and causes the view to be rendered using Android's software
+ * rendering pipeline, even if hardware acceleration is enabled.</p>
+ *
+ * <p>Software layers have various usages:</p>
+ * <p>When the application is not using hardware acceleration, a software layer
+ * is useful to apply a specific color filter and/or blending mode and/or
+ * translucency to a view and all its children.</p>
+ * <p>When the application is using hardware acceleration, a software layer
+ * is useful to render drawing primitives not supported by the hardware
+ * accelerated pipeline. It can also be used to cache a complex view tree
+ * into a texture and reduce the complexity of drawing operations. For instance,
+ * when animating a complex view tree with a translation, a software layer can
+ * be used to render the view tree only once.</p>
+ * <p>Software layers should be avoided when the affected view tree updates
+ * often. Every update will require to re-render the software layer, which can
+ * potentially be slow (particularly when hardware acceleration is turned on
+ * since the layer will have to be uploaded into a hardware texture after every
+ * update.)</p>
+ *
+ * @see #getLayerType()
+ * @see #setLayerType(int, android.graphics.Paint)
+ * @see #LAYER_TYPE_NONE
+ * @see #LAYER_TYPE_HARDWARE
+ */
+ public static final int LAYER_TYPE_SOFTWARE = 1;
+
+ /**
+ * <p>Indicates that the view has a hardware layer. A hardware layer is backed
+ * by a hardware specific texture (generally Frame Buffer Objects or FBO on
+ * OpenGL hardware) and causes the view to be rendered using Android's hardware
+ * rendering pipeline, but only if hardware acceleration is turned on for the
+ * view hierarchy. When hardware acceleration is turned off, hardware layers
+ * behave exactly as {@link #LAYER_TYPE_SOFTWARE software layers}.</p>
+ *
+ * <p>A hardware layer is useful to apply a specific color filter and/or
+ * blending mode and/or translucency to a view and all its children.</p>
+ * <p>A hardware layer can be used to cache a complex view tree into a
+ * texture and reduce the complexity of drawing operations. For instance,
+ * when animating a complex view tree with a translation, a hardware layer can
+ * be used to render the view tree only once.</p>
+ * <p>A hardware layer can also be used to increase the rendering quality when
+ * rotation transformations are applied on a view. It can also be used to
+ * prevent potential clipping issues when applying 3D transforms on a view.</p>
+ *
+ * @see #getLayerType()
+ * @see #setLayerType(int, android.graphics.Paint)
+ * @see #LAYER_TYPE_NONE
+ * @see #LAYER_TYPE_SOFTWARE
+ */
+ public static final int LAYER_TYPE_HARDWARE = 2;
+
+ /** @hide */
+ @IntDef(prefix = { "LAYER_TYPE_" }, value = {
+ LAYER_TYPE_NONE,
+ LAYER_TYPE_SOFTWARE,
+ LAYER_TYPE_HARDWARE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LayerType {}
+
+ @ViewDebug.ExportedProperty(category = "drawing", mapping = {
+ @ViewDebug.IntToString(from = LAYER_TYPE_NONE, to = "NONE"),
+ @ViewDebug.IntToString(from = LAYER_TYPE_SOFTWARE, to = "SOFTWARE"),
+ @ViewDebug.IntToString(from = LAYER_TYPE_HARDWARE, to = "HARDWARE")
+ })
+ int mLayerType = LAYER_TYPE_NONE;
+ Paint mLayerPaint;
+
+ /**
+ * Set to true when drawing cache is enabled and cannot be created.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean mCachingFailed;
+ @UnsupportedAppUsage
+ private Bitmap mDrawingCache;
+ @UnsupportedAppUsage
+ private Bitmap mUnscaledDrawingCache;
+
+ /**
+ * RenderNode holding View properties, potentially holding a DisplayList of View content.
+ * <p>
+ * When non-null and valid, this is expected to contain an up-to-date copy
+ * of the View content. Its DisplayList content is cleared on temporary detach and reset on
+ * cleanup.
+ */
+ @UnsupportedAppUsage
+ final RenderNode mRenderNode;
+
+ /**
+ * Set to true when the view is sending hover accessibility events because it
+ * is the innermost hovered view.
+ */
+ private boolean mSendingHoverAccessibilityEvents;
+
+ /**
+ * Delegate for injecting accessibility functionality.
+ */
+ @UnsupportedAppUsage
+ AccessibilityDelegate mAccessibilityDelegate;
+
+ /**
+ * The view's overlay layer. Developers get a reference to the overlay via getOverlay()
+ * and add/remove objects to/from the overlay directly through the Overlay methods.
+ */
+ ViewOverlay mOverlay;
+
+ /**
+ * The currently active parent view for receiving delegated nested scrolling events.
+ * This is set by {@link #startNestedScroll(int)} during a touch interaction and cleared
+ * by {@link #stopNestedScroll()} at the same point where we clear
+ * requestDisallowInterceptTouchEvent.
+ */
+ private ViewParent mNestedScrollingParent;
+
+ /**
+ * Consistency verifier for debugging purposes.
+ * @hide
+ */
+ protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled() ?
+ new InputEventConsistencyVerifier(this, 0) : null;
+
+ private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
+
+ private int[] mTempNestedScrollConsumed;
+
+ /**
+ * An overlay is going to draw this View instead of being drawn as part of this
+ * View's parent. mGhostView is the View in the Overlay that must be invalidated
+ * when this view is invalidated.
+ */
+ GhostView mGhostView;
+
+ /**
+ * Holds pairs of adjacent attribute data: attribute name followed by its value.
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "attributes", hasAdjacentMapping = true)
+ public String[] mAttributes;
+
+ /**
+ * Maps a Resource id to its name.
+ */
+ private static SparseArray<String> mAttributeMap;
+
+ /**
+ * Queue of pending runnables. Used to postpone calls to post() until this
+ * view is attached and has a handler.
+ */
+ private HandlerActionQueue mRunQueue;
+
+ /**
+ * The pointer icon when the mouse hovers on this view. The default is null.
+ */
+ private PointerIcon mPointerIcon;
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ String mStartActivityRequestWho;
+
+ @Nullable
+ private RoundScrollbarRenderer mRoundScrollbarRenderer;
+
+ /** Used to delay visibility updates sent to the autofill manager */
+ private Handler mVisibilityChangeForAutofillHandler;
+
+ /**
+ * Used when app developers explicitly set the {@link ContentCaptureSession} associated with the
+ * view (through {@link #setContentCaptureSession(ContentCaptureSession)}.
+ */
+ @Nullable
+ private ContentCaptureSession mContentCaptureSession;
+
+ /**
+ * Whether {@link ContentCaptureSession} is cached, resets on {@link #invalidate()}.
+ */
+ private boolean mContentCaptureSessionCached;
+
+ @LayoutRes
+ private int mSourceLayoutId = ID_NULL;
+
+ @Nullable
+ private SparseIntArray mAttributeSourceResId;
+
+ @Nullable
+ private SparseArray<int[]> mAttributeResolutionStacks;
+
+ @StyleRes
+ private int mExplicitStyle;
+
+ /**
+ * Specifies which input source classes should provide unbuffered input events to this view
+ *
+ * @see View#requestUnbufferedDispatch(int)
+ */
+ @InputSourceClass
+ int mUnbufferedInputSource = InputDevice.SOURCE_CLASS_NONE;
+
+ @Nullable
+ private String[] mReceiveContentMimeTypes;
+
+ @Nullable
+ private ViewTranslationCallback mViewTranslationCallback;
+
+ @Nullable
+
+ private ViewTranslationResponse mViewTranslationResponse;
+
+ /**
+ * Simple constructor to use when creating a view from code.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ */
+ public View(Context context) {
+ mContext = context;
+ mResources = context != null ? context.getResources() : null;
+ mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | FOCUSABLE_AUTO;
+ // Set some flags defaults
+ mPrivateFlags2 =
+ (LAYOUT_DIRECTION_DEFAULT << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT) |
+ (TEXT_DIRECTION_DEFAULT << PFLAG2_TEXT_DIRECTION_MASK_SHIFT) |
+ (PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT) |
+ (TEXT_ALIGNMENT_DEFAULT << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT) |
+ (PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT) |
+ (IMPORTANT_FOR_ACCESSIBILITY_DEFAULT << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT);
+
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mAmbiguousGestureMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
+
+ setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS);
+ mUserPaddingStart = UNDEFINED_PADDING;
+ mUserPaddingEnd = UNDEFINED_PADDING;
+ mRenderNode = RenderNode.create(getClass().getName(), new ViewAnimationHostBridge(this));
+
+ if (!sCompatibilityDone && context != null) {
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+
+ // Older apps may need this compatibility hack for measurement.
+ sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+ // Older apps expect onMeasure() to always be called on a layout pass, regardless
+ // of whether a layout was requested on that View.
+ sIgnoreMeasureCache = targetSdkVersion < Build.VERSION_CODES.KITKAT;
+
+ // In M and newer, our widgets can pass a "hint" value in the size
+ // for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
+ // know what the expected parent size is going to be, so e.g. list items can size
+ // themselves at 1/3 the size of their container. It breaks older apps though,
+ // specifically apps that use some popular open source libraries.
+ sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;
+
+ // Old versions of the platform would give different results from
+ // LinearLayout measurement passes using EXACTLY and non-EXACTLY
+ // modes, so we always need to run an additional EXACTLY pass.
+ sAlwaysRemeasureExactly = targetSdkVersion <= Build.VERSION_CODES.M;
+
+ // Prior to N, TextureView would silently ignore calls to setBackground/setForeground.
+ // On N+, we throw, but that breaks compatibility with apps that use these methods.
+ sTextureViewIgnoresDrawableSetters = targetSdkVersion <= Build.VERSION_CODES.M;
+
+ // Prior to N, we would drop margins in LayoutParam conversions. The fix triggers bugs
+ // in apps so we target check it to avoid breaking existing apps.
+ sPreserveMarginParamsInLayoutParamConversion =
+ targetSdkVersion >= Build.VERSION_CODES.N;
+
+ sCascadedDragDrop = targetSdkVersion < Build.VERSION_CODES.N;
+
+ sHasFocusableExcludeAutoFocusable = targetSdkVersion < Build.VERSION_CODES.O;
+
+ sAutoFocusableOffUIThreadWontNotifyParents = targetSdkVersion < Build.VERSION_CODES.O;
+
+ sUseDefaultFocusHighlight = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_useDefaultFocusHighlight);
+
+ sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P;
+
+ sCanFocusZeroSized = targetSdkVersion < Build.VERSION_CODES.P;
+
+ sAlwaysAssignFocus = targetSdkVersion < Build.VERSION_CODES.P;
+
+ sAcceptZeroSizeDragShadow = targetSdkVersion < Build.VERSION_CODES.P;
+
+ sBrokenInsetsDispatch = targetSdkVersion < Build.VERSION_CODES.R;
+
+ sBrokenWindowBackground = targetSdkVersion < Build.VERSION_CODES.Q;
+
+ GradientDrawable.sWrapNegativeAngleMeasurements =
+ targetSdkVersion >= Build.VERSION_CODES.Q;
+
+ sForceLayoutWhenInsetsChanged = targetSdkVersion < Build.VERSION_CODES.R;
+
+ sCompatibilityDone = true;
+ }
+ }
+
+ /**
+ * Constructor that is called when inflating a view from XML. This is called
+ * when a view is being constructed from an XML file, supplying attributes
+ * that were specified in the XML file. This version uses a default style of
+ * 0, so the only attribute values applied are those in the Context's Theme
+ * and the given AttributeSet.
+ *
+ * <p>
+ * The method onFinishInflate() will be called after all children have been
+ * added.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @see #View(Context, AttributeSet, int)
+ */
+ public View(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style from a
+ * theme attribute. This constructor of View allows subclasses to use their
+ * own base style when they are inflating. For example, a Button class's
+ * constructor would call this version of the super class constructor and
+ * supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this
+ * allows the theme's button style to modify all of the base view attributes
+ * (in particular its background) as well as the Button class's attributes.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @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.
+ * @see #View(Context, AttributeSet)
+ */
+ public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style from a
+ * theme attribute or style resource. This constructor of View allows
+ * subclasses to use their own base style when they are inflating.
+ * <p>
+ * When determining the final value of a particular attribute, there are
+ * four inputs that come into play:
+ * <ol>
+ * <li>Any attribute values in the given AttributeSet.
+ * <li>The style resource specified in the AttributeSet (named "style").
+ * <li>The default style specified by <var>defStyleAttr</var>.
+ * <li>The default style specified by <var>defStyleRes</var>.
+ * <li>The base values in this theme.
+ * </ol>
+ * <p>
+ * Each of these inputs is considered in-order, with the first listed taking
+ * precedence over the following ones. In other words, if in the
+ * AttributeSet you have supplied <code><Button * textColor="#ff000000"></code>
+ * , then the button's text will <em>always</em> be black, regardless of
+ * what is specified in any of the styles.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @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.
+ * @see #View(Context, AttributeSet, int)
+ */
+ public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ this(context);
+
+ mSourceLayoutId = Resources.getAttributeSetSourceResId(attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
+
+ retrieveExplicitStyle(context.getTheme(), attrs);
+ saveAttributeDataForStyleable(context, com.android.internal.R.styleable.View, attrs, a,
+ defStyleAttr, defStyleRes);
+
+ if (sDebugViewAttributes) {
+ saveAttributeData(attrs, a);
+ }
+
+ Drawable background = null;
+
+ int leftPadding = -1;
+ int topPadding = -1;
+ int rightPadding = -1;
+ int bottomPadding = -1;
+ int startPadding = UNDEFINED_PADDING;
+ int endPadding = UNDEFINED_PADDING;
+
+ int padding = -1;
+ int paddingHorizontal = -1;
+ int paddingVertical = -1;
+
+ int viewFlagValues = 0;
+ int viewFlagMasks = 0;
+
+ boolean setScrollContainer = false;
+
+ int x = 0;
+ int y = 0;
+
+ float tx = 0;
+ float ty = 0;
+ float tz = 0;
+ float elevation = 0;
+ float rotation = 0;
+ float rotationX = 0;
+ float rotationY = 0;
+ float sx = 1f;
+ float sy = 1f;
+ boolean transformSet = false;
+
+ int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY;
+ int overScrollMode = mOverScrollMode;
+ boolean initializeScrollbars = false;
+ boolean initializeScrollIndicators = false;
+
+ boolean startPaddingDefined = false;
+ boolean endPaddingDefined = false;
+ boolean leftPaddingDefined = false;
+ boolean rightPaddingDefined = false;
+
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+
+ // Set default values.
+ viewFlagValues |= FOCUSABLE_AUTO;
+ viewFlagMasks |= FOCUSABLE_AUTO;
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case com.android.internal.R.styleable.View_background:
+ background = a.getDrawable(attr);
+ break;
+ case com.android.internal.R.styleable.View_padding:
+ padding = a.getDimensionPixelSize(attr, -1);
+ mUserPaddingLeftInitial = padding;
+ mUserPaddingRightInitial = padding;
+ leftPaddingDefined = true;
+ rightPaddingDefined = true;
+ break;
+ case com.android.internal.R.styleable.View_paddingHorizontal:
+ paddingHorizontal = a.getDimensionPixelSize(attr, -1);
+ mUserPaddingLeftInitial = paddingHorizontal;
+ mUserPaddingRightInitial = paddingHorizontal;
+ leftPaddingDefined = true;
+ rightPaddingDefined = true;
+ break;
+ case com.android.internal.R.styleable.View_paddingVertical:
+ paddingVertical = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingLeft:
+ leftPadding = a.getDimensionPixelSize(attr, -1);
+ mUserPaddingLeftInitial = leftPadding;
+ leftPaddingDefined = true;
+ break;
+ case com.android.internal.R.styleable.View_paddingTop:
+ topPadding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingRight:
+ rightPadding = a.getDimensionPixelSize(attr, -1);
+ mUserPaddingRightInitial = rightPadding;
+ rightPaddingDefined = true;
+ break;
+ case com.android.internal.R.styleable.View_paddingBottom:
+ bottomPadding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingStart:
+ startPadding = a.getDimensionPixelSize(attr, UNDEFINED_PADDING);
+ startPaddingDefined = (startPadding != UNDEFINED_PADDING);
+ break;
+ case com.android.internal.R.styleable.View_paddingEnd:
+ endPadding = a.getDimensionPixelSize(attr, UNDEFINED_PADDING);
+ endPaddingDefined = (endPadding != UNDEFINED_PADDING);
+ break;
+ case com.android.internal.R.styleable.View_scrollX:
+ x = a.getDimensionPixelOffset(attr, 0);
+ break;
+ case com.android.internal.R.styleable.View_scrollY:
+ y = a.getDimensionPixelOffset(attr, 0);
+ break;
+ case com.android.internal.R.styleable.View_alpha:
+ setAlpha(a.getFloat(attr, 1f));
+ break;
+ case com.android.internal.R.styleable.View_transformPivotX:
+ setPivotX(a.getDimension(attr, 0));
+ break;
+ case com.android.internal.R.styleable.View_transformPivotY:
+ setPivotY(a.getDimension(attr, 0));
+ break;
+ case com.android.internal.R.styleable.View_translationX:
+ tx = a.getDimension(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_translationY:
+ ty = a.getDimension(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_translationZ:
+ tz = a.getDimension(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_elevation:
+ elevation = a.getDimension(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_rotation:
+ rotation = a.getFloat(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_rotationX:
+ rotationX = a.getFloat(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_rotationY:
+ rotationY = a.getFloat(attr, 0);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_scaleX:
+ sx = a.getFloat(attr, 1f);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_scaleY:
+ sy = a.getFloat(attr, 1f);
+ transformSet = true;
+ break;
+ case com.android.internal.R.styleable.View_id:
+ mID = a.getResourceId(attr, NO_ID);
+ break;
+ case com.android.internal.R.styleable.View_tag:
+ mTag = a.getText(attr);
+ break;
+ case com.android.internal.R.styleable.View_fitsSystemWindows:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= FITS_SYSTEM_WINDOWS;
+ viewFlagMasks |= FITS_SYSTEM_WINDOWS;
+ }
+ break;
+ case com.android.internal.R.styleable.View_focusable:
+ viewFlagValues = (viewFlagValues & ~FOCUSABLE_MASK) | getFocusableAttribute(a);
+ if ((viewFlagValues & FOCUSABLE_AUTO) == 0) {
+ viewFlagMasks |= FOCUSABLE_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_focusableInTouchMode:
+ if (a.getBoolean(attr, false)) {
+ // unset auto focus since focusableInTouchMode implies explicit focusable
+ viewFlagValues &= ~FOCUSABLE_AUTO;
+ viewFlagValues |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE;
+ viewFlagMasks |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_clickable:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= CLICKABLE;
+ viewFlagMasks |= CLICKABLE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_allowClickWhenDisabled:
+ setAllowClickWhenDisabled(a.getBoolean(attr, false));
+ break;
+ case com.android.internal.R.styleable.View_longClickable:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= LONG_CLICKABLE;
+ viewFlagMasks |= LONG_CLICKABLE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_contextClickable:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= CONTEXT_CLICKABLE;
+ viewFlagMasks |= CONTEXT_CLICKABLE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_saveEnabled:
+ if (!a.getBoolean(attr, true)) {
+ viewFlagValues |= SAVE_DISABLED;
+ viewFlagMasks |= SAVE_DISABLED_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_duplicateParentState:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= DUPLICATE_PARENT_STATE;
+ viewFlagMasks |= DUPLICATE_PARENT_STATE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_visibility:
+ final int visibility = a.getInt(attr, 0);
+ if (visibility != 0) {
+ viewFlagValues |= VISIBILITY_FLAGS[visibility];
+ viewFlagMasks |= VISIBILITY_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_layoutDirection:
+ // Clear any layout direction flags (included resolved bits) already set
+ mPrivateFlags2 &=
+ ~(PFLAG2_LAYOUT_DIRECTION_MASK | PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK);
+ // Set the layout direction flags depending on the value of the attribute
+ final int layoutDirection = a.getInt(attr, -1);
+ final int value = (layoutDirection != -1) ?
+ LAYOUT_DIRECTION_FLAGS[layoutDirection] : LAYOUT_DIRECTION_DEFAULT;
+ mPrivateFlags2 |= (value << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT);
+ break;
+ case com.android.internal.R.styleable.View_drawingCacheQuality:
+ final int cacheQuality = a.getInt(attr, 0);
+ if (cacheQuality != 0) {
+ viewFlagValues |= DRAWING_CACHE_QUALITY_FLAGS[cacheQuality];
+ viewFlagMasks |= DRAWING_CACHE_QUALITY_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_contentDescription:
+ setContentDescription(a.getString(attr));
+ break;
+ case com.android.internal.R.styleable.View_accessibilityTraversalBefore:
+ setAccessibilityTraversalBefore(a.getResourceId(attr, NO_ID));
+ break;
+ case com.android.internal.R.styleable.View_accessibilityTraversalAfter:
+ setAccessibilityTraversalAfter(a.getResourceId(attr, NO_ID));
+ break;
+ case com.android.internal.R.styleable.View_labelFor:
+ setLabelFor(a.getResourceId(attr, NO_ID));
+ break;
+ case com.android.internal.R.styleable.View_soundEffectsEnabled:
+ if (!a.getBoolean(attr, true)) {
+ viewFlagValues &= ~SOUND_EFFECTS_ENABLED;
+ viewFlagMasks |= SOUND_EFFECTS_ENABLED;
+ }
+ break;
+ case com.android.internal.R.styleable.View_hapticFeedbackEnabled:
+ if (!a.getBoolean(attr, true)) {
+ viewFlagValues &= ~HAPTIC_FEEDBACK_ENABLED;
+ viewFlagMasks |= HAPTIC_FEEDBACK_ENABLED;
+ }
+ break;
+ case R.styleable.View_scrollbars:
+ final int scrollbars = a.getInt(attr, SCROLLBARS_NONE);
+ if (scrollbars != SCROLLBARS_NONE) {
+ viewFlagValues |= scrollbars;
+ viewFlagMasks |= SCROLLBARS_MASK;
+ initializeScrollbars = true;
+ }
+ break;
+ //noinspection deprecation
+ case R.styleable.View_fadingEdge:
+ if (targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ // Ignore the attribute starting with ICS
+ break;
+ }
+ // With builds < ICS, fall through and apply fading edges
+ case R.styleable.View_requiresFadingEdge:
+ final int fadingEdge = a.getInt(attr, FADING_EDGE_NONE);
+ if (fadingEdge != FADING_EDGE_NONE) {
+ viewFlagValues |= fadingEdge;
+ viewFlagMasks |= FADING_EDGE_MASK;
+ initializeFadingEdgeInternal(a);
+ }
+ break;
+ case R.styleable.View_scrollbarStyle:
+ scrollbarStyle = a.getInt(attr, SCROLLBARS_INSIDE_OVERLAY);
+ if (scrollbarStyle != SCROLLBARS_INSIDE_OVERLAY) {
+ viewFlagValues |= scrollbarStyle & SCROLLBARS_STYLE_MASK;
+ viewFlagMasks |= SCROLLBARS_STYLE_MASK;
+ }
+ break;
+ case R.styleable.View_isScrollContainer:
+ setScrollContainer = true;
+ if (a.getBoolean(attr, false)) {
+ setScrollContainer(true);
+ }
+ break;
+ case com.android.internal.R.styleable.View_keepScreenOn:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= KEEP_SCREEN_ON;
+ viewFlagMasks |= KEEP_SCREEN_ON;
+ }
+ break;
+ case R.styleable.View_filterTouchesWhenObscured:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= FILTER_TOUCHES_WHEN_OBSCURED;
+ viewFlagMasks |= FILTER_TOUCHES_WHEN_OBSCURED;
+ }
+ break;
+ case R.styleable.View_nextFocusLeft:
+ mNextFocusLeftId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusRight:
+ mNextFocusRightId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusUp:
+ mNextFocusUpId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusDown:
+ mNextFocusDownId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusForward:
+ mNextFocusForwardId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextClusterForward:
+ mNextClusterForwardId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_minWidth:
+ mMinWidth = a.getDimensionPixelSize(attr, 0);
+ break;
+ case R.styleable.View_minHeight:
+ mMinHeight = a.getDimensionPixelSize(attr, 0);
+ break;
+ case R.styleable.View_onClick:
+ if (context.isRestricted()) {
+ throw new IllegalStateException("The android:onClick attribute cannot "
+ + "be used within a restricted context");
+ }
+
+ final String handlerName = a.getString(attr);
+ if (handlerName != null) {
+ setOnClickListener(new DeclaredOnClickListener(this, handlerName));
+ }
+ break;
+ case R.styleable.View_overScrollMode:
+ overScrollMode = a.getInt(attr, OVER_SCROLL_IF_CONTENT_SCROLLS);
+ break;
+ case R.styleable.View_verticalScrollbarPosition:
+ mVerticalScrollbarPosition = a.getInt(attr, SCROLLBAR_POSITION_DEFAULT);
+ break;
+ case R.styleable.View_layerType:
+ setLayerType(a.getInt(attr, LAYER_TYPE_NONE), null);
+ break;
+ case R.styleable.View_textDirection:
+ // Clear any text direction flag already set
+ mPrivateFlags2 &= ~PFLAG2_TEXT_DIRECTION_MASK;
+ // Set the text direction flags depending on the value of the attribute
+ final int textDirection = a.getInt(attr, -1);
+ if (textDirection != -1) {
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_FLAGS[textDirection];
+ }
+ break;
+ case R.styleable.View_textAlignment:
+ // Clear any text alignment flag already set
+ mPrivateFlags2 &= ~PFLAG2_TEXT_ALIGNMENT_MASK;
+ // Set the text alignment flag depending on the value of the attribute
+ final int textAlignment = a.getInt(attr, TEXT_ALIGNMENT_DEFAULT);
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_FLAGS[textAlignment];
+ break;
+ case R.styleable.View_importantForAccessibility:
+ setImportantForAccessibility(a.getInt(attr,
+ IMPORTANT_FOR_ACCESSIBILITY_DEFAULT));
+ break;
+ case R.styleable.View_accessibilityLiveRegion:
+ setAccessibilityLiveRegion(a.getInt(attr, ACCESSIBILITY_LIVE_REGION_DEFAULT));
+ break;
+ case R.styleable.View_transitionName:
+ setTransitionName(a.getString(attr));
+ break;
+ case R.styleable.View_nestedScrollingEnabled:
+ setNestedScrollingEnabled(a.getBoolean(attr, false));
+ break;
+ case R.styleable.View_stateListAnimator:
+ setStateListAnimator(AnimatorInflater.loadStateListAnimator(context,
+ a.getResourceId(attr, 0)));
+ break;
+ case R.styleable.View_backgroundTint:
+ // This will get applied later during setBackground().
+ if (mBackgroundTint == null) {
+ mBackgroundTint = new TintInfo();
+ }
+ mBackgroundTint.mTintList = a.getColorStateList(
+ R.styleable.View_backgroundTint);
+ mBackgroundTint.mHasTintList = true;
+ break;
+ case R.styleable.View_backgroundTintMode:
+ // This will get applied later during setBackground().
+ if (mBackgroundTint == null) {
+ mBackgroundTint = new TintInfo();
+ }
+ mBackgroundTint.mBlendMode = Drawable.parseBlendMode(a.getInt(
+ R.styleable.View_backgroundTintMode, -1), null);
+ mBackgroundTint.mHasTintMode = true;
+ break;
+ case R.styleable.View_outlineProvider:
+ setOutlineProviderFromAttribute(a.getInt(R.styleable.View_outlineProvider,
+ PROVIDER_BACKGROUND));
+ break;
+ case R.styleable.View_foreground:
+ if (targetSdkVersion >= Build.VERSION_CODES.M || this instanceof FrameLayout) {
+ setForeground(a.getDrawable(attr));
+ }
+ break;
+ case R.styleable.View_foregroundGravity:
+ if (targetSdkVersion >= Build.VERSION_CODES.M || this instanceof FrameLayout) {
+ setForegroundGravity(a.getInt(attr, Gravity.NO_GRAVITY));
+ }
+ break;
+ case R.styleable.View_foregroundTintMode:
+ if (targetSdkVersion >= Build.VERSION_CODES.M || this instanceof FrameLayout) {
+ setForegroundTintBlendMode(
+ Drawable.parseBlendMode(a.getInt(attr, -1),
+ null));
+ }
+ break;
+ case R.styleable.View_foregroundTint:
+ if (targetSdkVersion >= Build.VERSION_CODES.M || this instanceof FrameLayout) {
+ setForegroundTintList(a.getColorStateList(attr));
+ }
+ break;
+ case R.styleable.View_foregroundInsidePadding:
+ if (targetSdkVersion >= Build.VERSION_CODES.M || this instanceof FrameLayout) {
+ if (mForegroundInfo == null) {
+ mForegroundInfo = new ForegroundInfo();
+ }
+ mForegroundInfo.mInsidePadding = a.getBoolean(attr,
+ mForegroundInfo.mInsidePadding);
+ }
+ break;
+ case R.styleable.View_scrollIndicators:
+ final int scrollIndicators =
+ (a.getInt(attr, 0) << SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT)
+ & SCROLL_INDICATORS_PFLAG3_MASK;
+ if (scrollIndicators != 0) {
+ mPrivateFlags3 |= scrollIndicators;
+ initializeScrollIndicators = true;
+ }
+ break;
+ case R.styleable.View_pointerIcon:
+ final int resourceId = a.getResourceId(attr, 0);
+ if (resourceId != 0) {
+ setPointerIcon(PointerIcon.load(
+ context.getResources(), resourceId));
+ } else {
+ final int pointerType = a.getInt(attr, PointerIcon.TYPE_NOT_SPECIFIED);
+ if (pointerType != PointerIcon.TYPE_NOT_SPECIFIED) {
+ setPointerIcon(PointerIcon.getSystemIcon(context, pointerType));
+ }
+ }
+ break;
+ case R.styleable.View_forceHasOverlappingRendering:
+ if (a.peekValue(attr) != null) {
+ forceHasOverlappingRendering(a.getBoolean(attr, true));
+ }
+ break;
+ case R.styleable.View_tooltipText:
+ setTooltipText(a.getText(attr));
+ break;
+ case R.styleable.View_keyboardNavigationCluster:
+ if (a.peekValue(attr) != null) {
+ setKeyboardNavigationCluster(a.getBoolean(attr, true));
+ }
+ break;
+ case R.styleable.View_focusedByDefault:
+ if (a.peekValue(attr) != null) {
+ setFocusedByDefault(a.getBoolean(attr, true));
+ }
+ break;
+ case R.styleable.View_autofillHints:
+ if (a.peekValue(attr) != null) {
+ CharSequence[] rawHints = null;
+ String rawString = null;
+
+ if (a.getType(attr) == TypedValue.TYPE_REFERENCE) {
+ int resId = a.getResourceId(attr, 0);
+
+ try {
+ rawHints = a.getTextArray(attr);
+ } catch (Resources.NotFoundException e) {
+ rawString = getResources().getString(resId);
+ }
+ } else {
+ rawString = a.getString(attr);
+ }
+
+ if (rawHints == null) {
+ if (rawString == null) {
+ throw new IllegalArgumentException(
+ "Could not resolve autofillHints");
+ } else {
+ rawHints = rawString.split(",");
+ }
+ }
+
+ String[] hints = new String[rawHints.length];
+
+ int numHints = rawHints.length;
+ for (int rawHintNum = 0; rawHintNum < numHints; rawHintNum++) {
+ hints[rawHintNum] = rawHints[rawHintNum].toString().trim();
+ }
+ setAutofillHints(hints);
+ }
+ break;
+ case R.styleable.View_importantForAutofill:
+ if (a.peekValue(attr) != null) {
+ setImportantForAutofill(a.getInt(attr, IMPORTANT_FOR_AUTOFILL_AUTO));
+ }
+ break;
+ case R.styleable.View_importantForContentCapture:
+ if (a.peekValue(attr) != null) {
+ setImportantForContentCapture(a.getInt(attr,
+ IMPORTANT_FOR_CONTENT_CAPTURE_AUTO));
+ }
+ case R.styleable.View_defaultFocusHighlightEnabled:
+ if (a.peekValue(attr) != null) {
+ setDefaultFocusHighlightEnabled(a.getBoolean(attr, true));
+ }
+ break;
+ case R.styleable.View_screenReaderFocusable:
+ if (a.peekValue(attr) != null) {
+ setScreenReaderFocusable(a.getBoolean(attr, false));
+ }
+ break;
+ case R.styleable.View_accessibilityPaneTitle:
+ if (a.peekValue(attr) != null) {
+ setAccessibilityPaneTitle(a.getString(attr));
+ }
+ break;
+ case R.styleable.View_outlineSpotShadowColor:
+ setOutlineSpotShadowColor(a.getColor(attr, Color.BLACK));
+ break;
+ case R.styleable.View_outlineAmbientShadowColor:
+ setOutlineAmbientShadowColor(a.getColor(attr, Color.BLACK));
+ break;
+ case com.android.internal.R.styleable.View_accessibilityHeading:
+ setAccessibilityHeading(a.getBoolean(attr, false));
+ break;
+ case R.styleable.View_forceDarkAllowed:
+ mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
+ break;
+ case R.styleable.View_scrollCaptureHint:
+ setScrollCaptureHint((a.getInt(attr, SCROLL_CAPTURE_HINT_AUTO)));
+ break;
+ case R.styleable.View_clipToOutline:
+ setClipToOutline(a.getBoolean(attr, false));
+ break;
+ }
+ }
+
+ setOverScrollMode(overScrollMode);
+
+ // Cache start/end user padding as we cannot fully resolve padding here (we don't have yet
+ // the resolved layout direction). Those cached values will be used later during padding
+ // resolution.
+ mUserPaddingStart = startPadding;
+ mUserPaddingEnd = endPadding;
+
+ if (background != null) {
+ setBackground(background);
+ }
+
+ // setBackground above will record that padding is currently provided by the background.
+ // If we have padding specified via xml, record that here instead and use it.
+ mLeftPaddingDefined = leftPaddingDefined;
+ mRightPaddingDefined = rightPaddingDefined;
+
+ // Valid paddingHorizontal/paddingVertical beats leftPadding, rightPadding, topPadding,
+ // bottomPadding, and padding set by background. Valid padding beats everything.
+ if (padding >= 0) {
+ leftPadding = padding;
+ topPadding = padding;
+ rightPadding = padding;
+ bottomPadding = padding;
+ mUserPaddingLeftInitial = padding;
+ mUserPaddingRightInitial = padding;
+ } else {
+ if (paddingHorizontal >= 0) {
+ leftPadding = paddingHorizontal;
+ rightPadding = paddingHorizontal;
+ mUserPaddingLeftInitial = paddingHorizontal;
+ mUserPaddingRightInitial = paddingHorizontal;
+ }
+ if (paddingVertical >= 0) {
+ topPadding = paddingVertical;
+ bottomPadding = paddingVertical;
+ }
+ }
+
+ if (isRtlCompatibilityMode()) {
+ // RTL compatibility mode: pre Jelly Bean MR1 case OR no RTL support case.
+ // left / right padding are used if defined (meaning here nothing to do). If they are not
+ // defined and start / end padding are defined (e.g. in Frameworks resources), then we use
+ // start / end and resolve them as left / right (layout direction is not taken into account).
+ // Padding from the background drawable is stored at this point in mUserPaddingLeftInitial
+ // and mUserPaddingRightInitial) so drawable padding will be used as ultimate default if
+ // defined.
+ if (!mLeftPaddingDefined && startPaddingDefined) {
+ leftPadding = startPadding;
+ }
+ mUserPaddingLeftInitial = (leftPadding >= 0) ? leftPadding : mUserPaddingLeftInitial;
+ if (!mRightPaddingDefined && endPaddingDefined) {
+ rightPadding = endPadding;
+ }
+ mUserPaddingRightInitial = (rightPadding >= 0) ? rightPadding : mUserPaddingRightInitial;
+ } else {
+ // Jelly Bean MR1 and after case: if start/end defined, they will override any left/right
+ // values defined. Otherwise, left /right values are used.
+ // Padding from the background drawable is stored at this point in mUserPaddingLeftInitial
+ // and mUserPaddingRightInitial) so drawable padding will be used as ultimate default if
+ // defined.
+ final boolean hasRelativePadding = startPaddingDefined || endPaddingDefined;
+
+ if (mLeftPaddingDefined && !hasRelativePadding) {
+ mUserPaddingLeftInitial = leftPadding;
+ }
+ if (mRightPaddingDefined && !hasRelativePadding) {
+ mUserPaddingRightInitial = rightPadding;
+ }
+ }
+
+ // mPaddingTop and mPaddingBottom may have been set by setBackground(Drawable) so must pass
+ // them on if topPadding or bottomPadding are not valid.
+ internalSetPadding(
+ mUserPaddingLeftInitial,
+ topPadding >= 0 ? topPadding : mPaddingTop,
+ mUserPaddingRightInitial,
+ bottomPadding >= 0 ? bottomPadding : mPaddingBottom);
+
+ if (viewFlagMasks != 0) {
+ setFlags(viewFlagValues, viewFlagMasks);
+ }
+
+ if (initializeScrollbars) {
+ initializeScrollbarsInternal(a);
+ }
+
+ if (initializeScrollIndicators) {
+ initializeScrollIndicatorsInternal();
+ }
+
+ a.recycle();
+
+ // Needs to be called after mViewFlags is set
+ if (scrollbarStyle != SCROLLBARS_INSIDE_OVERLAY) {
+ recomputePadding();
+ }
+
+ if (x != 0 || y != 0) {
+ scrollTo(x, y);
+ }
+
+ if (transformSet) {
+ setTranslationX(tx);
+ setTranslationY(ty);
+ setTranslationZ(tz);
+ setElevation(elevation);
+ setRotation(rotation);
+ setRotationX(rotationX);
+ setRotationY(rotationY);
+ setScaleX(sx);
+ setScaleY(sy);
+ }
+
+ if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) {
+ setScrollContainer(true);
+ }
+
+ computeOpaqueFlags();
+ }
+
+ /**
+ * Returns the ordered list of resource ID that are considered when resolving attribute values
+ * for this {@link View}. The list will include layout resource ID if the View is inflated from
+ * XML. It will also include a set of explicit styles if specified in XML using
+ * {@code style="..."}. Finally, it will include the default styles resolved from the theme.
+ *
+ * <p>
+ * <b>Note:</b> this method will only return actual values if the view attribute debugging
+ * is enabled in Android developer options.
+ *
+ * @param attribute Attribute resource ID for which the resolution stack should be returned.
+ * @return ordered list of resource ID that are considered when resolving attribute values for
+ * this {@link View}.
+ */
+ @NonNull
+ public int[] getAttributeResolutionStack(@AttrRes int attribute) {
+ if (!sDebugViewAttributes
+ || mAttributeResolutionStacks == null
+ || mAttributeResolutionStacks.get(attribute) == null) {
+ return new int[0];
+ }
+ int[] attributeResolutionStack = mAttributeResolutionStacks.get(attribute);
+ int stackSize = attributeResolutionStack.length;
+ if (mSourceLayoutId != ID_NULL) {
+ stackSize++;
+ }
+
+ int currentIndex = 0;
+ int[] stack = new int[stackSize];
+
+ if (mSourceLayoutId != ID_NULL) {
+ stack[currentIndex] = mSourceLayoutId;
+ currentIndex++;
+ }
+ for (int i = 0; i < attributeResolutionStack.length; i++) {
+ stack[currentIndex] = attributeResolutionStack[i];
+ currentIndex++;
+ }
+ return stack;
+ }
+
+ /**
+ * Returns the mapping of attribute resource ID to source resource ID where the attribute value
+ * was set. Source resource ID can either be a layout resource ID, if the value was set in XML
+ * within the View tag, or a style resource ID, if the attribute was set in a style. The source
+ * resource value will be one of the resource IDs from {@link #getAttributeSourceResourceMap()}.
+ *
+ * <p>
+ * <b>Note:</b> this method will only return actual values if the view attribute debugging
+ * is enabled in Android developer options.
+ *
+ * @return mapping of attribute resource ID to source resource ID where the attribute value
+ * was set.
+ */
+ @NonNull
+ @SuppressWarnings("AndroidFrameworkEfficientCollections")
+ public Map<Integer, Integer> getAttributeSourceResourceMap() {
+ HashMap<Integer, Integer> map = new HashMap<>();
+ if (!sDebugViewAttributes || mAttributeSourceResId == null) {
+ return map;
+ }
+ for (int i = 0; i < mAttributeSourceResId.size(); i++) {
+ map.put(mAttributeSourceResId.keyAt(i), mAttributeSourceResId.valueAt(i));
+ }
+ return map;
+ }
+
+ /**
+ * Returns the resource ID for the style specified using {@code style="..."} in the
+ * {@link AttributeSet}'s backing XML element or {@link Resources#ID_NULL} otherwise if not
+ * specified or otherwise not applicable.
+ * <p>
+ * Each {@link View} can have an explicit style specified in the layout file.
+ * This style is used first during the {@link View} attribute resolution, then if an attribute
+ * is not defined there the resource system looks at default style and theme as fallbacks.
+ *
+ * <p>
+ * <b>Note:</b> this method will only return actual values if the view attribute debugging
+ * is enabled in Android developer options.
+ *
+ * @return The resource ID for the style specified using {@code style="..."} in the
+ * {@link AttributeSet}'s backing XML element or {@link Resources#ID_NULL} otherwise
+ * if not specified or otherwise not applicable.
+ */
+ @StyleRes
+ public int getExplicitStyle() {
+ if (!sDebugViewAttributes) {
+ return ID_NULL;
+ }
+ return mExplicitStyle;
+ }
+
+ /**
+ * An implementation of OnClickListener that attempts to lazily load a
+ * named click handling method from a parent or ancestor context.
+ */
+ private static class DeclaredOnClickListener implements OnClickListener {
+ private final View mHostView;
+ private final String mMethodName;
+
+ private Method mResolvedMethod;
+ private Context mResolvedContext;
+
+ public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
+ mHostView = hostView;
+ mMethodName = methodName;
+ }
+
+ @Override
+ public void onClick(@NonNull View v) {
+ if (mResolvedMethod == null) {
+ resolveMethod(mHostView.getContext(), mMethodName);
+ }
+
+ try {
+ mResolvedMethod.invoke(mResolvedContext, v);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(
+ "Could not execute non-public method for android:onClick", e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalStateException(
+ "Could not execute method for android:onClick", e);
+ }
+ }
+
+ @NonNull
+ private void resolveMethod(@Nullable Context context, @NonNull String name) {
+ while (context != null) {
+ try {
+ if (!context.isRestricted()) {
+ final Method method = context.getClass().getMethod(mMethodName, View.class);
+ if (method != null) {
+ mResolvedMethod = method;
+ mResolvedContext = context;
+ return;
+ }
+ }
+ } catch (NoSuchMethodException e) {
+ // Failed to find method, keep searching up the hierarchy.
+ }
+
+ if (context instanceof ContextWrapper) {
+ context = ((ContextWrapper) context).getBaseContext();
+ } else {
+ // Can't search up the hierarchy, null out and fail.
+ context = null;
+ }
+ }
+
+ final int id = mHostView.getId();
+ final String idText = id == NO_ID ? "" : " with id '"
+ + mHostView.getContext().getResources().getResourceEntryName(id) + "'";
+ throw new IllegalStateException("Could not find method " + mMethodName
+ + "(View) in a parent or ancestor Context for android:onClick "
+ + "attribute defined on view " + mHostView.getClass() + idText);
+ }
+ }
+
+ /**
+ * Non-public constructor for use in testing
+ */
+ @UnsupportedAppUsage
+ View() {
+ mResources = null;
+ mRenderNode = RenderNode.create(getClass().getName(), new ViewAnimationHostBridge(this));
+ }
+
+ /**
+ * Returns {@code true} when the View is attached and the system developer setting to show
+ * the layout bounds is enabled or {@code false} otherwise.
+ */
+ public final boolean isShowingLayoutBounds() {
+ return DEBUG_DRAW || mAttachInfo != null && mAttachInfo.mDebugLayout;
+ }
+
+ /**
+ * Used to test isShowingLayoutBounds(). This sets the local value used
+ * by that function. This method does nothing if the layout isn't attached.
+ *
+ * @hide
+ */
+ @TestApi
+ public final void setShowingLayoutBounds(boolean debugLayout) {
+ if (mAttachInfo != null) {
+ mAttachInfo.mDebugLayout = debugLayout;
+ }
+ }
+
+ private static SparseArray<String> getAttributeMap() {
+ if (mAttributeMap == null) {
+ mAttributeMap = new SparseArray<>();
+ }
+ return mAttributeMap;
+ }
+
+ private void retrieveExplicitStyle(@NonNull Resources.Theme theme,
+ @Nullable AttributeSet attrs) {
+ if (!sDebugViewAttributes) {
+ return;
+ }
+ mExplicitStyle = theme.getExplicitStyle(attrs);
+ }
+
+ /**
+ * Stores debugging information about attributes. This should be called in a constructor by
+ * every custom {@link View} that uses a custom styleable. If the custom view does not call it,
+ * then the custom attributes used by this view will not be visible in layout inspection tools.
+ *
+ * @param context Context under which this view is created.
+ * @param styleable A reference to styleable array R.styleable.Foo
+ * @param attrs AttributeSet used to construct this view.
+ * @param t Resolved {@link TypedArray} returned by a call to
+ * {@link Resources#obtainAttributes(AttributeSet, int[])}.
+ * @param defStyleAttr Default style attribute passed into the view constructor.
+ * @param defStyleRes Default style resource passed into the view constructor.
+ */
+ public final void saveAttributeDataForStyleable(@NonNull Context context,
+ @NonNull int[] styleable, @Nullable AttributeSet attrs, @NonNull TypedArray t,
+ int defStyleAttr, int defStyleRes) {
+ if (!sDebugViewAttributes) {
+ return;
+ }
+
+ int[] attributeResolutionStack = context.getTheme().getAttributeResolutionStack(
+ defStyleAttr, defStyleRes, mExplicitStyle);
+
+ if (mAttributeResolutionStacks == null) {
+ mAttributeResolutionStacks = new SparseArray<>();
+ }
+
+ if (mAttributeSourceResId == null) {
+ mAttributeSourceResId = new SparseIntArray();
+ }
+
+ final int indexCount = t.getIndexCount();
+ for (int j = 0; j < indexCount; ++j) {
+ final int index = t.getIndex(j);
+ mAttributeSourceResId.append(styleable[index], t.getSourceResourceId(index, 0));
+ mAttributeResolutionStacks.append(styleable[index], attributeResolutionStack);
+ }
+ }
+
+ private void saveAttributeData(@Nullable AttributeSet attrs, @NonNull TypedArray t) {
+ final int attrsCount = attrs == null ? 0 : attrs.getAttributeCount();
+ final int indexCount = t.getIndexCount();
+ final String[] attributes = new String[(attrsCount + indexCount) * 2];
+
+ int i = 0;
+
+ // Store raw XML attributes.
+ for (int j = 0; j < attrsCount; ++j) {
+ attributes[i] = attrs.getAttributeName(j);
+ attributes[i + 1] = attrs.getAttributeValue(j);
+ i += 2;
+ }
+
+ // Store resolved styleable attributes.
+ final Resources res = t.getResources();
+ final SparseArray<String> attributeMap = getAttributeMap();
+ for (int j = 0; j < indexCount; ++j) {
+ final int index = t.getIndex(j);
+ if (!t.hasValueOrEmpty(index)) {
+ // Value is undefined. Skip it.
+ continue;
+ }
+
+ final int resourceId = t.getResourceId(index, 0);
+ if (resourceId == 0) {
+ // Value is not a reference. Skip it.
+ continue;
+ }
+
+ String resourceName = attributeMap.get(resourceId);
+ if (resourceName == null) {
+ try {
+ resourceName = res.getResourceName(resourceId);
+ } catch (Resources.NotFoundException e) {
+ resourceName = "0x" + Integer.toHexString(resourceId);
+ }
+ attributeMap.put(resourceId, resourceName);
+ }
+
+ attributes[i] = resourceName;
+ attributes[i + 1] = t.getString(index);
+ i += 2;
+ }
+
+ // Trim to fit contents.
+ final String[] trimmed = new String[i];
+ System.arraycopy(attributes, 0, trimmed, 0, i);
+ mAttributes = trimmed;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder(128);
+ out.append(getClass().getName());
+ out.append('{');
+ out.append(Integer.toHexString(System.identityHashCode(this)));
+ out.append(' ');
+ switch (mViewFlags&VISIBILITY_MASK) {
+ case VISIBLE: out.append('V'); break;
+ case INVISIBLE: out.append('I'); break;
+ case GONE: out.append('G'); break;
+ default: out.append('.'); break;
+ }
+ out.append((mViewFlags & FOCUSABLE) == FOCUSABLE ? 'F' : '.');
+ out.append((mViewFlags&ENABLED_MASK) == ENABLED ? 'E' : '.');
+ out.append((mViewFlags&DRAW_MASK) == WILL_NOT_DRAW ? '.' : 'D');
+ out.append((mViewFlags&SCROLLBARS_HORIZONTAL) != 0 ? 'H' : '.');
+ out.append((mViewFlags&SCROLLBARS_VERTICAL) != 0 ? 'V' : '.');
+ out.append((mViewFlags&CLICKABLE) != 0 ? 'C' : '.');
+ out.append((mViewFlags&LONG_CLICKABLE) != 0 ? 'L' : '.');
+ out.append((mViewFlags&CONTEXT_CLICKABLE) != 0 ? 'X' : '.');
+ out.append(' ');
+ out.append((mPrivateFlags&PFLAG_IS_ROOT_NAMESPACE) != 0 ? 'R' : '.');
+ out.append((mPrivateFlags&PFLAG_FOCUSED) != 0 ? 'F' : '.');
+ out.append((mPrivateFlags&PFLAG_SELECTED) != 0 ? 'S' : '.');
+ if ((mPrivateFlags&PFLAG_PREPRESSED) != 0) {
+ out.append('p');
+ } else {
+ out.append((mPrivateFlags&PFLAG_PRESSED) != 0 ? 'P' : '.');
+ }
+ out.append((mPrivateFlags&PFLAG_HOVERED) != 0 ? 'H' : '.');
+ out.append((mPrivateFlags&PFLAG_ACTIVATED) != 0 ? 'A' : '.');
+ out.append((mPrivateFlags&PFLAG_INVALIDATED) != 0 ? 'I' : '.');
+ out.append((mPrivateFlags&PFLAG_DIRTY_MASK) != 0 ? 'D' : '.');
+ out.append(' ');
+ out.append(mLeft);
+ out.append(',');
+ out.append(mTop);
+ out.append('-');
+ out.append(mRight);
+ out.append(',');
+ out.append(mBottom);
+ final int id = getId();
+ if (id != NO_ID) {
+ out.append(" #");
+ out.append(Integer.toHexString(id));
+ final Resources r = mResources;
+ if (id > 0 && Resources.resourceHasPackage(id) && r != null) {
+ try {
+ String pkgname;
+ switch (id&0xff000000) {
+ case 0x7f000000:
+ pkgname="app";
+ break;
+ case 0x01000000:
+ pkgname="android";
+ break;
+ default:
+ pkgname = r.getResourcePackageName(id);
+ break;
+ }
+ String typename = r.getResourceTypeName(id);
+ String entryname = r.getResourceEntryName(id);
+ out.append(" ");
+ out.append(pkgname);
+ out.append(":");
+ out.append(typename);
+ out.append("/");
+ out.append(entryname);
+ } catch (Resources.NotFoundException e) {
+ }
+ }
+ }
+ if (mAutofillId != null) {
+ out.append(" aid="); out.append(mAutofillId);
+ }
+ out.append("}");
+ return out.toString();
+ }
+
+ /**
+ * <p>
+ * Initializes the fading edges from a given set of styled attributes. This
+ * method should be called by subclasses that need fading edges and when an
+ * instance of these subclasses is created programmatically rather than
+ * being inflated from XML. This method is automatically called when the XML
+ * is inflated.
+ * </p>
+ *
+ * @param a the styled attributes set to initialize the fading edges from
+ *
+ * @removed
+ */
+ protected void initializeFadingEdge(TypedArray a) {
+ // This method probably shouldn't have been included in the SDK to begin with.
+ // It relies on 'a' having been initialized using an attribute filter array that is
+ // not publicly available to the SDK. The old method has been renamed
+ // to initializeFadingEdgeInternal and hidden for framework use only;
+ // this one initializes using defaults to make it safe to call for apps.
+
+ TypedArray arr = mContext.obtainStyledAttributes(com.android.internal.R.styleable.View);
+
+ initializeFadingEdgeInternal(arr);
+
+ arr.recycle();
+ }
+
+ /**
+ * <p>
+ * Initializes the fading edges from a given set of styled attributes. This
+ * method should be called by subclasses that need fading edges and when an
+ * instance of these subclasses is created programmatically rather than
+ * being inflated from XML. This method is automatically called when the XML
+ * is inflated.
+ * </p>
+ *
+ * @param a the styled attributes set to initialize the fading edges from
+ * @hide This is the real method; the public one is shimmed to be safe to call from apps.
+ */
+ protected void initializeFadingEdgeInternal(TypedArray a) {
+ initScrollCache();
+
+ mScrollCache.fadingEdgeLength = a.getDimensionPixelSize(
+ R.styleable.View_fadingEdgeLength,
+ ViewConfiguration.get(mContext).getScaledFadingEdgeLength());
+ }
+
+ /**
+ * Returns the size of the vertical faded edges used to indicate that more
+ * content in this view is visible.
+ *
+ * @return The size in pixels of the vertical faded edge or 0 if vertical
+ * faded edges are not enabled for this view.
+ * @attr ref android.R.styleable#View_fadingEdgeLength
+ */
+ public int getVerticalFadingEdgeLength() {
+ if (isVerticalFadingEdgeEnabled()) {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ return cache.fadingEdgeLength;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Set the size of the faded edge used to indicate that more content in this
+ * view is available. Will not change whether the fading edge is enabled; use
+ * {@link #setVerticalFadingEdgeEnabled(boolean)} or
+ * {@link #setHorizontalFadingEdgeEnabled(boolean)} to enable the fading edge
+ * for the vertical or horizontal fading edges.
+ *
+ * @param length The size in pixels of the faded edge used to indicate that more
+ * content in this view is visible.
+ */
+ public void setFadingEdgeLength(int length) {
+ initScrollCache();
+ mScrollCache.fadingEdgeLength = length;
+ }
+
+ /**
+ * Returns the size of the horizontal faded edges used to indicate that more
+ * content in this view is visible.
+ *
+ * @return The size in pixels of the horizontal faded edge or 0 if horizontal
+ * faded edges are not enabled for this view.
+ * @attr ref android.R.styleable#View_fadingEdgeLength
+ */
+ public int getHorizontalFadingEdgeLength() {
+ if (isHorizontalFadingEdgeEnabled()) {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ return cache.fadingEdgeLength;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the width of the vertical scrollbar.
+ *
+ * @return The width in pixels of the vertical scrollbar or 0 if there
+ * is no vertical scrollbar.
+ */
+ public int getVerticalScrollbarWidth() {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ ScrollBarDrawable scrollBar = cache.scrollBar;
+ if (scrollBar != null) {
+ int size = scrollBar.getSize(true);
+ if (size <= 0) {
+ size = cache.scrollBarSize;
+ }
+ return size;
+ }
+ return 0;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the height of the horizontal scrollbar.
+ *
+ * @return The height in pixels of the horizontal scrollbar or 0 if
+ * there is no horizontal scrollbar.
+ */
+ protected int getHorizontalScrollbarHeight() {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ ScrollBarDrawable scrollBar = cache.scrollBar;
+ if (scrollBar != null) {
+ int size = scrollBar.getSize(false);
+ if (size <= 0) {
+ size = cache.scrollBarSize;
+ }
+ return size;
+ }
+ return 0;
+ }
+ return 0;
+ }
+
+ /**
+ * <p>
+ * Initializes the scrollbars from a given set of styled attributes. This
+ * method should be called by subclasses that need scrollbars and when an
+ * instance of these subclasses is created programmatically rather than
+ * being inflated from XML. This method is automatically called when the XML
+ * is inflated.
+ * </p>
+ *
+ * @param a the styled attributes set to initialize the scrollbars from
+ *
+ * @removed
+ */
+ protected void initializeScrollbars(TypedArray a) {
+ // It's not safe to use this method from apps. The parameter 'a' must have been obtained
+ // using the View filter array which is not available to the SDK. As such, internal
+ // framework usage now uses initializeScrollbarsInternal and we grab a default
+ // TypedArray with the right filter instead here.
+ TypedArray arr = mContext.obtainStyledAttributes(com.android.internal.R.styleable.View);
+
+ initializeScrollbarsInternal(arr);
+
+ // We ignored the method parameter. Recycle the one we actually did use.
+ arr.recycle();
+ }
+
+ private void initializeScrollBarDrawable() {
+ initScrollCache();
+
+ if (mScrollCache.scrollBar == null) {
+ mScrollCache.scrollBar = new ScrollBarDrawable();
+ mScrollCache.scrollBar.setState(getDrawableState());
+ mScrollCache.scrollBar.setCallback(this);
+ }
+ }
+
+ /**
+ * <p>
+ * Initializes the scrollbars from a given set of styled attributes. This
+ * method should be called by subclasses that need scrollbars and when an
+ * instance of these subclasses is created programmatically rather than
+ * being inflated from XML. This method is automatically called when the XML
+ * is inflated.
+ * </p>
+ *
+ * @param a the styled attributes set to initialize the scrollbars from
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void initializeScrollbarsInternal(TypedArray a) {
+ initScrollCache();
+
+ final ScrollabilityCache scrollabilityCache = mScrollCache;
+
+ if (scrollabilityCache.scrollBar == null) {
+ scrollabilityCache.scrollBar = new ScrollBarDrawable();
+ scrollabilityCache.scrollBar.setState(getDrawableState());
+ scrollabilityCache.scrollBar.setCallback(this);
+ }
+
+ final boolean fadeScrollbars = a.getBoolean(R.styleable.View_fadeScrollbars, true);
+
+ if (!fadeScrollbars) {
+ scrollabilityCache.state = ScrollabilityCache.ON;
+ }
+ scrollabilityCache.fadeScrollBars = fadeScrollbars;
+
+
+ scrollabilityCache.scrollBarFadeDuration = a.getInt(
+ R.styleable.View_scrollbarFadeDuration, ViewConfiguration
+ .getScrollBarFadeDuration());
+ scrollabilityCache.scrollBarDefaultDelayBeforeFade = a.getInt(
+ R.styleable.View_scrollbarDefaultDelayBeforeFade,
+ ViewConfiguration.getScrollDefaultDelay());
+
+
+ scrollabilityCache.scrollBarSize = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.View_scrollbarSize,
+ ViewConfiguration.get(mContext).getScaledScrollBarSize());
+
+ Drawable track = a.getDrawable(R.styleable.View_scrollbarTrackHorizontal);
+ scrollabilityCache.scrollBar.setHorizontalTrackDrawable(track);
+
+ Drawable thumb = a.getDrawable(R.styleable.View_scrollbarThumbHorizontal);
+ if (thumb != null) {
+ scrollabilityCache.scrollBar.setHorizontalThumbDrawable(thumb);
+ }
+
+ boolean alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawHorizontalTrack,
+ false);
+ if (alwaysDraw) {
+ scrollabilityCache.scrollBar.setAlwaysDrawHorizontalTrack(true);
+ }
+
+ track = a.getDrawable(R.styleable.View_scrollbarTrackVertical);
+ scrollabilityCache.scrollBar.setVerticalTrackDrawable(track);
+
+ thumb = a.getDrawable(R.styleable.View_scrollbarThumbVertical);
+ if (thumb != null) {
+ scrollabilityCache.scrollBar.setVerticalThumbDrawable(thumb);
+ }
+
+ alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawVerticalTrack,
+ false);
+ if (alwaysDraw) {
+ scrollabilityCache.scrollBar.setAlwaysDrawVerticalTrack(true);
+ }
+
+ // Apply layout direction to the new Drawables if needed
+ final int layoutDirection = getLayoutDirection();
+ if (track != null) {
+ track.setLayoutDirection(layoutDirection);
+ }
+ if (thumb != null) {
+ thumb.setLayoutDirection(layoutDirection);
+ }
+
+ // Re-apply user/background padding so that scrollbar(s) get added
+ resolvePadding();
+ }
+
+ /**
+ * Defines the vertical scrollbar thumb drawable
+ * @attr ref android.R.styleable#View_scrollbarThumbVertical
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ public void setVerticalScrollbarThumbDrawable(@Nullable Drawable drawable) {
+ initializeScrollBarDrawable();
+ mScrollCache.scrollBar.setVerticalThumbDrawable(drawable);
+ }
+
+ /**
+ * Defines the vertical scrollbar track drawable
+ * @attr ref android.R.styleable#View_scrollbarTrackVertical
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ public void setVerticalScrollbarTrackDrawable(@Nullable Drawable drawable) {
+ initializeScrollBarDrawable();
+ mScrollCache.scrollBar.setVerticalTrackDrawable(drawable);
+ }
+
+ /**
+ * Defines the horizontal thumb drawable
+ * @attr ref android.R.styleable#View_scrollbarThumbHorizontal
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ */
+ public void setHorizontalScrollbarThumbDrawable(@Nullable Drawable drawable) {
+ initializeScrollBarDrawable();
+ mScrollCache.scrollBar.setHorizontalThumbDrawable(drawable);
+ }
+
+ /**
+ * Defines the horizontal track drawable
+ * @attr ref android.R.styleable#View_scrollbarTrackHorizontal
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ */
+ public void setHorizontalScrollbarTrackDrawable(@Nullable Drawable drawable) {
+ initializeScrollBarDrawable();
+ mScrollCache.scrollBar.setHorizontalTrackDrawable(drawable);
+ }
+
+ /**
+ * Returns the currently configured Drawable for the thumb of the vertical scroll bar if it
+ * exists, null otherwise.
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ public @Nullable Drawable getVerticalScrollbarThumbDrawable() {
+ return mScrollCache != null ? mScrollCache.scrollBar.getVerticalThumbDrawable() : null;
+ }
+
+ /**
+ * Returns the currently configured Drawable for the track of the vertical scroll bar if it
+ * exists, null otherwise.
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ public @Nullable Drawable getVerticalScrollbarTrackDrawable() {
+ return mScrollCache != null ? mScrollCache.scrollBar.getVerticalTrackDrawable() : null;
+ }
+
+ /**
+ * Returns the currently configured Drawable for the thumb of the horizontal scroll bar if it
+ * exists, null otherwise.
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ */
+ public @Nullable Drawable getHorizontalScrollbarThumbDrawable() {
+ return mScrollCache != null ? mScrollCache.scrollBar.getHorizontalThumbDrawable() : null;
+ }
+
+ /**
+ * Returns the currently configured Drawable for the track of the horizontal scroll bar if it
+ * exists, null otherwise.
+ *
+ * @see #awakenScrollBars(int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ */
+ public @Nullable Drawable getHorizontalScrollbarTrackDrawable() {
+ return mScrollCache != null ? mScrollCache.scrollBar.getHorizontalTrackDrawable() : null;
+ }
+
+ private void initializeScrollIndicatorsInternal() {
+ // Some day maybe we'll break this into top/left/start/etc. and let the
+ // client control it. Until then, you can have any scroll indicator you
+ // want as long as it's a 1dp foreground-colored rectangle.
+ if (mScrollIndicatorDrawable == null) {
+ mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
+ }
+ }
+
+ /**
+ * <p>
+ * Initalizes the scrollability cache if necessary.
+ * </p>
+ */
+ private void initScrollCache() {
+ if (mScrollCache == null) {
+ mScrollCache = new ScrollabilityCache(ViewConfiguration.get(mContext), this);
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private ScrollabilityCache getScrollCache() {
+ initScrollCache();
+ return mScrollCache;
+ }
+
+ /**
+ * Set the position of the vertical scroll bar. Should be one of
+ * {@link #SCROLLBAR_POSITION_DEFAULT}, {@link #SCROLLBAR_POSITION_LEFT} or
+ * {@link #SCROLLBAR_POSITION_RIGHT}.
+ *
+ * @param position Where the vertical scroll bar should be positioned.
+ */
+ public void setVerticalScrollbarPosition(int position) {
+ if (mVerticalScrollbarPosition != position) {
+ mVerticalScrollbarPosition = position;
+ computeOpaqueFlags();
+ resolvePadding();
+ }
+ }
+
+ /**
+ * @return The position where the vertical scroll bar will show, if applicable.
+ * @see #setVerticalScrollbarPosition(int)
+ */
+ public int getVerticalScrollbarPosition() {
+ return mVerticalScrollbarPosition;
+ }
+
+ boolean isOnScrollbar(float x, float y) {
+ if (mScrollCache == null) {
+ return false;
+ }
+ x += getScrollX();
+ y += getScrollY();
+ final boolean canScrollVertically =
+ computeVerticalScrollRange() > computeVerticalScrollExtent();
+ if (isVerticalScrollBarEnabled() && !isVerticalScrollBarHidden() && canScrollVertically) {
+ final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
+ getVerticalScrollBarBounds(null, touchBounds);
+ if (touchBounds.contains((int) x, (int) y)) {
+ return true;
+ }
+ }
+ final boolean canScrollHorizontally =
+ computeHorizontalScrollRange() > computeHorizontalScrollExtent();
+ if (isHorizontalScrollBarEnabled() && canScrollHorizontally) {
+ final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
+ getHorizontalScrollBarBounds(null, touchBounds);
+ if (touchBounds.contains((int) x, (int) y)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @UnsupportedAppUsage
+ boolean isOnScrollbarThumb(float x, float y) {
+ return isOnVerticalScrollbarThumb(x, y) || isOnHorizontalScrollbarThumb(x, y);
+ }
+
+ private boolean isOnVerticalScrollbarThumb(float x, float y) {
+ if (mScrollCache == null || !isVerticalScrollBarEnabled() || isVerticalScrollBarHidden()) {
+ return false;
+ }
+ final int range = computeVerticalScrollRange();
+ final int extent = computeVerticalScrollExtent();
+ if (range > extent) {
+ x += getScrollX();
+ y += getScrollY();
+ final Rect bounds = mScrollCache.mScrollBarBounds;
+ final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
+ getVerticalScrollBarBounds(bounds, touchBounds);
+ final int offset = computeVerticalScrollOffset();
+ final int thumbLength = ScrollBarUtils.getThumbLength(bounds.height(), bounds.width(),
+ extent, range);
+ final int thumbOffset = ScrollBarUtils.getThumbOffset(bounds.height(), thumbLength,
+ extent, range, offset);
+ final int thumbTop = bounds.top + thumbOffset;
+ final int adjust = Math.max(mScrollCache.scrollBarMinTouchTarget - thumbLength, 0) / 2;
+ if (x >= touchBounds.left && x <= touchBounds.right
+ && y >= thumbTop - adjust && y <= thumbTop + thumbLength + adjust) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isOnHorizontalScrollbarThumb(float x, float y) {
+ if (mScrollCache == null || !isHorizontalScrollBarEnabled()) {
+ return false;
+ }
+ final int range = computeHorizontalScrollRange();
+ final int extent = computeHorizontalScrollExtent();
+ if (range > extent) {
+ x += getScrollX();
+ y += getScrollY();
+ final Rect bounds = mScrollCache.mScrollBarBounds;
+ final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
+ getHorizontalScrollBarBounds(bounds, touchBounds);
+ final int offset = computeHorizontalScrollOffset();
+
+ final int thumbLength = ScrollBarUtils.getThumbLength(bounds.width(), bounds.height(),
+ extent, range);
+ final int thumbOffset = ScrollBarUtils.getThumbOffset(bounds.width(), thumbLength,
+ extent, range, offset);
+ final int thumbLeft = bounds.left + thumbOffset;
+ final int adjust = Math.max(mScrollCache.scrollBarMinTouchTarget - thumbLength, 0) / 2;
+ if (x >= thumbLeft - adjust && x <= thumbLeft + thumbLength + adjust
+ && y >= touchBounds.top && y <= touchBounds.bottom) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @UnsupportedAppUsage
+ boolean isDraggingScrollBar() {
+ return mScrollCache != null
+ && mScrollCache.mScrollBarDraggingState != ScrollabilityCache.NOT_DRAGGING;
+ }
+
+ /**
+ * Sets the state of all scroll indicators.
+ * <p>
+ * See {@link #setScrollIndicators(int, int)} for usage information.
+ *
+ * @param indicators a bitmask of indicators that should be enabled, or
+ * {@code 0} to disable all indicators
+ * @see #setScrollIndicators(int, int)
+ * @see #getScrollIndicators()
+ * @attr ref android.R.styleable#View_scrollIndicators
+ */
+ @RemotableViewMethod
+ public void setScrollIndicators(@ScrollIndicators int indicators) {
+ setScrollIndicators(indicators,
+ SCROLL_INDICATORS_PFLAG3_MASK >>> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT);
+ }
+
+ /**
+ * Sets the state of the scroll indicators specified by the mask. To change
+ * all scroll indicators at once, see {@link #setScrollIndicators(int)}.
+ * <p>
+ * When a scroll indicator is enabled, it will be displayed if the view
+ * can scroll in the direction of the indicator.
+ * <p>
+ * Multiple indicator types may be enabled or disabled by passing the
+ * logical OR of the desired types. If multiple types are specified, they
+ * will all be set to the same enabled state.
+ * <p>
+ * For example, to enable the top scroll indicatorExample: {@code setScrollIndicators
+ *
+ * @param indicators the indicator direction, or the logical OR of multiple
+ * indicator directions. One or more of:
+ * <ul>
+ * <li>{@link #SCROLL_INDICATOR_TOP}</li>
+ * <li>{@link #SCROLL_INDICATOR_BOTTOM}</li>
+ * <li>{@link #SCROLL_INDICATOR_LEFT}</li>
+ * <li>{@link #SCROLL_INDICATOR_RIGHT}</li>
+ * <li>{@link #SCROLL_INDICATOR_START}</li>
+ * <li>{@link #SCROLL_INDICATOR_END}</li>
+ * </ul>
+ * @see #setScrollIndicators(int)
+ * @see #getScrollIndicators()
+ * @attr ref android.R.styleable#View_scrollIndicators
+ */
+ public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask) {
+ // Shift and sanitize mask.
+ mask <<= SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+ mask &= SCROLL_INDICATORS_PFLAG3_MASK;
+
+ // Shift and mask indicators.
+ indicators <<= SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+ indicators &= mask;
+
+ // Merge with non-masked flags.
+ final int updatedFlags = indicators | (mPrivateFlags3 & ~mask);
+
+ if (mPrivateFlags3 != updatedFlags) {
+ mPrivateFlags3 = updatedFlags;
+
+ if (indicators != 0) {
+ initializeScrollIndicatorsInternal();
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns a bitmask representing the enabled scroll indicators.
+ * <p>
+ * For example, if the top and left scroll indicators are enabled and all
+ * other indicators are disabled, the return value will be
+ * {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
+ * <p>
+ * To check whether the bottom scroll indicator is enabled, use the value
+ * of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
+ *
+ * @return a bitmask representing the enabled scroll indicators
+ */
+ @InspectableProperty(flagMapping = {
+ @FlagEntry(target = SCROLL_INDICATORS_NONE, mask = 0xffff_ffff, name = "none"),
+ @FlagEntry(target = SCROLL_INDICATOR_TOP, name = "top"),
+ @FlagEntry(target = SCROLL_INDICATOR_BOTTOM, name = "bottom"),
+ @FlagEntry(target = SCROLL_INDICATOR_LEFT, name = "left"),
+ @FlagEntry(target = SCROLL_INDICATOR_RIGHT, name = "right"),
+ @FlagEntry(target = SCROLL_INDICATOR_START, name = "start"),
+ @FlagEntry(target = SCROLL_INDICATOR_END, name = "end")
+ })
+ @ScrollIndicators
+ public int getScrollIndicators() {
+ return (mPrivateFlags3 & SCROLL_INDICATORS_PFLAG3_MASK)
+ >>> SCROLL_INDICATORS_TO_PFLAGS3_LSHIFT;
+ }
+
+ @UnsupportedAppUsage
+ ListenerInfo getListenerInfo() {
+ if (mListenerInfo != null) {
+ return mListenerInfo;
+ }
+ mListenerInfo = new ListenerInfo();
+ return mListenerInfo;
+ }
+
+ /**
+ * Register a callback to be invoked when the scroll X or Y positions of
+ * this view change.
+ * <p>
+ * <b>Note:</b> Some views handle scrolling independently from View and may
+ * have their own separate listeners for scroll-type events. For example,
+ * {@link android.widget.ListView ListView} allows clients to register an
+ * {@link android.widget.ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener) AbsListView.OnScrollListener}
+ * to listen for changes in list scroll position.
+ *
+ * @param l The listener to notify when the scroll X or Y position changes.
+ * @see android.view.View#getScrollX()
+ * @see android.view.View#getScrollY()
+ */
+ public void setOnScrollChangeListener(OnScrollChangeListener l) {
+ getListenerInfo().mOnScrollChangeListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when focus of this view changed.
+ *
+ * @param l The callback that will run.
+ */
+ public void setOnFocusChangeListener(OnFocusChangeListener l) {
+ getListenerInfo().mOnFocusChangeListener = l;
+ }
+
+ /**
+ * Add a listener that will be called when the bounds of the view change due to
+ * layout processing.
+ *
+ * @param listener The listener that will be called when layout bounds change.
+ */
+ public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
+ ListenerInfo li = getListenerInfo();
+ if (li.mOnLayoutChangeListeners == null) {
+ li.mOnLayoutChangeListeners = new ArrayList<OnLayoutChangeListener>();
+ }
+ if (!li.mOnLayoutChangeListeners.contains(listener)) {
+ li.mOnLayoutChangeListeners.add(listener);
+ }
+ }
+
+ /**
+ * Remove a listener for layout changes.
+ *
+ * @param listener The listener for layout bounds change.
+ */
+ public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
+ ListenerInfo li = mListenerInfo;
+ if (li == null || li.mOnLayoutChangeListeners == null) {
+ return;
+ }
+ li.mOnLayoutChangeListeners.remove(listener);
+ }
+
+ /**
+ * Add a listener for attach state changes.
+ *
+ * This listener will be called whenever this view is attached or detached
+ * from a window. Remove the listener using
+ * {@link #removeOnAttachStateChangeListener(OnAttachStateChangeListener)}.
+ *
+ * @param listener Listener to attach
+ * @see #removeOnAttachStateChangeListener(OnAttachStateChangeListener)
+ */
+ public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
+ ListenerInfo li = getListenerInfo();
+ if (li.mOnAttachStateChangeListeners == null) {
+ li.mOnAttachStateChangeListeners
+ = new CopyOnWriteArrayList<OnAttachStateChangeListener>();
+ }
+ li.mOnAttachStateChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener for attach state changes. The listener will receive no further
+ * notification of window attach/detach events.
+ *
+ * @param listener Listener to remove
+ * @see #addOnAttachStateChangeListener(OnAttachStateChangeListener)
+ */
+ public void removeOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
+ ListenerInfo li = mListenerInfo;
+ if (li == null || li.mOnAttachStateChangeListeners == null) {
+ return;
+ }
+ li.mOnAttachStateChangeListeners.remove(listener);
+ }
+
+ /**
+ * Returns the focus-change callback registered for this view.
+ *
+ * @return The callback, or null if one is not registered.
+ */
+ public OnFocusChangeListener getOnFocusChangeListener() {
+ ListenerInfo li = mListenerInfo;
+ return li != null ? li.mOnFocusChangeListener : null;
+ }
+
+ /**
+ * Register a callback to be invoked when this view is clicked. If this view is not
+ * clickable, it becomes clickable.
+ *
+ * @param l The callback that will run
+ *
+ * @see #setClickable(boolean)
+ */
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ if (!isClickable()) {
+ setClickable(true);
+ }
+ getListenerInfo().mOnClickListener = l;
+ }
+
+ /**
+ * Return whether this view has an attached OnClickListener. Returns
+ * true if there is a listener, false if there is none.
+ */
+ public boolean hasOnClickListeners() {
+ ListenerInfo li = mListenerInfo;
+ return (li != null && li.mOnClickListener != null);
+ }
+
+ /**
+ * Register a callback to be invoked when this view is clicked and held. If this view is not
+ * long clickable, it becomes long clickable.
+ *
+ * @param l The callback that will run
+ *
+ * @see #setLongClickable(boolean)
+ */
+ public void setOnLongClickListener(@Nullable OnLongClickListener l) {
+ if (!isLongClickable()) {
+ setLongClickable(true);
+ }
+ getListenerInfo().mOnLongClickListener = l;
+ }
+
+ /**
+ * Return whether this view has an attached OnLongClickListener. Returns
+ * true if there is a listener, false if there is none.
+ */
+ public boolean hasOnLongClickListeners() {
+ ListenerInfo li = mListenerInfo;
+ return (li != null && li.mOnLongClickListener != null);
+ }
+
+ /**
+ * @return the registered {@link OnLongClickListener} if there is one, {@code null} otherwise.
+ * @hide
+ */
+ @Nullable
+ public OnLongClickListener getOnLongClickListener() {
+ ListenerInfo li = mListenerInfo;
+ return (li != null) ? li.mOnLongClickListener : null;
+ }
+
+ /**
+ * Register a callback to be invoked when this view is context clicked. If the view is not
+ * context clickable, it becomes context clickable.
+ *
+ * @param l The callback that will run
+ * @see #setContextClickable(boolean)
+ */
+ public void setOnContextClickListener(@Nullable OnContextClickListener l) {
+ if (!isContextClickable()) {
+ setContextClickable(true);
+ }
+ getListenerInfo().mOnContextClickListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when the context menu for this view is
+ * being built. If this view is not long clickable, it becomes long clickable.
+ *
+ * @param l The callback that will run
+ *
+ */
+ public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
+ if (!isLongClickable()) {
+ setLongClickable(true);
+ }
+ getListenerInfo().mOnCreateContextMenuListener = l;
+ }
+
+ /**
+ * Set an observer to collect stats for each frame rendered for this view.
+ *
+ * @hide
+ */
+ public void addFrameMetricsListener(Window window,
+ Window.OnFrameMetricsAvailableListener listener,
+ Handler handler) {
+ if (mAttachInfo != null) {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ if (mFrameMetricsObservers == null) {
+ mFrameMetricsObservers = new ArrayList<>();
+ }
+
+ FrameMetricsObserver fmo = new FrameMetricsObserver(window, handler, listener);
+ mFrameMetricsObservers.add(fmo);
+ mAttachInfo.mThreadedRenderer.addObserver(fmo.getRendererObserver());
+ } else {
+ Log.w(VIEW_LOG_TAG, "View not hardware-accelerated. Unable to observe frame stats");
+ }
+ } else {
+ if (mFrameMetricsObservers == null) {
+ mFrameMetricsObservers = new ArrayList<>();
+ }
+
+ FrameMetricsObserver fmo = new FrameMetricsObserver(window, handler, listener);
+ mFrameMetricsObservers.add(fmo);
+ }
+ }
+
+ /**
+ * Remove observer configured to collect frame stats for this view.
+ *
+ * @hide
+ */
+ public void removeFrameMetricsListener(
+ Window.OnFrameMetricsAvailableListener listener) {
+ ThreadedRenderer renderer = getThreadedRenderer();
+ FrameMetricsObserver fmo = findFrameMetricsObserver(listener);
+ if (fmo == null) {
+ throw new IllegalArgumentException(
+ "attempt to remove OnFrameMetricsAvailableListener that was never added");
+ }
+
+ if (mFrameMetricsObservers != null) {
+ mFrameMetricsObservers.remove(fmo);
+ if (renderer != null) {
+ renderer.removeObserver(fmo.getRendererObserver());
+ }
+ }
+ }
+
+ private void registerPendingFrameMetricsObservers() {
+ if (mFrameMetricsObservers != null) {
+ ThreadedRenderer renderer = getThreadedRenderer();
+ if (renderer != null) {
+ for (FrameMetricsObserver fmo : mFrameMetricsObservers) {
+ renderer.addObserver(fmo.getRendererObserver());
+ }
+ } else {
+ Log.w(VIEW_LOG_TAG, "View not hardware-accelerated. Unable to observe frame stats");
+ }
+ }
+ }
+
+ private FrameMetricsObserver findFrameMetricsObserver(
+ Window.OnFrameMetricsAvailableListener listener) {
+ if (mFrameMetricsObservers != null) {
+ for (int i = 0; i < mFrameMetricsObservers.size(); i++) {
+ FrameMetricsObserver observer = mFrameMetricsObservers.get(i);
+ if (observer.mListener == listener) {
+ return observer;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** @hide */
+ public void setNotifyAutofillManagerOnClick(boolean notify) {
+ if (notify) {
+ mPrivateFlags |= PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK;
+ } else {
+ mPrivateFlags &= ~PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK;
+ }
+ }
+
+ private void notifyAutofillManagerOnClick() {
+ if ((mPrivateFlags & PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK) != 0) {
+ try {
+ getAutofillManager().notifyViewClicked(this);
+ } finally {
+ // Set it to already called so it's not called twice when called by
+ // performClickInternal()
+ mPrivateFlags &= ~PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK;
+ }
+ }
+ }
+
+ /**
+ * Entry point for {@link #performClick()} - other methods on View should call it instead of
+ * {@code performClick()} directly to make sure the autofill manager is notified when
+ * necessary (as subclasses could extend {@code performClick()} without calling the parent's
+ * method).
+ */
+ private boolean performClickInternal() {
+ // Must notify autofill manager before performing the click actions to avoid scenarios where
+ // the app has a click listener that changes the state of views the autofill service might
+ // be interested on.
+ notifyAutofillManagerOnClick();
+
+ return performClick();
+ }
+
+ /**
+ * Call this view's OnClickListener, if it is defined. Performs all normal
+ * actions associated with clicking: reporting accessibility event, playing
+ * a sound, etc.
+ *
+ * @return True there was an assigned OnClickListener that was called, false
+ * otherwise is returned.
+ */
+ // NOTE: other methods on View should not call this method directly, but performClickInternal()
+ // instead, to guarantee that the autofill manager is notified when necessary (as subclasses
+ // could extend this method without calling super.performClick()).
+ public boolean performClick() {
+ // We still need to call this method to handle the cases where performClick() was called
+ // externally, instead of through performClickInternal()
+ notifyAutofillManagerOnClick();
+
+ final boolean result;
+ final ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnClickListener != null) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ li.mOnClickListener.onClick(this);
+ result = true;
+ } else {
+ result = false;
+ }
+
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+ notifyEnterOrExitForAutoFillIfNeeded(true);
+
+ return result;
+ }
+
+ /**
+ * Directly call any attached OnClickListener. Unlike {@link #performClick()},
+ * this only calls the listener, and does not do any associated clicking
+ * actions like reporting an accessibility event.
+ *
+ * @return True there was an assigned OnClickListener that was called, false
+ * otherwise is returned.
+ */
+ public boolean callOnClick() {
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnClickListener != null) {
+ li.mOnClickListener.onClick(this);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Calls this view's OnLongClickListener, if it is defined. Invokes the
+ * context menu if the OnLongClickListener did not consume the event.
+ *
+ * @return {@code true} if one of the above receivers consumed the event,
+ * {@code false} otherwise
+ */
+ public boolean performLongClick() {
+ return performLongClickInternal(mLongClickX, mLongClickY);
+ }
+
+ /**
+ * Calls this view's OnLongClickListener, if it is defined. Invokes the
+ * context menu if the OnLongClickListener did not consume the event,
+ * anchoring it to an (x,y) coordinate.
+ *
+ * @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
+ * to disable anchoring
+ * @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
+ * to disable anchoring
+ * @return {@code true} if one of the above receivers consumed the event,
+ * {@code false} otherwise
+ */
+ public boolean performLongClick(float x, float y) {
+ mLongClickX = x;
+ mLongClickY = y;
+ final boolean handled = performLongClick();
+ mLongClickX = Float.NaN;
+ mLongClickY = Float.NaN;
+ return handled;
+ }
+
+ /**
+ * Calls this view's OnLongClickListener, if it is defined. Invokes the
+ * context menu if the OnLongClickListener did not consume the event,
+ * optionally anchoring it to an (x,y) coordinate.
+ *
+ * @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
+ * to disable anchoring
+ * @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
+ * to disable anchoring
+ * @return {@code true} if one of the above receivers consumed the event,
+ * {@code false} otherwise
+ */
+ private boolean performLongClickInternal(float x, float y) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+
+ boolean handled = false;
+ final ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnLongClickListener != null) {
+ handled = li.mOnLongClickListener.onLongClick(View.this);
+ }
+ if (!handled) {
+ final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
+ handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
+ }
+ if ((mViewFlags & TOOLTIP) == TOOLTIP) {
+ if (!handled) {
+ handled = showLongClickTooltip((int) x, (int) y);
+ }
+ }
+ if (handled) {
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ return handled;
+ }
+
+ /**
+ * Call this view's OnContextClickListener, if it is defined.
+ *
+ * @param x the x coordinate of the context click
+ * @param y the y coordinate of the context click
+ * @return True if there was an assigned OnContextClickListener that consumed the event, false
+ * otherwise.
+ */
+ public boolean performContextClick(float x, float y) {
+ return performContextClick();
+ }
+
+ /**
+ * Call this view's OnContextClickListener, if it is defined.
+ *
+ * @return True if there was an assigned OnContextClickListener that consumed the event, false
+ * otherwise.
+ */
+ public boolean performContextClick() {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CONTEXT_CLICKED);
+
+ boolean handled = false;
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnContextClickListener != null) {
+ handled = li.mOnContextClickListener.onContextClick(View.this);
+ }
+ if (handled) {
+ performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
+ }
+ return handled;
+ }
+
+ /**
+ * Performs button-related actions during a touch down event.
+ *
+ * @param event The event.
+ * @return True if the down was consumed.
+ *
+ * @hide
+ */
+ protected boolean performButtonActionOnTouchDown(MotionEvent event) {
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE) &&
+ (event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
+ showContextMenu(event.getX(), event.getY());
+ mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Shows the context menu for this view.
+ *
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
+ * @see #showContextMenu(float, float)
+ */
+ public boolean showContextMenu() {
+ return getParent().showContextMenuForChild(this);
+ }
+
+ /**
+ * Shows the context menu for this view anchored to the specified
+ * view-relative coordinate.
+ *
+ * @param x the X coordinate in pixels relative to the view to which the
+ * menu should be anchored, or {@link Float#NaN} to disable anchoring
+ * @param y the Y coordinate in pixels relative to the view to which the
+ * menu should be anchored, or {@link Float#NaN} to disable anchoring
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
+ */
+ public boolean showContextMenu(float x, float y) {
+ return getParent().showContextMenuForChild(this, x, y);
+ }
+
+ /**
+ * Start an action mode with the default type {@link ActionMode#TYPE_PRIMARY}.
+ *
+ * @param callback Callback that will control the lifecycle of the action mode
+ * @return The new action mode if it is started, null otherwise
+ *
+ * @see ActionMode
+ * @see #startActionMode(android.view.ActionMode.Callback, int)
+ */
+ public ActionMode startActionMode(ActionMode.Callback callback) {
+ return startActionMode(callback, ActionMode.TYPE_PRIMARY);
+ }
+
+ /**
+ * Start an action mode with the given type.
+ *
+ * @param callback Callback that will control the lifecycle of the action mode
+ * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}.
+ * @return The new action mode if it is started, null otherwise
+ *
+ * @see ActionMode
+ */
+ public ActionMode startActionMode(ActionMode.Callback callback, int type) {
+ ViewParent parent = getParent();
+ if (parent == null) return null;
+ try {
+ return parent.startActionModeForChild(this, callback, type);
+ } catch (AbstractMethodError ame) {
+ // Older implementations of custom views might not implement this.
+ return parent.startActionModeForChild(this, callback);
+ }
+ }
+
+ /**
+ * Call {@link Context#startActivityForResult(String, Intent, int, Bundle)} for the View's
+ * Context, creating a unique View identifier to retrieve the result.
+ *
+ * @param intent The Intent to be started.
+ * @param requestCode The request code to use.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public void startActivityForResult(Intent intent, int requestCode) {
+ mStartActivityRequestWho = "@android:view:" + System.identityHashCode(this);
+ getContext().startActivityForResult(mStartActivityRequestWho, intent, requestCode, null);
+ }
+
+ /**
+ * If this View corresponds to the calling who, dispatches the activity result.
+ * @param who The identifier for the targeted View to receive the result.
+ * @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").
+ * @return {@code true} if the activity result was dispatched.
+ * @hide
+ */
+ public boolean dispatchActivityResult(
+ String who, int requestCode, int resultCode, Intent data) {
+ if (mStartActivityRequestWho != null && mStartActivityRequestWho.equals(who)) {
+ onActivityResult(requestCode, resultCode, data);
+ mStartActivityRequestWho = null;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 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
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // Do nothing.
+ }
+
+ /**
+ * Register a callback to be invoked when a hardware key is pressed in this view.
+ * Key presses in software input methods will generally not trigger the methods of
+ * this listener.
+ * @param l the key listener to attach to this view
+ */
+ public void setOnKeyListener(OnKeyListener l) {
+ getListenerInfo().mOnKeyListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when a touch event is sent to this view.
+ * @param l the touch listener to attach to this view
+ */
+ public void setOnTouchListener(OnTouchListener l) {
+ getListenerInfo().mOnTouchListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when a generic motion event is sent to this view.
+ * @param l the generic motion listener to attach to this view
+ */
+ public void setOnGenericMotionListener(OnGenericMotionListener l) {
+ getListenerInfo().mOnGenericMotionListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when a hover event is sent to this view.
+ * @param l the hover listener to attach to this view
+ */
+ public void setOnHoverListener(OnHoverListener l) {
+ getListenerInfo().mOnHoverListener = l;
+ }
+
+ /**
+ * Register a drag event listener callback object for this View. The parameter is
+ * an implementation of {@link android.view.View.OnDragListener}. To send a drag event to a
+ * View, the system calls the
+ * {@link android.view.View.OnDragListener#onDrag(View,DragEvent)} method.
+ * @param l An implementation of {@link android.view.View.OnDragListener}.
+ */
+ public void setOnDragListener(OnDragListener l) {
+ getListenerInfo().mOnDragListener = l;
+ }
+
+ /**
+ * Give this view focus. This will cause
+ * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called.
+ *
+ * Note: this does not check whether this {@link View} should get focus, it just
+ * gives it focus no matter what. It should only be called internally by framework
+ * code that knows what it is doing, namely {@link #requestFocus(int, Rect)}.
+ *
+ * @param direction values are {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. This is the direction which
+ * focus moved when requestFocus() is called. It may not always
+ * apply, in which case use the default View.FOCUS_DOWN.
+ * @param previouslyFocusedRect The rectangle of the view that had focus
+ * prior in this View's coordinate system.
+ */
+ void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
+ if (DBG) {
+ System.out.println(this + " requestFocus()");
+ }
+
+ if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
+ mPrivateFlags |= PFLAG_FOCUSED;
+
+ View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
+
+ if (mParent != null) {
+ mParent.requestChildFocus(this, this);
+ updateFocusedInCluster(oldFocus, direction);
+ }
+
+ if (mAttachInfo != null) {
+ mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
+ }
+
+ onFocusChanged(true, direction, previouslyFocusedRect);
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Sets this view's preference for reveal behavior when it gains focus.
+ *
+ * <p>When set to true, this is a signal to ancestor views in the hierarchy that
+ * this view would prefer to be brought fully into view when it gains focus.
+ * For example, a text field that a user is meant to type into. Other views such
+ * as scrolling containers may prefer to opt-out of this behavior.</p>
+ *
+ * <p>The default value for views is true, though subclasses may change this
+ * based on their preferred behavior.</p>
+ *
+ * @param revealOnFocus true to request reveal on focus in ancestors, false otherwise
+ *
+ * @see #getRevealOnFocusHint()
+ */
+ public final void setRevealOnFocusHint(boolean revealOnFocus) {
+ if (revealOnFocus) {
+ mPrivateFlags3 &= ~PFLAG3_NO_REVEAL_ON_FOCUS;
+ } else {
+ mPrivateFlags3 |= PFLAG3_NO_REVEAL_ON_FOCUS;
+ }
+ }
+
+ /**
+ * Returns this view's preference for reveal behavior when it gains focus.
+ *
+ * <p>When this method returns true for a child view requesting focus, ancestor
+ * views responding to a focus change in {@link ViewParent#requestChildFocus(View, View)}
+ * should make a best effort to make the newly focused child fully visible to the user.
+ * When it returns false, ancestor views should preferably not disrupt scroll positioning or
+ * other properties affecting visibility to the user as part of the focus change.</p>
+ *
+ * @return true if this view would prefer to become fully visible when it gains focus,
+ * false if it would prefer not to disrupt scroll positioning
+ *
+ * @see #setRevealOnFocusHint(boolean)
+ */
+ public final boolean getRevealOnFocusHint() {
+ return (mPrivateFlags3 & PFLAG3_NO_REVEAL_ON_FOCUS) == 0;
+ }
+
+ /**
+ * Populates <code>outRect</code> with the hotspot bounds. By default,
+ * the hotspot bounds are identical to the screen bounds.
+ *
+ * @param outRect rect to populate with hotspot bounds
+ * @hide Only for internal use by views and widgets.
+ */
+ public void getHotspotBounds(Rect outRect) {
+ final Drawable background = getBackground();
+ if (background != null) {
+ background.getHotspotBounds(outRect);
+ } else {
+ getBoundsOnScreen(outRect);
+ }
+ }
+
+ /**
+ * Request that a rectangle of this view be visible on the screen,
+ * scrolling if necessary just enough.
+ *
+ * <p>A View should call this if it maintains some notion of which part
+ * of its content is interesting. For example, a text editing view
+ * should call this when its cursor moves.
+ * <p>The Rectangle passed into this method should be in the View's content coordinate space.
+ * It should not be affected by which part of the View is currently visible or its scroll
+ * position.
+ *
+ * @param rectangle The rectangle in the View's content coordinate space
+ * @return Whether any parent scrolled.
+ */
+ public boolean requestRectangleOnScreen(Rect rectangle) {
+ return requestRectangleOnScreen(rectangle, false);
+ }
+
+ /**
+ * Request that a rectangle of this view be visible on the screen,
+ * scrolling if necessary just enough.
+ *
+ * <p>A View should call this if it maintains some notion of which part
+ * of its content is interesting. For example, a text editing view
+ * should call this when its cursor moves.
+ * <p>The Rectangle passed into this method should be in the View's content coordinate space.
+ * It should not be affected by which part of the View is currently visible or its scroll
+ * position.
+ * <p>When <code>immediate</code> is set to true, scrolling will not be
+ * animated.
+ *
+ * @param rectangle The rectangle in the View's content coordinate space
+ * @param immediate True to forbid animated scrolling, false otherwise
+ * @return Whether any parent scrolled.
+ */
+ public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
+ if (mParent == null) {
+ return false;
+ }
+
+ View child = this;
+
+ RectF position = (mAttachInfo != null) ? mAttachInfo.mTmpTransformRect : new RectF();
+ position.set(rectangle);
+
+ ViewParent parent = mParent;
+ boolean scrolled = false;
+ while (parent != null) {
+ rectangle.set((int) position.left, (int) position.top,
+ (int) position.right, (int) position.bottom);
+
+ scrolled |= parent.requestChildRectangleOnScreen(child, rectangle, immediate);
+
+ if (!(parent instanceof View)) {
+ break;
+ }
+
+ // move it from child's content coordinate space to parent's content coordinate space
+ position.offset(child.mLeft - child.getScrollX(), child.mTop -child.getScrollY());
+
+ child = (View) parent;
+ parent = child.getParent();
+ }
+
+ return scrolled;
+ }
+
+ /**
+ * Called when this view wants to give up focus. If focus is cleared
+ * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} is called.
+ * <p>
+ * <strong>Note:</strong> When not in touch-mode, the framework will try to give focus
+ * to the first focusable View from the top after focus is cleared. Hence, if this
+ * View is the first from the top that can take focus, then all callbacks
+ * related to clearing focus will be invoked after which the framework will
+ * give focus to this view.
+ * </p>
+ */
+ public void clearFocus() {
+ if (DBG) {
+ System.out.println(this + " clearFocus()");
+ }
+
+ final boolean refocus = sAlwaysAssignFocus || !isInTouchMode();
+ clearFocusInternal(null, true, refocus);
+ }
+
+ /**
+ * Clears focus from the view, optionally propagating the change up through
+ * the parent hierarchy and requesting that the root view place new focus.
+ *
+ * @param propagate whether to propagate the change up through the parent
+ * hierarchy
+ * @param refocus when propagate is true, specifies whether to request the
+ * root view place new focus
+ */
+ void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
+ if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
+ mPrivateFlags &= ~PFLAG_FOCUSED;
+ clearParentsWantFocus();
+
+ if (propagate && mParent != null) {
+ mParent.clearChildFocus(this);
+ }
+
+ onFocusChanged(false, 0, null);
+ refreshDrawableState();
+
+ if (propagate && (!refocus || !rootViewRequestFocus())) {
+ notifyGlobalFocusCleared(this);
+ }
+ }
+ }
+
+ void notifyGlobalFocusCleared(View oldFocus) {
+ if (oldFocus != null && mAttachInfo != null) {
+ mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null);
+ }
+ }
+
+ boolean rootViewRequestFocus() {
+ final View root = getRootView();
+ return root != null && root.requestFocus();
+ }
+
+ /**
+ * Called internally by the view system when a new view is getting focus.
+ * This is what clears the old focus.
+ * <p>
+ * <b>NOTE:</b> The parent view's focused child must be updated manually
+ * after calling this method. Otherwise, the view hierarchy may be left in
+ * an inconstent state.
+ */
+ void unFocus(View focused) {
+ if (DBG) {
+ System.out.println(this + " unFocus()");
+ }
+
+ clearFocusInternal(focused, false, false);
+ }
+
+ /**
+ * Returns true if this view has focus itself, or is the ancestor of the
+ * view that has focus.
+ *
+ * @return True if this view has or contains focus, false otherwise.
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ public boolean hasFocus() {
+ return (mPrivateFlags & PFLAG_FOCUSED) != 0;
+ }
+
+ /**
+ * Returns true if this view is focusable or if it contains a reachable View
+ * for which {@link #hasFocusable()} returns {@code true}. A "reachable hasFocusable()"
+ * is a view whose parents do not block descendants focus.
+ * Only {@link #VISIBLE} views are considered focusable.
+ *
+ * <p>As of {@link Build.VERSION_CODES#O} views that are determined to be focusable
+ * through {@link #FOCUSABLE_AUTO} will also cause this method to return {@code true}.
+ * Apps that declare a {@link android.content.pm.ApplicationInfo#targetSdkVersion} of
+ * earlier than {@link Build.VERSION_CODES#O} will continue to see this method return
+ * {@code false} for views not explicitly marked as focusable.
+ * Use {@link #hasExplicitFocusable()} if you require the pre-{@link Build.VERSION_CODES#O}
+ * behavior.</p>
+ *
+ * @return {@code true} if the view is focusable or if the view contains a focusable
+ * view, {@code false} otherwise
+ *
+ * @see ViewGroup#FOCUS_BLOCK_DESCENDANTS
+ * @see ViewGroup#getTouchscreenBlocksFocus()
+ * @see #hasExplicitFocusable()
+ */
+ public boolean hasFocusable() {
+ return hasFocusable(!sHasFocusableExcludeAutoFocusable, false);
+ }
+
+ /**
+ * Returns true if this view is focusable or if it contains a reachable View
+ * for which {@link #hasExplicitFocusable()} returns {@code true}.
+ * A "reachable hasExplicitFocusable()" is a view whose parents do not block descendants focus.
+ * Only {@link #VISIBLE} views for which {@link #getFocusable()} would return
+ * {@link #FOCUSABLE} are considered focusable.
+ *
+ * <p>This method preserves the pre-{@link Build.VERSION_CODES#O} behavior of
+ * {@link #hasFocusable()} in that only views explicitly set focusable will cause
+ * this method to return true. A view set to {@link #FOCUSABLE_AUTO} that resolves
+ * to focusable will not.</p>
+ *
+ * @return {@code true} if the view is focusable or if the view contains a focusable
+ * view, {@code false} otherwise
+ *
+ * @see #hasFocusable()
+ */
+ public boolean hasExplicitFocusable() {
+ return hasFocusable(false, true);
+ }
+
+ boolean hasFocusable(boolean allowAutoFocus, boolean dispatchExplicit) {
+ if (!isFocusableInTouchMode()) {
+ for (ViewParent p = mParent; p instanceof ViewGroup; p = p.getParent()) {
+ final ViewGroup g = (ViewGroup) p;
+ if (g.shouldBlockFocusForTouchscreen()) {
+ return false;
+ }
+ }
+ }
+
+ // Invisible, gone, or disabled views are never focusable.
+ if ((mViewFlags & VISIBILITY_MASK) != VISIBLE
+ || (mViewFlags & ENABLED_MASK) != ENABLED) {
+ return false;
+ }
+
+ // Only use effective focusable value when allowed.
+ if ((allowAutoFocus || getFocusable() != FOCUSABLE_AUTO) && isFocusable()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called by the view system when the focus state of this view changes.
+ * When the focus change event is caused by directional navigation, direction
+ * and previouslyFocusedRect provide insight into where the focus is coming from.
+ * When overriding, be sure to call up through to the super class so that
+ * the standard focus handling will occur.
+ *
+ * @param gainFocus True if the View has focus; false otherwise.
+ * @param direction The direction focus has moved when requestFocus()
+ * is called to give this view focus. Values are
+ * {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT},
+ * {@link #FOCUS_RIGHT}, {@link #FOCUS_FORWARD}, or {@link #FOCUS_BACKWARD}.
+ * It may not always apply, in which case use the default.
+ * @param previouslyFocusedRect The rectangle, in this view's coordinate
+ * system, of the previously focused view. If applicable, this will be
+ * passed in as finer grained information about where the focus is coming
+ * from (in addition to direction). Will be <code>null</code> otherwise.
+ */
+ @CallSuper
+ protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
+ @Nullable Rect previouslyFocusedRect) {
+ if (gainFocus) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ } else {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ // Here we check whether we still need the default focus highlight, and switch it on/off.
+ switchDefaultFocusHighlight();
+
+ if (!gainFocus) {
+ if (isPressed()) {
+ setPressed(false);
+ }
+ if (hasWindowFocus()) {
+ notifyFocusChangeToImeFocusController(false /* hasFocus */);
+ }
+ onFocusLost();
+ } else if (hasWindowFocus()) {
+ notifyFocusChangeToImeFocusController(true /* hasFocus */);
+ }
+
+ invalidate(true);
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnFocusChangeListener != null) {
+ li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
+ }
+
+ if (mAttachInfo != null) {
+ mAttachInfo.mKeyDispatchState.reset(this);
+ }
+
+ if (mParent != null) {
+ mParent.onDescendantUnbufferedRequested();
+ }
+
+ notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
+ }
+
+ /**
+ * Notify {@link ImeFocusController} about the focus change of the {@link View}.
+ *
+ * @param hasFocus {@code true} when the {@link View} is being focused.
+ */
+ private void notifyFocusChangeToImeFocusController(boolean hasFocus) {
+ if (mAttachInfo == null) {
+ return;
+ }
+ mAttachInfo.mViewRootImpl.getImeFocusController().onViewFocusChanged(this, hasFocus);
+ }
+
+ /** @hide */
+ public void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
+ if (canNotifyAutofillEnterExitEvent()) {
+ AutofillManager afm = getAutofillManager();
+ if (afm != null) {
+ if (enter && isFocused()) {
+ // We have not been laid out yet, hence cannot evaluate
+ // whether this view is visible to the user, we will do
+ // the evaluation once layout is complete.
+ if (!isLaidOut()) {
+ mPrivateFlags3 |= PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
+ } else if (isVisibleToUser()) {
+ // TODO This is a potential problem that View gets focus before it's visible
+ // to User. Ideally View should handle the event when isVisibleToUser()
+ // becomes true where it should issue notifyViewEntered().
+ afm.notifyViewEntered(this);
+ }
+ } else if (!enter && !isFocused()) {
+ afm.notifyViewExited(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Visually distinct portion of a window with window-like semantics are considered panes for
+ * accessibility purposes. One example is the content view of a fragment that is replaced.
+ * In order for accessibility services to understand a pane's window-like behavior, panes
+ * should have descriptive titles. Views with pane titles produce {@link AccessibilityEvent}s
+ * when they appear, disappear, or change title.
+ *
+ * @param accessibilityPaneTitle The pane's title. Setting to {@code null} indicates that this
+ * View is not a pane.
+ *
+ * {@see AccessibilityNodeInfo#setPaneTitle(CharSequence)}
+ *
+ * @attr ref android.R.styleable#View_accessibilityPaneTitle
+ */
+ public void setAccessibilityPaneTitle(@Nullable CharSequence accessibilityPaneTitle) {
+ if (!TextUtils.equals(accessibilityPaneTitle, mAccessibilityPaneTitle)) {
+ mAccessibilityPaneTitle = accessibilityPaneTitle;
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_TITLE);
+ }
+ }
+
+ /**
+ * Get the title of the pane for purposes of accessibility.
+ *
+ * @return The current pane title.
+ *
+ * {@see #setAccessibilityPaneTitle}.
+ *
+ * @attr ref android.R.styleable#View_accessibilityPaneTitle
+ */
+ @InspectableProperty
+ @Nullable
+ public CharSequence getAccessibilityPaneTitle() {
+ return mAccessibilityPaneTitle;
+ }
+
+ private boolean isAccessibilityPane() {
+ return mAccessibilityPaneTitle != null;
+ }
+
+ /**
+ * Sends an accessibility event of the given type. If accessibility is
+ * not enabled this method has no effect. The default implementation calls
+ * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first
+ * to populate information about the event source (this View), then calls
+ * {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} to
+ * populate the text content of the event source including its descendants,
+ * and last calls
+ * {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
+ * on its parent to request sending of the event to interested parties.
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#sendAccessibilityEvent(View, int)} is
+ * responsible for handling this call.
+ * </p>
+ *
+ * @param eventType The type of the event to send, as defined by several types from
+ * {@link android.view.accessibility.AccessibilityEvent}, such as
+ * {@link android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED} or
+ * {@link android.view.accessibility.AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}.
+ *
+ * @see #onInitializeAccessibilityEvent(AccessibilityEvent)
+ * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ * @see ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)
+ * @see AccessibilityDelegate
+ */
+ public void sendAccessibilityEvent(int eventType) {
+ if (mAccessibilityDelegate != null) {
+ mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
+ } else {
+ sendAccessibilityEventInternal(eventType);
+ }
+ }
+
+ /**
+ * Convenience method for sending a {@link AccessibilityEvent#TYPE_ANNOUNCEMENT}
+ * {@link AccessibilityEvent} to suggest that an accessibility service announce the
+ * specified text to its users.
+ * <p>
+ * Note: The event generated with this API carries no semantic meaning, and is appropriate only
+ * in exceptional situations. Apps can generally achieve correct behavior for accessibility by
+ * accurately supplying the semantics of their UI.
+ * They should not need to specify what exactly is announced to users.
+ *
+ * @param text The announcement text.
+ */
+ public void announceForAccessibility(CharSequence text) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled() && mParent != null) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ onInitializeAccessibilityEvent(event);
+ event.getText().add(text);
+ event.setContentDescription(null);
+ mParent.requestSendAccessibilityEvent(this, event);
+ }
+ }
+
+ /**
+ * @see #sendAccessibilityEvent(int)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ public void sendAccessibilityEventInternal(int eventType) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType));
+ }
+ }
+
+ /**
+ * This method behaves exactly as {@link #sendAccessibilityEvent(int)} but
+ * takes as an argument an empty {@link AccessibilityEvent} and does not
+ * perform a check whether accessibility is enabled.
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#sendAccessibilityEventUnchecked(View, AccessibilityEvent)}
+ * is responsible for handling this call.
+ * </p>
+ *
+ * @param event The event to send.
+ *
+ * @see #sendAccessibilityEvent(int)
+ */
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+ if (mAccessibilityDelegate != null) {
+ mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event);
+ } else {
+ sendAccessibilityEventUncheckedInternal(event);
+ }
+ }
+
+ /**
+ * @see #sendAccessibilityEventUnchecked(AccessibilityEvent)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) {
+ // Panes disappearing are relevant even if though the view is no longer visible.
+ boolean isWindowStateChanged =
+ (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ boolean isWindowDisappearedEvent = isWindowStateChanged && ((event.getContentChangeTypes()
+ & AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED) != 0);
+ boolean detached = detached();
+ if (!isShown() && !isWindowDisappearedEvent && !detached) {
+ return;
+ }
+ onInitializeAccessibilityEvent(event);
+ // Only a subset of accessibility events populates text content.
+ if ((event.getEventType() & POPULATING_ACCESSIBILITY_EVENT_TYPES) != 0) {
+ dispatchPopulateAccessibilityEvent(event);
+ }
+ SendAccessibilityEventThrottle throttle = getThrottleForAccessibilityEvent(event);
+ if (throttle != null) {
+ throttle.post(event);
+ } else if (!isWindowDisappearedEvent && detached) {
+ // Views could be attached soon later. Accessibility events during this temporarily
+ // detached period should be sent too.
+ postDelayed(() -> {
+ if (AccessibilityManager.getInstance(mContext).isEnabled() && isShown()) {
+ requestParentSendAccessibilityEvent(event);
+ }
+ }, ViewConfiguration.getSendRecurringAccessibilityEventsInterval());
+ } else {
+ requestParentSendAccessibilityEvent(event);
+ }
+ }
+
+ private void requestParentSendAccessibilityEvent(AccessibilityEvent event) {
+ ViewParent parent = getParent();
+ if (parent != null) {
+ getParent().requestSendAccessibilityEvent(this, event);
+ }
+ }
+
+ private SendAccessibilityEventThrottle getThrottleForAccessibilityEvent(
+ AccessibilityEvent event) {
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ if (mSendViewScrolledAccessibilityEvent == null) {
+ mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
+ }
+ return mSendViewScrolledAccessibilityEvent;
+ }
+ boolean isStateContentChanged = (event.getContentChangeTypes()
+ & AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION) != 0;
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+ && isStateContentChanged) {
+ if (mSendStateChangedAccessibilityEvent == null) {
+ mSendStateChangedAccessibilityEvent = new SendAccessibilityEventThrottle();
+ }
+ return mSendStateChangedAccessibilityEvent;
+ }
+ return null;
+ }
+
+ private void clearAccessibilityThrottles() {
+ cancel(mSendViewScrolledAccessibilityEvent);
+ cancel(mSendStateChangedAccessibilityEvent);
+ }
+
+ /**
+ * Dispatches an {@link AccessibilityEvent} to the {@link View} first and then
+ * to its children for adding their text content to the event. Note that the
+ * event text is populated in a separate dispatch path since we add to the
+ * event not only the text of the source but also the text of all its descendants.
+ * A typical implementation will call
+ * {@link #onPopulateAccessibilityEvent(AccessibilityEvent)} on the this view
+ * and then call the {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)}
+ * on each child. Override this method if custom population of the event text
+ * content is required.
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#dispatchPopulateAccessibilityEvent(View, AccessibilityEvent)}
+ * is responsible for handling this call.
+ * </p>
+ * <p>
+ * <em>Note:</em> Accessibility events of certain types are not dispatched for
+ * populating the event text via this method. For details refer to {@link AccessibilityEvent}.
+ * </p>
+ *
+ * @param event The event.
+ *
+ * @return True if the event population was completed.
+ */
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ if (mAccessibilityDelegate != null) {
+ return mAccessibilityDelegate.dispatchPopulateAccessibilityEvent(this, event);
+ } else {
+ return dispatchPopulateAccessibilityEventInternal(event);
+ }
+ }
+
+ /**
+ * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
+ return false;
+ }
+
+ /**
+ * Called from {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)}
+ * giving a chance to this View to populate the accessibility event with its
+ * text content. While this method is free to modify event
+ * attributes other than text content, doing so should normally be performed in
+ * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)}.
+ * <p>
+ * Example: Adding formatted date string to an accessibility event in addition
+ * to the text added by the super implementation:
+ * <pre> public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ * super.onPopulateAccessibilityEvent(event);
+ * final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY;
+ * String selectedDateUtterance = DateUtils.formatDateTime(mContext,
+ * mCurrentDate.getTimeInMillis(), flags);
+ * event.getText().add(selectedDateUtterance);
+ * }</pre>
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#onPopulateAccessibilityEvent(View, AccessibilityEvent)}
+ * is responsible for handling this call.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Always call the super implementation before adding
+ * information to the event, in case the default implementation has basic information to add.
+ * </p>
+ *
+ * @param event The accessibility event which to populate.
+ *
+ * @see #sendAccessibilityEvent(int)
+ * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ */
+ @CallSuper
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ if (mAccessibilityDelegate != null) {
+ mAccessibilityDelegate.onPopulateAccessibilityEvent(this, event);
+ } else {
+ onPopulateAccessibilityEventInternal(event);
+ }
+ }
+
+ /**
+ * @see #onPopulateAccessibilityEvent(AccessibilityEvent)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ if ((event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
+ && isAccessibilityPane()) {
+ event.getText().add(getAccessibilityPaneTitle());
+ }
+ }
+
+ /**
+ * Initializes an {@link AccessibilityEvent} with information about
+ * this View which is the event source. In other words, the source of
+ * an accessibility event is the view whose state change triggered firing
+ * the event.
+ * <p>
+ * Example: Setting the password property of an event in addition
+ * to properties set by the super implementation:
+ * <pre> public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ * super.onInitializeAccessibilityEvent(event);
+ * event.setPassword(true);
+ * }</pre>
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#onInitializeAccessibilityEvent(View, AccessibilityEvent)}
+ * is responsible for handling this call.
+ * </p>
+ * <p class="note"><strong>Note:</strong> Always call the super implementation before adding
+ * information to the event, in case the default implementation has basic information to add.
+ * </p>
+ * @param event The event to initialize.
+ *
+ * @see #sendAccessibilityEvent(int)
+ * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ */
+ @CallSuper
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ if (mAccessibilityDelegate != null) {
+ mAccessibilityDelegate.onInitializeAccessibilityEvent(this, event);
+ } else {
+ onInitializeAccessibilityEventInternal(event);
+ }
+ }
+
+ /**
+ * @see #onInitializeAccessibilityEvent(AccessibilityEvent)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ event.setSource(this);
+ event.setClassName(getAccessibilityClassName());
+ event.setPackageName(getContext().getPackageName());
+ event.setEnabled(isEnabled());
+ event.setContentDescription(mContentDescription);
+ event.setScrollX(getScrollX());
+ event.setScrollY(getScrollY());
+
+ switch (event.getEventType()) {
+ case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
+ ArrayList<View> focusablesTempList = (mAttachInfo != null)
+ ? mAttachInfo.mTempArrayList : new ArrayList<View>();
+ getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL);
+ event.setItemCount(focusablesTempList.size());
+ event.setCurrentItemIndex(focusablesTempList.indexOf(this));
+ if (mAttachInfo != null) {
+ focusablesTempList.clear();
+ }
+ } break;
+ case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
+ CharSequence text = getIterableTextForAccessibility();
+ if (text != null && text.length() > 0) {
+ event.setFromIndex(getAccessibilitySelectionStart());
+ event.setToIndex(getAccessibilitySelectionEnd());
+ event.setItemCount(text.length());
+ }
+ } break;
+ }
+ }
+
+ /**
+ * Returns an {@link AccessibilityNodeInfo} representing this view from the
+ * point of view of an {@link android.accessibilityservice.AccessibilityService}.
+ * This method is responsible for obtaining an accessibility node info from a
+ * pool of reusable instances and calling
+ * {@link #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} on this view to
+ * initialize the former.
+ * <p>
+ * Note: The client is responsible for recycling the obtained instance by calling
+ * {@link AccessibilityNodeInfo#recycle()} to minimize object creation.
+ * </p>
+ *
+ * @return A populated {@link AccessibilityNodeInfo}.
+ *
+ * @see AccessibilityNodeInfo
+ */
+ public AccessibilityNodeInfo createAccessibilityNodeInfo() {
+ if (mAccessibilityDelegate != null) {
+ return mAccessibilityDelegate.createAccessibilityNodeInfo(this);
+ } else {
+ return createAccessibilityNodeInfoInternal();
+ }
+ }
+
+ /**
+ * @see #createAccessibilityNodeInfo()
+ *
+ * @hide
+ */
+ public AccessibilityNodeInfo createAccessibilityNodeInfoInternal() {
+ AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider != null) {
+ return provider.createAccessibilityNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID);
+ } else {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this);
+ onInitializeAccessibilityNodeInfo(info);
+ return info;
+ }
+ }
+
+ /**
+ * Initializes an {@link AccessibilityNodeInfo} with information about this view.
+ * The base implementation sets:
+ * <ul>
+ * <li>{@link AccessibilityNodeInfo#setParent(View)},</li>
+ * <li>{@link AccessibilityNodeInfo#setBoundsInParent(Rect)},</li>
+ * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)},</li>
+ * <li>{@link AccessibilityNodeInfo#setPackageName(CharSequence)},</li>
+ * <li>{@link AccessibilityNodeInfo#setClassName(CharSequence)},</li>
+ * <li>{@link AccessibilityNodeInfo#setContentDescription(CharSequence)},</li>
+ * <li>{@link AccessibilityNodeInfo#setEnabled(boolean)},</li>
+ * <li>{@link AccessibilityNodeInfo#setClickable(boolean)},</li>
+ * <li>{@link AccessibilityNodeInfo#setFocusable(boolean)},</li>
+ * <li>{@link AccessibilityNodeInfo#setFocused(boolean)},</li>
+ * <li>{@link AccessibilityNodeInfo#setLongClickable(boolean)},</li>
+ * <li>{@link AccessibilityNodeInfo#setSelected(boolean)},</li>
+ * <li>{@link AccessibilityNodeInfo#setContextClickable(boolean)}</li>
+ * </ul>
+ * <p>
+ * Subclasses should override this method, call the super implementation,
+ * and set additional attributes.
+ * </p>
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfo)}
+ * is responsible for handling this call.
+ * </p>
+ *
+ * @param info The instance to initialize.
+ */
+ @CallSuper
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ if (mAccessibilityDelegate != null) {
+ mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(this, info);
+ } else {
+ onInitializeAccessibilityNodeInfoInternal(info);
+ }
+ }
+
+ /**
+ * Gets the location of this view in screen coordinates.
+ *
+ * @param outRect The output location
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void getBoundsOnScreen(Rect outRect) {
+ getBoundsOnScreen(outRect, false);
+ }
+
+ /**
+ * Gets the location of this view in screen coordinates.
+ *
+ * @param outRect The output location
+ * @param clipToParent Whether to clip child bounds to the parent ones.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
+ if (mAttachInfo == null) {
+ return;
+ }
+
+ RectF position = mAttachInfo.mTmpTransformRect;
+ position.set(0, 0, mRight - mLeft, mBottom - mTop);
+ mapRectFromViewToScreenCoords(position, clipToParent);
+ outRect.set(Math.round(position.left), Math.round(position.top),
+ Math.round(position.right), Math.round(position.bottom));
+ }
+
+ /**
+ * Map a rectangle from view-relative coordinates to screen-relative coordinates
+ *
+ * @param rect The rectangle to be mapped
+ * @param clipToParent Whether to clip child bounds to the parent ones.
+ * @hide
+ */
+ public void mapRectFromViewToScreenCoords(RectF rect, boolean clipToParent) {
+ if (!hasIdentityMatrix()) {
+ getMatrix().mapRect(rect);
+ }
+
+ rect.offset(mLeft, mTop);
+
+ ViewParent parent = mParent;
+ while (parent instanceof View) {
+ View parentView = (View) parent;
+
+ rect.offset(-parentView.mScrollX, -parentView.mScrollY);
+
+ if (clipToParent) {
+ rect.left = Math.max(rect.left, 0);
+ rect.top = Math.max(rect.top, 0);
+ rect.right = Math.min(rect.right, parentView.getWidth());
+ rect.bottom = Math.min(rect.bottom, parentView.getHeight());
+ }
+
+ if (!parentView.hasIdentityMatrix()) {
+ parentView.getMatrix().mapRect(rect);
+ }
+
+ rect.offset(parentView.mLeft, parentView.mTop);
+
+ parent = parentView.mParent;
+ }
+
+ if (parent instanceof ViewRootImpl) {
+ ViewRootImpl viewRootImpl = (ViewRootImpl) parent;
+ rect.offset(0, -viewRootImpl.mCurScrollY);
+ }
+
+ rect.offset(mAttachInfo.mWindowLeft, mAttachInfo.mWindowTop);
+ }
+
+ /**
+ * Return the class name of this object to be used for accessibility purposes.
+ * Subclasses should only override this if they are implementing something that
+ * should be seen as a completely new class of view when used by accessibility,
+ * unrelated to the class it is deriving from. This is used to fill in
+ * {@link AccessibilityNodeInfo#setClassName AccessibilityNodeInfo.setClassName}.
+ */
+ public CharSequence getAccessibilityClassName() {
+ return View.class.getName();
+ }
+
+ /**
+ * Called when assist structure is being retrieved from a view as part of
+ * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}.
+ * @param structure Fill in with structured view data. The default implementation
+ * fills in all data that can be inferred from the view itself.
+ */
+ public void onProvideStructure(ViewStructure structure) {
+ onProvideStructure(structure, VIEW_STRUCTURE_FOR_ASSIST, /* flags= */ 0);
+ }
+
+ /**
+ * Populates a {@link ViewStructure} to fullfil an autofill request.
+ *
+ * <p>The structure should contain at least the following properties:
+ * <ul>
+ * <li>Autofill id ({@link ViewStructure#setAutofillId(AutofillId, int)}).
+ * <li>Autofill type ({@link ViewStructure#setAutofillType(int)}).
+ * <li>Autofill value ({@link ViewStructure#setAutofillValue(AutofillValue)}).
+ * <li>Whether the data is sensitive ({@link ViewStructure#setDataIsSensitive(boolean)}).
+ * </ul>
+ *
+ * <p>It's also recommended to set the following properties - the more properties the structure
+ * has, the higher the chances of an {@link android.service.autofill.AutofillService} properly
+ * using the structure:
+ *
+ * <ul>
+ * <li>Autofill hints ({@link ViewStructure#setAutofillHints(String[])}).
+ * <li>Autofill options ({@link ViewStructure#setAutofillOptions(CharSequence[])}) when the
+ * view can only be filled with predefined values (typically used when the autofill type
+ * is {@link #AUTOFILL_TYPE_LIST}).
+ * <li>Resource id ({@link ViewStructure#setId(int, String, String, String)}).
+ * <li>Class name ({@link ViewStructure#setClassName(String)}).
+ * <li>Content description ({@link ViewStructure#setContentDescription(CharSequence)}).
+ * <li>Visual properties such as visibility ({@link ViewStructure#setVisibility(int)}),
+ * dimensions ({@link ViewStructure#setDimens(int, int, int, int, int, int)}), and
+ * opacity ({@link ViewStructure#setOpaque(boolean)}).
+ * <li>For views representing text fields, text properties such as the text itself
+ * ({@link ViewStructure#setText(CharSequence)}), text hints
+ * ({@link ViewStructure#setHint(CharSequence)}, input type
+ * ({@link ViewStructure#setInputType(int)}),
+ * <li>For views representing HTML nodes, its web domain
+ * ({@link ViewStructure#setWebDomain(String)}) and HTML properties
+ * (({@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}).
+ * </ul>
+ *
+ * <p>The default implementation of this method already sets most of these properties based on
+ * related {@link View} methods (for example, the autofill id is set using
+ * {@link #getAutofillId()}, the autofill type set using {@link #getAutofillType()}, etc.),
+ * and views in the standard Android widgets library also override it to set their
+ * relevant properties (for example, {@link android.widget.TextView} already sets the text
+ * properties), so it's recommended to only override this method
+ * (and call {@code super.onProvideAutofillStructure()}) when:
+ *
+ * <ul>
+ * <li>The view contents does not include PII (Personally Identifiable Information), so it
+ * can call {@link ViewStructure#setDataIsSensitive(boolean)} passing {@code false}.
+ * <li>The view can only be autofilled with predefined options, so it can call
+ * {@link ViewStructure#setAutofillOptions(CharSequence[])}.
+ * </ul>
+ *
+ * <p><b>Note:</b> The {@code left} and {@code top} values set in
+ * {@link ViewStructure#setDimens(int, int, int, int, int, int)} must be relative to the next
+ * {@link ViewGroup#isImportantForAutofill()} predecessor view included in the structure.
+ *
+ * <p>Views support the Autofill Framework mainly by:
+ * <ul>
+ * <li>Providing the metadata defining what the view means and how it can be autofilled.
+ * <li>Notifying the Android System when the view value changed by calling
+ * {@link AutofillManager#notifyValueChanged(View)}.
+ * <li>Implementing the methods that autofill the view.
+ * </ul>
+ * <p>This method is responsible for the former; {@link #autofill(AutofillValue)} is responsible
+ * for the latter.
+ *
+ * @param structure fill in with structured view data for autofill purposes.
+ * @param flags optional flags.
+ *
+ * @see #AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ */
+ public void onProvideAutofillStructure(ViewStructure structure, @AutofillFlags int flags) {
+ onProvideStructure(structure, VIEW_STRUCTURE_FOR_AUTOFILL, flags);
+ }
+
+ /**
+ * Populates a {@link ViewStructure} for content capture.
+ *
+ * <p>This method is called after a view that is eligible for content capture
+ * (for example, if it {@link #isImportantForContentCapture()}, an intelligence service is
+ * enabled for the user, and the activity rendering the view is enabled for content capture)
+ * is laid out and is visible. The populated structure is then passed to the service through
+ * {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}.
+ *
+ * <p>The default implementation of this method sets the most relevant properties based on
+ * related {@link View} methods, and views in the standard Android widgets library also
+ * override it to set their relevant properties. Therefore, if overriding this method, it
+ * is recommended to call {@code super.onProvideContentCaptureStructure()}.
+ *
+ * <p><b>Note: </b>views that manage a virtual structure under this view must populate just
+ * the node representing this view and return right away, then asynchronously report (not
+ * necessarily in the UI thread) when the children nodes appear, disappear or have their text
+ * changed by calling
+ * {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)},
+ * {@link ContentCaptureSession#notifyViewDisappeared(AutofillId)}, and
+ * {@link ContentCaptureSession#notifyViewTextChanged(AutofillId, CharSequence)}
+ * respectively. The structure for a child must be created using
+ * {@link ContentCaptureSession#newVirtualViewStructure(AutofillId, long)}, and the
+ * {@code autofillId} for a child can be obtained either through
+ * {@code childStructure.getAutofillId()} or
+ * {@link ContentCaptureSession#newAutofillId(AutofillId, long)}.
+ *
+ * <p>When the virtual view hierarchy represents a web page, you should also:
+ *
+ * <ul>
+ * <li>Call {@link ContentCaptureManager#getContentCaptureConditions()} to infer content
+ * capture events should be generate for that URL.
+ * <li>Create a new {@link ContentCaptureSession} child for every HTML element that
+ * renders a new URL (like an {@code IFRAME}) and use that session to notify events from
+ * that subtree.
+ * </ul>
+ *
+ * <p><b>Note: </b>the following methods of the {@code structure} will be ignored:
+ * <ul>
+ * <li>{@link ViewStructure#setChildCount(int)}
+ * <li>{@link ViewStructure#addChildCount(int)}
+ * <li>{@link ViewStructure#getChildCount()}
+ * <li>{@link ViewStructure#newChild(int)}
+ * <li>{@link ViewStructure#asyncNewChild(int)}
+ * <li>{@link ViewStructure#asyncCommit()}
+ * <li>{@link ViewStructure#setWebDomain(String)}
+ * <li>{@link ViewStructure#newHtmlInfoBuilder(String)}
+ * <li>{@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}
+ * <li>{@link ViewStructure#setDataIsSensitive(boolean)}
+ * <li>{@link ViewStructure#setAlpha(float)}
+ * <li>{@link ViewStructure#setElevation(float)}
+ * <li>{@link ViewStructure#setTransformation(Matrix)}
+ *
+ * </ul>
+ */
+ public void onProvideContentCaptureStructure(@NonNull ViewStructure structure, int flags) {
+ onProvideStructure(structure, VIEW_STRUCTURE_FOR_CONTENT_CAPTURE, flags);
+ }
+
+ /** @hide */
+ protected void onProvideStructure(@NonNull ViewStructure structure,
+ @ViewStructureType int viewFor, int flags) {
+ final int id = mID;
+ if (id != NO_ID && !isViewIdGenerated(id)) {
+ String pkg, type, entry;
+ try {
+ final Resources res = getResources();
+ entry = res.getResourceEntryName(id);
+ type = res.getResourceTypeName(id);
+ pkg = res.getResourcePackageName(id);
+ } catch (Resources.NotFoundException e) {
+ entry = type = pkg = null;
+ }
+ structure.setId(id, pkg, type, entry);
+ } else {
+ structure.setId(id, null, null, null);
+ }
+
+ if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL
+ || viewFor == VIEW_STRUCTURE_FOR_CONTENT_CAPTURE) {
+ final @AutofillType int autofillType = getAutofillType();
+ // Don't need to fill autofill info if view does not support it.
+ // For example, only TextViews that are editable support autofill
+ if (autofillType != AUTOFILL_TYPE_NONE) {
+ structure.setAutofillType(autofillType);
+ structure.setAutofillHints(getAutofillHints());
+ structure.setAutofillValue(getAutofillValue());
+ }
+ structure.setImportantForAutofill(getImportantForAutofill());
+ structure.setReceiveContentMimeTypes(getReceiveContentMimeTypes());
+ }
+
+ int ignoredParentLeft = 0;
+ int ignoredParentTop = 0;
+ if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL
+ && (flags & AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) == 0) {
+ View parentGroup = null;
+
+ ViewParent viewParent = getParent();
+ if (viewParent instanceof View) {
+ parentGroup = (View) viewParent;
+ }
+
+ while (parentGroup != null && !parentGroup.isImportantForAutofill()) {
+ ignoredParentLeft += parentGroup.mLeft;
+ ignoredParentTop += parentGroup.mTop;
+
+ viewParent = parentGroup.getParent();
+ if (viewParent instanceof View) {
+ parentGroup = (View) viewParent;
+ } else {
+ break;
+ }
+ }
+ }
+
+ structure.setDimens(ignoredParentLeft + mLeft, ignoredParentTop + mTop, mScrollX, mScrollY,
+ mRight - mLeft, mBottom - mTop);
+ if (viewFor == VIEW_STRUCTURE_FOR_ASSIST) {
+ if (!hasIdentityMatrix()) {
+ structure.setTransformation(getMatrix());
+ }
+ structure.setElevation(getZ());
+ }
+ structure.setVisibility(getVisibility());
+ structure.setEnabled(isEnabled());
+ if (isClickable()) {
+ structure.setClickable(true);
+ }
+ if (isFocusable()) {
+ structure.setFocusable(true);
+ }
+ if (isFocused()) {
+ structure.setFocused(true);
+ }
+ if (isAccessibilityFocused()) {
+ structure.setAccessibilityFocused(true);
+ }
+ if (isSelected()) {
+ structure.setSelected(true);
+ }
+ if (isActivated()) {
+ structure.setActivated(true);
+ }
+ if (isLongClickable()) {
+ structure.setLongClickable(true);
+ }
+ if (this instanceof Checkable) {
+ structure.setCheckable(true);
+ if (((Checkable)this).isChecked()) {
+ structure.setChecked(true);
+ }
+ }
+ if (isOpaque()) {
+ structure.setOpaque(true);
+ }
+ if (isContextClickable()) {
+ structure.setContextClickable(true);
+ }
+ structure.setClassName(getAccessibilityClassName().toString());
+ structure.setContentDescription(getContentDescription());
+ }
+
+ /**
+ * Called when assist structure is being retrieved from a view as part of
+ * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData} to
+ * generate additional virtual structure under this view. The default implementation
+ * uses {@link #getAccessibilityNodeProvider()} to try to generate this from the
+ * view's virtual accessibility nodes, if any. You can override this for a more
+ * optimal implementation providing this data.
+ */
+ public void onProvideVirtualStructure(ViewStructure structure) {
+ onProvideVirtualStructureCompat(structure, false);
+ }
+
+ /**
+ * Fallback implementation to populate a ViewStructure from accessibility state.
+ *
+ * @param structure The structure to populate.
+ * @param forAutofill Whether the structure is needed for autofill.
+ */
+ private void onProvideVirtualStructureCompat(ViewStructure structure, boolean forAutofill) {
+ final AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider != null) {
+ if (forAutofill && Log.isLoggable(AUTOFILL_LOG_TAG, Log.VERBOSE)) {
+ Log.v(AUTOFILL_LOG_TAG, "onProvideVirtualStructureCompat() for " + this);
+ }
+ final AccessibilityNodeInfo info = createAccessibilityNodeInfo();
+ structure.setChildCount(1);
+ final ViewStructure root = structure.newChild(0);
+ populateVirtualStructure(root, provider, info, forAutofill);
+ info.recycle();
+ }
+ }
+
+ /**
+ * Populates a {@link ViewStructure} containing virtual children to fullfil an autofill
+ * request.
+ *
+ * <p>This method should be used when the view manages a virtual structure under this view. For
+ * example, a view that draws input fields using {@link #draw(Canvas)}.
+ *
+ * <p>When implementing this method, subclasses must follow the rules below:
+ *
+ * <ul>
+ * <li>Add virtual children by calling the {@link ViewStructure#newChild(int)} or
+ * {@link ViewStructure#asyncNewChild(int)} methods, where the {@code id} is an unique id
+ * identifying the children in the virtual structure.
+ * <li>The children hierarchy can have multiple levels if necessary, but ideally it should
+ * exclude intermediate levels that are irrelevant for autofill; that would improve the
+ * autofill performance.
+ * <li>Also implement {@link #autofill(SparseArray)} to autofill the virtual
+ * children.
+ * <li>Set the autofill properties of the child structure as defined by
+ * {@link #onProvideAutofillStructure(ViewStructure, int)}, using
+ * {@link ViewStructure#setAutofillId(AutofillId, int)} to set its autofill id.
+ * <li>Call {@link android.view.autofill.AutofillManager#notifyViewEntered(View, int, Rect)}
+ * and/or {@link android.view.autofill.AutofillManager#notifyViewExited(View, int)}
+ * when the focused virtual child changed.
+ * <li>Override {@link #isVisibleToUserForAutofill(int)} to allow the platform to query
+ * whether a given virtual view is visible to the user in order to support triggering
+ * save when all views of interest go away.
+ * <li>Call
+ * {@link android.view.autofill.AutofillManager#notifyValueChanged(View, int, AutofillValue)}
+ * when the value of a virtual child changed.
+ * <li>Call {@link
+ * android.view.autofill.AutofillManager#notifyViewVisibilityChanged(View, int, boolean)}
+ * when the visibility of a virtual child changed.
+ * <li>Call
+ * {@link android.view.autofill.AutofillManager#notifyViewClicked(View, int)} when a virtual
+ * child is clicked.
+ * <li>Call {@link AutofillManager#commit()} when the autofill context of the view structure
+ * changed and the current context should be committed (for example, when the user tapped
+ * a {@code SUBMIT} button in an HTML page).
+ * <li>Call {@link AutofillManager#cancel()} when the autofill context of the view structure
+ * changed and the current context should be canceled (for example, when the user tapped
+ * a {@code CANCEL} button in an HTML page).
+ * <li>Provide ways for users to manually request autofill by calling
+ * {@link AutofillManager#requestAutofill(View, int, Rect)}.
+ * <li>The {@code left} and {@code top} values set in
+ * {@link ViewStructure#setDimens(int, int, int, int, int, int)} must be relative to the
+ * next {@link ViewGroup#isImportantForAutofill()} predecessor view included in the
+ * structure.
+ * </ul>
+ *
+ * <p>Views with virtual children support the Autofill Framework mainly by:
+ * <ul>
+ * <li>Providing the metadata defining what the virtual children mean and how they can be
+ * autofilled.
+ * <li>Implementing the methods that autofill the virtual children.
+ * </ul>
+ * <p>This method is responsible for the former; {@link #autofill(SparseArray)} is responsible
+ * for the latter.
+ *
+ * @param structure fill in with virtual children data for autofill purposes.
+ * @param flags optional flags.
+ *
+ * @see #AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ */
+ public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
+ if (mContext.isAutofillCompatibilityEnabled()) {
+ onProvideVirtualStructureCompat(structure, true);
+ }
+ }
+
+ /**
+ * Sets the listener to be {@link #performReceiveContent used} to handle insertion of
+ * content into this view.
+ *
+ * <p>Depending on the type of view, this listener may be invoked for different scenarios. For
+ * example, for an editable {@link android.widget.TextView}, this listener will be invoked for
+ * the following scenarios:
+ * <ol>
+ * <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the
+ * insertion/selection menu)
+ * <li>Content insertion from the keyboard (from {@link InputConnection#commitContent})
+ * <li>Drag and drop (drop events from {@link #onDragEvent})
+ * <li>Autofill
+ * <li>Selection replacement via {@link Intent#ACTION_PROCESS_TEXT}
+ * </ol>
+ *
+ * <p>When setting a listener, clients must also declare the accepted MIME types.
+ * The listener will still be invoked even if the MIME type of the content is not one of the
+ * declared MIME types (e.g. if the user pastes content whose type is not one of the declared
+ * MIME types).
+ * In that case, the listener may reject the content (defer to the default platform behavior)
+ * or execute some other fallback logic (e.g. show an appropriate message to the user).
+ * The declared MIME types serve as a hint to allow different features to optionally alter
+ * their behavior. For example, a soft keyboard may optionally choose to hide its UI for
+ * inserting GIFs for a particular input field if the MIME types set here for that field
+ * don't include "image/gif" or "image/*".
+ *
+ * <p>Note: MIME type matching in the Android framework is case-sensitive, unlike formal RFC
+ * MIME types. As a result, you should always write your MIME types with lowercase letters,
+ * or use {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to
+ * lowercase.
+ *
+ * @param mimeTypes The MIME types accepted by the given listener. These may use patterns
+ * such as "image/*", but may not start with a wildcard. This argument must
+ * not be null or empty if a non-null listener is passed in.
+ * @param listener The listener to use. This can be null to reset to the default behavior.
+ */
+ public void setOnReceiveContentListener(
+ @SuppressLint("NullableCollection") @Nullable String[] mimeTypes,
+ @Nullable OnReceiveContentListener listener) {
+ if (listener != null) {
+ Preconditions.checkArgument(mimeTypes != null && mimeTypes.length > 0,
+ "When the listener is set, MIME types must also be set");
+ }
+ if (mimeTypes != null) {
+ Preconditions.checkArgument(Arrays.stream(mimeTypes).noneMatch(t -> t.startsWith("*")),
+ "A MIME type set here must not start with *: " + Arrays.toString(mimeTypes));
+ }
+ mReceiveContentMimeTypes = ArrayUtils.isEmpty(mimeTypes) ? null : mimeTypes;
+ getListenerInfo().mOnReceiveContentListener = listener;
+ }
+
+ /**
+ * Receives the given content. If no listener is set, invokes {@link #onReceiveContent}. If a
+ * listener is {@link #setOnReceiveContentListener set}, invokes the listener instead; if the
+ * listener returns a non-null result, invokes {@link #onReceiveContent} to handle it.
+ *
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content that was not accepted (may be all, some, or none
+ * of the passed-in content).
+ */
+ @Nullable
+ public ContentInfo performReceiveContent(@NonNull ContentInfo payload) {
+ final OnReceiveContentListener listener = (mListenerInfo == null) ? null
+ : getListenerInfo().mOnReceiveContentListener;
+ if (listener != null) {
+ final ContentInfo remaining = listener.onReceiveContent(this, payload);
+ return (remaining == null) ? null : onReceiveContent(remaining);
+ }
+ return onReceiveContent(payload);
+ }
+
+ /**
+ * Implements the default behavior for receiving content for this type of view. The default
+ * view implementation is a no-op (returns the passed-in content without acting on it).
+ *
+ * <p>Widgets should override this method to define their default behavior for receiving
+ * content. Apps should {@link #setOnReceiveContentListener set a listener} to provide
+ * app-specific handling for receiving content.
+ *
+ * <p>See {@link #setOnReceiveContentListener} and {@link #performReceiveContent} for more info.
+ *
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content that was not handled (may be all, some, or none
+ * of the passed-in content).
+ */
+ @Nullable
+ public ContentInfo onReceiveContent(@NonNull ContentInfo payload) {
+ return payload;
+ }
+
+ /**
+ * Returns the MIME types accepted by {@link #performReceiveContent} for this view, as
+ * configured via {@link #setOnReceiveContentListener}. By default returns null.
+ *
+ * <p>Different features (e.g. pasting from the clipboard, inserting stickers from the soft
+ * keyboard, etc) may optionally use this metadata to conditionally alter their behavior. For
+ * example, a soft keyboard may choose to hide its UI for inserting GIFs for a particular
+ * input field if the MIME types returned here for that field don't include "image/gif" or
+ * "image/*".
+ *
+ * <p>Note: Comparisons of MIME types should be performed using utilities such as
+ * {@link ClipDescription#compareMimeTypes} rather than simple string equality, in order to
+ * correctly handle patterns such as "text/*", "image/*", etc. Note that MIME type matching
+ * in the Android framework is case-sensitive, unlike formal RFC MIME types. As a result,
+ * you should always write your MIME types with lowercase letters, or use
+ * {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to
+ * lowercase.
+ *
+ * @return The MIME types accepted by {@link #performReceiveContent} for this view (may
+ * include patterns such as "image/*").
+ */
+ @SuppressLint("NullableCollection")
+ @Nullable
+ public String[] getReceiveContentMimeTypes() {
+ return mReceiveContentMimeTypes;
+ }
+
+ /**
+ * Automatically fills the content of this view with the {@code value}.
+ *
+ * <p>Views support the Autofill Framework mainly by:
+ * <ul>
+ * <li>Providing the metadata defining what the view means and how it can be autofilled.
+ * <li>Implementing the methods that autofill the view.
+ * </ul>
+ * <p>{@link #onProvideAutofillStructure(ViewStructure, int)} is responsible for the former,
+ * this method is responsible for latter.
+ *
+ * <p>This method does nothing by default, but when overridden it typically:
+ * <ol>
+ * <li>Checks if the provided value matches the expected type (which is defined by
+ * {@link #getAutofillType()}).
+ * <li>Checks if the view is editable - if it isn't, it should return right away.
+ * <li>Call the proper getter method on {@link AutofillValue} to fetch the actual value.
+ * <li>Pass the actual value to the equivalent setter in the view.
+ * </ol>
+ *
+ * <p>For example, a text-field view could implement the method this way:
+ *
+ * <pre class="prettyprint">
+ * @Override
+ * public void autofill(AutofillValue value) {
+ * if (!value.isText() || !this.isEditable()) {
+ * return;
+ * }
+ * CharSequence text = value.getTextValue();
+ * if (text != null) {
+ * this.setText(text);
+ * }
+ * }
+ * </pre>
+ *
+ * <p>If the value is updated asynchronously, the next call to
+ * {@link AutofillManager#notifyValueChanged(View)} must happen <b>after</b> the value was
+ * changed to the autofilled value. If not, the view will not be considered autofilled.
+ *
+ * <p><b>Note:</b> After this method is called, the value returned by
+ * {@link #getAutofillValue()} must be equal to the {@code value} passed to it, otherwise the
+ * view will not be highlighted as autofilled.
+ *
+ * @param value value to be autofilled.
+ */
+ public void autofill(@SuppressWarnings("unused") AutofillValue value) {
+ }
+
+ /**
+ * Automatically fills the content of the virtual children within this view.
+ *
+ * <p>Views with virtual children support the Autofill Framework mainly by:
+ * <ul>
+ * <li>Providing the metadata defining what the virtual children mean and how they can be
+ * autofilled.
+ * <li>Implementing the methods that autofill the virtual children.
+ * </ul>
+ * <p>{@link #onProvideAutofillVirtualStructure(ViewStructure, int)} is responsible for the
+ * former, this method is responsible for the latter - see {@link #autofill(AutofillValue)} and
+ * {@link #onProvideAutofillVirtualStructure(ViewStructure, int)} for more info about autofill.
+ *
+ * <p>If a child value is updated asynchronously, the next call to
+ * {@link AutofillManager#notifyValueChanged(View, int, AutofillValue)} must happen
+ * <b>after</b> the value was changed to the autofilled value. If not, the child will not be
+ * considered autofilled.
+ *
+ * <p><b>Note:</b> To indicate that a virtual view was autofilled,
+ * <code>?android:attr/autofilledHighlight</code> should be drawn over it until the data
+ * changes.
+ *
+ * @param values map of values to be autofilled, keyed by virtual child id.
+ *
+ * @attr ref android.R.styleable#Theme_autofilledHighlight
+ */
+ public void autofill(@NonNull @SuppressWarnings("unused") SparseArray<AutofillValue> values) {
+ if (!mContext.isAutofillCompatibilityEnabled()) {
+ return;
+ }
+ final AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider == null) {
+ return;
+ }
+ final int valueCount = values.size();
+ for (int i = 0; i < valueCount; i++) {
+ final AutofillValue value = values.valueAt(i);
+ if (value.isText()) {
+ final int virtualId = values.keyAt(i);
+ final CharSequence text = value.getTextValue();
+ final Bundle arguments = new Bundle();
+ arguments.putCharSequence(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
+ provider.performAction(virtualId, AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
+ }
+ }
+ }
+
+ /**
+ * Gets the unique, logical identifier of this view in the activity, for autofill purposes.
+ *
+ * <p>The autofill id is created on demand, unless it is explicitly set by
+ * {@link #setAutofillId(AutofillId)}.
+ *
+ * <p>See {@link #setAutofillId(AutofillId)} for more info.
+ *
+ * @return The View's autofill id.
+ */
+ public final AutofillId getAutofillId() {
+ if (mAutofillId == null) {
+ // The autofill id needs to be unique, but its value doesn't matter,
+ // so it's better to reuse the accessibility id to save space.
+ mAutofillId = new AutofillId(getAutofillViewId());
+ }
+ return mAutofillId;
+ }
+
+ /**
+ * Sets the unique, logical identifier of this view in the activity, for autofill purposes.
+ *
+ * <p>The autofill id is created on demand, and this method should only be called when a view is
+ * reused after {@link #dispatchProvideAutofillStructure(ViewStructure, int)} is called, as
+ * that method creates a snapshot of the view that is passed along to the autofill service.
+ *
+ * <p>This method is typically used when view subtrees are recycled to represent different
+ * content* —in this case, the autofill id can be saved before the view content is swapped
+ * out, and restored later when it's swapped back in. For example:
+ *
+ * <pre>
+ * EditText reusableView = ...;
+ * ViewGroup parentView = ...;
+ * AutofillManager afm = ...;
+ *
+ * // Swap out the view and change its contents
+ * AutofillId oldId = reusableView.getAutofillId();
+ * CharSequence oldText = reusableView.getText();
+ * parentView.removeView(reusableView);
+ * AutofillId newId = afm.getNextAutofillId();
+ * reusableView.setText("New I am");
+ * reusableView.setAutofillId(newId);
+ * parentView.addView(reusableView);
+ *
+ * // Later, swap the old content back in
+ * parentView.removeView(reusableView);
+ * reusableView.setAutofillId(oldId);
+ * reusableView.setText(oldText);
+ * parentView.addView(reusableView);
+ * </pre>
+ *
+ * <p>NOTE: If this view is a descendant of an {@link android.widget.AdapterView}, the system
+ * may reset its autofill id when this view is recycled. If the autofill ids need to be stable,
+ * they should be set again in
+ * {@link android.widget.Adapter#getView(int, android.view.View, android.view.ViewGroup)}.
+ *
+ * @param id an autofill ID that is unique in the {@link android.app.Activity} hosting the view,
+ * or {@code null} to reset it. Usually it's an id previously allocated to another view (and
+ * obtained through {@link #getAutofillId()}), or a new value obtained through
+ * {@link AutofillManager#getNextAutofillId()}.
+ *
+ * @throws IllegalStateException if the view is already {@link #isAttachedToWindow() attached to
+ * a window}.
+ *
+ * @throws IllegalArgumentException if the id is an autofill id associated with a virtual view.
+ */
+ public void setAutofillId(@Nullable AutofillId id) {
+ // TODO(b/37566627): add unit / CTS test for all possible combinations below
+ if (Log.isLoggable(AUTOFILL_LOG_TAG, Log.VERBOSE)) {
+ Log.v(AUTOFILL_LOG_TAG, "setAutofill(): from " + mAutofillId + " to " + id);
+ }
+ if (isAttachedToWindow()) {
+ throw new IllegalStateException("Cannot set autofill id when view is attached");
+ }
+ if (id != null && !id.isNonVirtual()) {
+ throw new IllegalStateException("Cannot set autofill id assigned to virtual views");
+ }
+ if (id == null && (mPrivateFlags3 & PFLAG3_AUTOFILLID_EXPLICITLY_SET) == 0) {
+ // Ignore reset because it was never explicitly set before.
+ return;
+ }
+ mAutofillId = id;
+ if (id != null) {
+ mAutofillViewId = id.getViewId();
+ mPrivateFlags3 |= PFLAG3_AUTOFILLID_EXPLICITLY_SET;
+ } else {
+ mAutofillViewId = NO_ID;
+ mPrivateFlags3 &= ~PFLAG3_AUTOFILLID_EXPLICITLY_SET;
+ }
+ }
+
+ /**
+ * Forces a reset of the autofill ids of the subtree rooted at this view. Like calling
+ * {@link #setAutofillId(AutofillId) setAutofillId(null)} for each view, but works even if the
+ * views are attached to a window.
+ *
+ * <p>This is useful if the views are being recycled, since an autofill id should uniquely
+ * identify a particular piece of content.
+ *
+ * @hide
+ */
+ public void resetSubtreeAutofillIds() {
+ if (mAutofillViewId == NO_ID) {
+ return;
+ }
+ if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.VERBOSE)) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "resetAutofillId() for " + mAutofillViewId);
+ } else if (Log.isLoggable(AUTOFILL_LOG_TAG, Log.VERBOSE)) {
+ Log.v(AUTOFILL_LOG_TAG, "resetAutofillId() for " + mAutofillViewId);
+ }
+ mAutofillId = null;
+ mAutofillViewId = NO_ID;
+ mPrivateFlags3 &= ~PFLAG3_AUTOFILLID_EXPLICITLY_SET;
+ }
+
+ /**
+ * Describes the autofill type of this view, so an
+ * {@link android.service.autofill.AutofillService} can create the proper {@link AutofillValue}
+ * when autofilling the view.
+ *
+ * <p>By default returns {@link #AUTOFILL_TYPE_NONE}, but views should override it to properly
+ * support the Autofill Framework.
+ *
+ * @return either {@link #AUTOFILL_TYPE_NONE}, {@link #AUTOFILL_TYPE_TEXT},
+ * {@link #AUTOFILL_TYPE_LIST}, {@link #AUTOFILL_TYPE_DATE}, or {@link #AUTOFILL_TYPE_TOGGLE}.
+ *
+ * @see #onProvideAutofillStructure(ViewStructure, int)
+ * @see #autofill(AutofillValue)
+ */
+ public @AutofillType int getAutofillType() {
+ return AUTOFILL_TYPE_NONE;
+ }
+
+ /**
+ * Gets the hints that help an {@link android.service.autofill.AutofillService} determine how
+ * to autofill the view with the user's data.
+ *
+ * <p>See {@link #setAutofillHints(String...)} for more info about these hints.
+ *
+ * @return The hints set via the attribute or {@link #setAutofillHints(String...)}, or
+ * {@code null} if no hints were set.
+ *
+ * @attr ref android.R.styleable#View_autofillHints
+ */
+ @ViewDebug.ExportedProperty()
+ @InspectableProperty
+ @Nullable public String[] getAutofillHints() {
+ return mAutofillHints;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public boolean isAutofilled() {
+ return (mPrivateFlags3 & PFLAG3_IS_AUTOFILLED) != 0;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean hideAutofillHighlight() {
+ return (mPrivateFlags4 & PFLAG4_AUTOFILL_HIDE_HIGHLIGHT) != 0;
+ }
+
+ /**
+ * Gets the {@link View}'s current autofill value.
+ *
+ * <p>By default returns {@code null}, but subclasses should override it and return an
+ * appropriate value to properly support the Autofill Framework.
+ *
+ * @see #onProvideAutofillStructure(ViewStructure, int)
+ * @see #autofill(AutofillValue)
+ */
+ @Nullable
+ public AutofillValue getAutofillValue() {
+ return null;
+ }
+
+ /**
+ * Gets the mode for determining whether this view is important for autofill.
+ *
+ * <p>See {@link #setImportantForAutofill(int)} and {@link #isImportantForAutofill()} for more
+ * info about this mode.
+ *
+ * @return {@link #IMPORTANT_FOR_AUTOFILL_AUTO} by default, or value passed to
+ * {@link #setImportantForAutofill(int)}.
+ *
+ * @attr ref android.R.styleable#View_importantForAutofill
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_AUTOFILL_AUTO, to = "auto"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_AUTOFILL_YES, to = "yes"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_AUTOFILL_NO, to = "no"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS,
+ to = "yesExcludeDescendants"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS,
+ to = "noExcludeDescendants")})
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = IMPORTANT_FOR_AUTOFILL_AUTO, name = "auto"),
+ @EnumEntry(value = IMPORTANT_FOR_AUTOFILL_YES, name = "yes"),
+ @EnumEntry(value = IMPORTANT_FOR_AUTOFILL_NO, name = "no"),
+ @EnumEntry(value = IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS,
+ name = "yesExcludeDescendants"),
+ @EnumEntry(value = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS,
+ name = "noExcludeDescendants"),
+ })
+ public @AutofillImportance int getImportantForAutofill() {
+ return (mPrivateFlags3
+ & PFLAG3_IMPORTANT_FOR_AUTOFILL_MASK) >> PFLAG3_IMPORTANT_FOR_AUTOFILL_SHIFT;
+ }
+
+ /**
+ * Sets the mode for determining whether this view is considered important for autofill.
+ *
+ * <p>The platform determines the importance for autofill automatically but you
+ * can use this method to customize the behavior. For example:
+ *
+ * <ol>
+ * <li>When the view contents is irrelevant for autofill (for example, a text field used in a
+ * "Captcha" challenge), it should be {@link #IMPORTANT_FOR_AUTOFILL_NO}.
+ * <li>When both the view and its children are irrelevant for autofill (for example, the root
+ * view of an activity containing a spreadhseet editor), it should be
+ * {@link #IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS}.
+ * <li>When the view content is relevant for autofill but its children aren't (for example,
+ * a credit card expiration date represented by a custom view that overrides the proper
+ * autofill methods and has 2 children representing the month and year), it should
+ * be {@link #IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS}.
+ * </ol>
+ *
+ * <p><b>Note:</b> Setting the mode as {@link #IMPORTANT_FOR_AUTOFILL_NO} or
+ * {@link #IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS} does not guarantee the view (and its
+ * children) will be always be considered not important; for example, when the user explicitly
+ * makes an autofill request, all views are considered important. See
+ * {@link #isImportantForAutofill()} for more details about how the View's importance for
+ * autofill is used.
+ *
+ * @param mode {@link #IMPORTANT_FOR_AUTOFILL_AUTO}, {@link #IMPORTANT_FOR_AUTOFILL_YES},
+ * {@link #IMPORTANT_FOR_AUTOFILL_NO}, {@link #IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS},
+ * or {@link #IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS}.
+ *
+ * @attr ref android.R.styleable#View_importantForAutofill
+ */
+ public void setImportantForAutofill(@AutofillImportance int mode) {
+ mPrivateFlags3 &= ~PFLAG3_IMPORTANT_FOR_AUTOFILL_MASK;
+ mPrivateFlags3 |= (mode << PFLAG3_IMPORTANT_FOR_AUTOFILL_SHIFT)
+ & PFLAG3_IMPORTANT_FOR_AUTOFILL_MASK;
+ }
+
+ /**
+ * Hints the Android System whether the {@link android.app.assist.AssistStructure.ViewNode}
+ * associated with this view is considered important for autofill purposes.
+ *
+ * <p>Generally speaking, a view is important for autofill if:
+ * <ol>
+ * <li>The view can be autofilled by an {@link android.service.autofill.AutofillService}.
+ * <li>The view contents can help an {@link android.service.autofill.AutofillService}
+ * determine how other views can be autofilled.
+ * <ol>
+ *
+ * <p>For example, view containers should typically return {@code false} for performance reasons
+ * (since the important info is provided by their children), but if its properties have relevant
+ * information (for example, a resource id called {@code credentials}, it should return
+ * {@code true}. On the other hand, views representing labels or editable fields should
+ * typically return {@code true}, but in some cases they could return {@code false}
+ * (for example, if they're part of a "Captcha" mechanism).
+ *
+ * <p>The value returned by this method depends on the value returned by
+ * {@link #getImportantForAutofill()}:
+ *
+ * <ol>
+ * <li>if it returns {@link #IMPORTANT_FOR_AUTOFILL_YES} or
+ * {@link #IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS}, then it returns {@code true}
+ * <li>if it returns {@link #IMPORTANT_FOR_AUTOFILL_NO} or
+ * {@link #IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS}, then it returns {@code false}
+ * <li>if it returns {@link #IMPORTANT_FOR_AUTOFILL_AUTO}, then it uses some simple heuristics
+ * that can return {@code true} in some cases (like a container with a resource id),
+ * but {@code false} in most.
+ * <li>otherwise, it returns {@code false}.
+ * </ol>
+ *
+ * <p>When a view is considered important for autofill:
+ * <ul>
+ * <li>The view might automatically trigger an autofill request when focused on.
+ * <li>The contents of the view are included in the {@link ViewStructure} used in an autofill
+ * request.
+ * </ul>
+ *
+ * <p>On the other hand, when a view is considered not important for autofill:
+ * <ul>
+ * <li>The view never automatically triggers autofill requests, but it can trigger a manual
+ * request through {@link AutofillManager#requestAutofill(View)}.
+ * <li>The contents of the view are not included in the {@link ViewStructure} used in an
+ * autofill request, unless the request has the
+ * {@link #AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS} flag.
+ * </ul>
+ *
+ * @return whether the view is considered important for autofill.
+ *
+ * @see #setImportantForAutofill(int)
+ * @see #IMPORTANT_FOR_AUTOFILL_AUTO
+ * @see #IMPORTANT_FOR_AUTOFILL_YES
+ * @see #IMPORTANT_FOR_AUTOFILL_NO
+ * @see #IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS
+ * @see #IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
+ * @see AutofillManager#requestAutofill(View)
+ */
+ public final boolean isImportantForAutofill() {
+ // Check parent mode to ensure we're not hidden.
+ ViewParent parent = mParent;
+ while (parent instanceof View) {
+ final int parentImportance = ((View) parent).getImportantForAutofill();
+ if (parentImportance == IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
+ || parentImportance == IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS) {
+ if (Log.isLoggable(AUTOFILL_LOG_TAG, Log.VERBOSE)) {
+ Log.v(AUTOFILL_LOG_TAG, "View (" + this + ") is not important for autofill "
+ + "because parent " + parent + "'s importance is " + parentImportance);
+ }
+ return false;
+ }
+ parent = parent.getParent();
+ }
+
+ final int importance = getImportantForAutofill();
+
+ // First, check the explicit states.
+ if (importance == IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS
+ || importance == IMPORTANT_FOR_AUTOFILL_YES) {
+ return true;
+ }
+ if (importance == IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
+ || importance == IMPORTANT_FOR_AUTOFILL_NO) {
+ if (Log.isLoggable(AUTOFILL_LOG_TAG, Log.VERBOSE)) {
+ Log.v(AUTOFILL_LOG_TAG, "View (" + this + ") is not important for autofill "
+ + "because its importance is " + importance);
+ }
+ return false;
+ }
+
+ // Then use some heuristics to handle AUTO.
+ if (importance != IMPORTANT_FOR_AUTOFILL_AUTO) {
+ Log.w(AUTOFILL_LOG_TAG, "invalid autofill importance (" + importance + " on view "
+ + this);
+ return false;
+ }
+
+ // Always include views that have an explicit resource id.
+ final int id = mID;
+ if (id != NO_ID && !isViewIdGenerated(id)) {
+ final Resources res = getResources();
+ String entry = null;
+ String pkg = null;
+ try {
+ entry = res.getResourceEntryName(id);
+ pkg = res.getResourcePackageName(id);
+ } catch (Resources.NotFoundException e) {
+ // ignore
+ }
+ if (entry != null && pkg != null && pkg.equals(mContext.getPackageName())) {
+ return true;
+ }
+ }
+
+ // If the app developer explicitly set hints for it, it's important.
+ if (getAutofillHints() != null) {
+ return true;
+ }
+
+ // Otherwise, assume it's not important...
+ return false;
+ }
+
+ /**
+ * Gets the mode for determining whether this view is important for content capture.
+ *
+ * <p>See {@link #setImportantForContentCapture(int)} and
+ * {@link #isImportantForContentCapture()} for more info about this mode.
+ *
+ * @return {@link #IMPORTANT_FOR_CONTENT_CAPTURE_AUTO} by default, or value passed to
+ * {@link #setImportantForContentCapture(int)}.
+ *
+ * @attr ref android.R.styleable#View_importantForContentCapture
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_CONTENT_CAPTURE_AUTO, to = "auto"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_CONTENT_CAPTURE_YES, to = "yes"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_CONTENT_CAPTURE_NO, to = "no"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS,
+ to = "yesExcludeDescendants"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS,
+ to = "noExcludeDescendants")})
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = IMPORTANT_FOR_CONTENT_CAPTURE_AUTO, name = "auto"),
+ @EnumEntry(value = IMPORTANT_FOR_CONTENT_CAPTURE_YES, name = "yes"),
+ @EnumEntry(value = IMPORTANT_FOR_CONTENT_CAPTURE_NO, name = "no"),
+ @EnumEntry(value = IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS,
+ name = "yesExcludeDescendants"),
+ @EnumEntry(value = IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS,
+ name = "noExcludeDescendants"),
+ })
+ public @ContentCaptureImportance int getImportantForContentCapture() {
+ // NOTE: the important for content capture values were the first flags added and are set in
+ // the rightmost position, so we don't need to shift them
+ return mPrivateFlags4 & PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK;
+ }
+
+ /**
+ * Sets the mode for determining whether this view is considered important for content capture.
+ *
+ * <p>The platform determines the importance for autofill automatically but you
+ * can use this method to customize the behavior. Typically, a view that provides text should
+ * be marked as {@link #IMPORTANT_FOR_CONTENT_CAPTURE_YES}.
+ *
+ * @param mode {@link #IMPORTANT_FOR_CONTENT_CAPTURE_AUTO},
+ * {@link #IMPORTANT_FOR_CONTENT_CAPTURE_YES}, {@link #IMPORTANT_FOR_CONTENT_CAPTURE_NO},
+ * {@link #IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS},
+ * or {@link #IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS}.
+ *
+ * @attr ref android.R.styleable#View_importantForContentCapture
+ */
+ public void setImportantForContentCapture(@ContentCaptureImportance int mode) {
+ // Reset first
+ mPrivateFlags4 &= ~PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK;
+ // Then set again
+ // NOTE: the important for content capture values were the first flags added and are set in
+ // the rightmost position, so we don't need to shift them
+ mPrivateFlags4 |= (mode & PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK);
+ }
+
+ /**
+ * Hints the Android System whether this view is considered important for content capture, based
+ * on the value explicitly set by {@link #setImportantForContentCapture(int)} and heuristics
+ * when it's {@link #IMPORTANT_FOR_CONTENT_CAPTURE_AUTO}.
+ *
+ * <p>See {@link ContentCaptureManager} for more info about content capture.
+ *
+ * @return whether the view is considered important for content capture.
+ *
+ * @see #setImportantForContentCapture(int)
+ * @see #IMPORTANT_FOR_CONTENT_CAPTURE_AUTO
+ * @see #IMPORTANT_FOR_CONTENT_CAPTURE_YES
+ * @see #IMPORTANT_FOR_CONTENT_CAPTURE_NO
+ * @see #IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS
+ * @see #IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS
+ */
+ public final boolean isImportantForContentCapture() {
+ boolean isImportant;
+ if ((mPrivateFlags4 & PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED) != 0) {
+ isImportant = (mPrivateFlags4 & PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE) != 0;
+ return isImportant;
+ }
+
+ isImportant = calculateIsImportantForContentCapture();
+
+ mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE;
+ if (isImportant) {
+ mPrivateFlags4 |= PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE;
+ }
+ mPrivateFlags4 |= PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED;
+ return isImportant;
+ }
+
+ /**
+ * Calculates whether the flag is important for content capture so it can be used by
+ * {@link #isImportantForContentCapture()} while the tree is traversed.
+ */
+ private boolean calculateIsImportantForContentCapture() {
+ // Check parent mode to ensure we're important
+ ViewParent parent = mParent;
+ while (parent instanceof View) {
+ final int parentImportance = ((View) parent).getImportantForContentCapture();
+ if (parentImportance == IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS
+ || parentImportance == IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS) {
+ if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.VERBOSE)) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "View (" + this + ") is not important for "
+ + "content capture because parent " + parent + "'s importance is "
+ + parentImportance);
+ }
+ return false;
+ }
+ parent = parent.getParent();
+ }
+
+ final int importance = getImportantForContentCapture();
+
+ // First, check the explicit states.
+ if (importance == IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS
+ || importance == IMPORTANT_FOR_CONTENT_CAPTURE_YES) {
+ return true;
+ }
+ if (importance == IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS
+ || importance == IMPORTANT_FOR_CONTENT_CAPTURE_NO) {
+ if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.VERBOSE)) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "View (" + this + ") is not important for content "
+ + "capture because its importance is " + importance);
+ }
+ return false;
+ }
+
+ // Then use some heuristics to handle AUTO.
+ if (importance != IMPORTANT_FOR_CONTENT_CAPTURE_AUTO) {
+ Log.w(CONTENT_CAPTURE_LOG_TAG, "invalid content capture importance (" + importance
+ + " on view " + this);
+ return false;
+ }
+
+ // View group is important if at least one children also is
+ if (this instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) this;
+ for (int i = 0; i < group.getChildCount(); i++) {
+ final View child = group.getChildAt(i);
+ if (child.isImportantForContentCapture()) {
+ return true;
+ }
+ }
+ }
+
+ // If the app developer explicitly set hints or autofill hintsfor it, it's important.
+ if (getAutofillHints() != null) {
+ return true;
+ }
+
+ // Otherwise, assume it's not important...
+ return false;
+ }
+
+ /**
+ * Helper used to notify the {@link ContentCaptureManager} when the view is removed or
+ * added, based on whether it's laid out and visible, and without knowing if the parent removed
+ * it from the view hierarchy.
+ *
+ * <p>This method is called from many places (visibility changed, view laid out, view attached
+ * or detached to/from window, etc...) and hence must contain the logic to call the manager, as
+ * described below:
+ *
+ * <ol>
+ * <li>It should only be called when content capture is enabled for the view.
+ * <li>It must call viewAppeared() before viewDisappeared()
+ * <li>viewAppearead() can only be called when the view is visible and laidout
+ * <li>It should not call the same event twice.
+ * </ol>
+ */
+ private void notifyAppearedOrDisappearedForContentCaptureIfNeeded(boolean appeared) {
+ AttachInfo ai = mAttachInfo;
+ // Skip it while the view is being laid out for the first time
+ if (ai != null && !ai.mReadyForContentCaptureUpdates) return;
+
+ // First check if context has client, so it saves a service lookup when it doesn't
+ if (mContext.getContentCaptureOptions() == null) return;
+
+ if (appeared) {
+ // The appeared event stops sending to AiAi.
+ // 1. The view is hidden.
+ // 2. The same event was sent.
+ // 3. The view is not laid out, and it will be laid out in the future.
+ // Some recycled views cached its layout and a relayout is unnecessary. In this case,
+ // system still needs to notify content capture the view appeared. When a view is
+ // recycled, it will set the flag PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED.
+ final boolean isRecycledWithoutRelayout = getNotifiedContentCaptureDisappeared()
+ && getVisibility() == VISIBLE
+ && !isLayoutRequested();
+ if (getVisibility() != VISIBLE || getNotifiedContentCaptureAppeared()
+ || !(isLaidOut() || isRecycledWithoutRelayout)) {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "Ignoring 'appeared' on " + this + ": laid="
+ + isLaidOut() + ", visibleToUser=" + isVisibleToUser()
+ + ", visible=" + (getVisibility() == VISIBLE)
+ + ": alreadyNotifiedAppeared=" + getNotifiedContentCaptureAppeared()
+ + ", alreadyNotifiedDisappeared="
+ + getNotifiedContentCaptureDisappeared());
+ }
+ return;
+ }
+ } else {
+ if (!getNotifiedContentCaptureAppeared() || getNotifiedContentCaptureDisappeared()) {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "Ignoring 'disappeared' on " + this + ": laid="
+ + isLaidOut() + ", visibleToUser=" + isVisibleToUser()
+ + ", visible=" + (getVisibility() == VISIBLE)
+ + ": alreadyNotifiedAppeared=" + getNotifiedContentCaptureAppeared()
+ + ", alreadyNotifiedDisappeared="
+ + getNotifiedContentCaptureDisappeared());
+ }
+ return;
+ }
+ }
+
+ ContentCaptureSession session = getContentCaptureSession();
+ if (session == null) return;
+
+ // ... and finally at the view level
+ // NOTE: isImportantForContentCapture() is more expensive than cm.isContentCaptureEnabled()
+ if (!isImportantForContentCapture()) return;
+
+ if (appeared) {
+ setNotifiedContentCaptureAppeared();
+
+ if (ai != null) {
+ ai.delayNotifyContentCaptureEvent(session, this, appeared);
+ } else {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.w(CONTENT_CAPTURE_LOG_TAG, "no AttachInfo on appeared for " + this);
+ }
+ }
+ } else {
+ mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED;
+ mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED;
+
+ if (ai != null) {
+ ai.delayNotifyContentCaptureEvent(session, this, appeared);
+ } else {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "no AttachInfo on disappeared for " + this);
+ }
+ }
+ }
+ }
+
+ private void setNotifiedContentCaptureAppeared() {
+ mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED;
+ mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED;
+ }
+
+ /** @hide */
+ protected boolean getNotifiedContentCaptureAppeared() {
+ return (mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED) != 0;
+ }
+
+
+ private boolean getNotifiedContentCaptureDisappeared() {
+ return (mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED) != 0;
+ }
+
+ /**
+ * Sets the (optional) {@link ContentCaptureSession} associated with this view.
+ *
+ * <p>This method should be called when you need to associate a {@link ContentCaptureContext} to
+ * the content capture events associated with this view or its view hierarchy (if it's a
+ * {@link ViewGroup}).
+ *
+ * <p>For example, if your activity is associated with a web domain, first you would need to
+ * set the context for the main DOM:
+ *
+ * <pre>
+ * ContentCaptureSession mainSession = rootView.getContentCaptureSession();
+ * mainSession.setContentCaptureContext(ContentCaptureContext.forLocusId(Uri.parse(myUrl));
+ * </pre>
+ *
+ * <p>Then if the page had an {@code IFRAME}, you would create a new session for it:
+ *
+ * <pre>
+ * ContentCaptureSession iframeSession = mainSession.createContentCaptureSession(
+ * ContentCaptureContext.forLocusId(Uri.parse(iframeUrl)));
+ * iframeView.setContentCaptureSession(iframeSession);
+ * </pre>
+ *
+ * @param contentCaptureSession a session created by
+ * {@link ContentCaptureSession#createContentCaptureSession(
+ * android.view.contentcapture.ContentCaptureContext)}.
+ */
+ public void setContentCaptureSession(@Nullable ContentCaptureSession contentCaptureSession) {
+ mContentCaptureSession = contentCaptureSession;
+ }
+
+ /**
+ * Gets the session used to notify content capture events.
+ *
+ * @return session explicitly set by {@link #setContentCaptureSession(ContentCaptureSession)},
+ * inherited by ancestors, default session or {@code null} if content capture is disabled for
+ * this view.
+ */
+ @Nullable
+ public final ContentCaptureSession getContentCaptureSession() {
+ if (mContentCaptureSessionCached) {
+ return mContentCaptureSession;
+ }
+
+ mContentCaptureSession = getAndCacheContentCaptureSession();
+ mContentCaptureSessionCached = true;
+ return mContentCaptureSession;
+ }
+
+ @Nullable
+ private ContentCaptureSession getAndCacheContentCaptureSession() {
+ // First try the session explicitly set by setContentCaptureSession()
+ if (mContentCaptureSession != null) {
+ return mContentCaptureSession;
+ }
+
+ // Then the session explicitly set in an ancestor
+ ContentCaptureSession session = null;
+ if (mParent instanceof View) {
+ session = ((View) mParent).getContentCaptureSession();
+ }
+
+ // Finally, if no session was explicitly set, use the context's default session.
+ if (session == null) {
+ final ContentCaptureManager ccm = mContext
+ .getSystemService(ContentCaptureManager.class);
+ return ccm == null ? null : ccm.getMainContentCaptureSession();
+ }
+ return session;
+ }
+
+ @Nullable
+ private AutofillManager getAutofillManager() {
+ return mContext.getSystemService(AutofillManager.class);
+ }
+
+ private boolean isAutofillable() {
+ if (getAutofillType() == AUTOFILL_TYPE_NONE) return false;
+
+ if (!isImportantForAutofill()) {
+ // View is not important for "regular" autofill, so we must check if Augmented Autofill
+ // is enabled for the activity
+ final AutofillOptions options = mContext.getAutofillOptions();
+ if (options == null || !options.isAugmentedAutofillEnabled(mContext)) {
+ return false;
+ }
+ final AutofillManager afm = getAutofillManager();
+ if (afm == null) return false;
+ afm.notifyViewEnteredForAugmentedAutofill(this);
+ }
+
+ return getAutofillViewId() > LAST_APP_AUTOFILL_ID;
+ }
+
+ /** @hide */
+ public boolean canNotifyAutofillEnterExitEvent() {
+ return isAutofillable() && isAttachedToWindow();
+ }
+
+ private void populateVirtualStructure(ViewStructure structure,
+ AccessibilityNodeProvider provider, AccessibilityNodeInfo info,
+ boolean forAutofill) {
+ structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()),
+ null, null, info.getViewIdResourceName());
+ Rect rect = structure.getTempRect();
+ info.getBoundsInParent(rect);
+ structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
+ structure.setVisibility(VISIBLE);
+ structure.setEnabled(info.isEnabled());
+ if (info.isClickable()) {
+ structure.setClickable(true);
+ }
+ if (info.isFocusable()) {
+ structure.setFocusable(true);
+ }
+ if (info.isFocused()) {
+ structure.setFocused(true);
+ }
+ if (info.isAccessibilityFocused()) {
+ structure.setAccessibilityFocused(true);
+ }
+ if (info.isSelected()) {
+ structure.setSelected(true);
+ }
+ if (info.isLongClickable()) {
+ structure.setLongClickable(true);
+ }
+ if (info.isCheckable()) {
+ structure.setCheckable(true);
+ if (info.isChecked()) {
+ structure.setChecked(true);
+ }
+ }
+ if (info.isContextClickable()) {
+ structure.setContextClickable(true);
+ }
+ if (forAutofill) {
+ structure.setAutofillId(new AutofillId(getAutofillId(),
+ AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId())));
+ }
+ CharSequence cname = info.getClassName();
+ structure.setClassName(cname != null ? cname.toString() : null);
+ structure.setContentDescription(info.getContentDescription());
+ if (forAutofill) {
+ final int maxTextLength = info.getMaxTextLength();
+ if (maxTextLength != -1) {
+ structure.setMaxTextLength(maxTextLength);
+ }
+ structure.setHint(info.getHintText());
+ }
+ CharSequence text = info.getText();
+ boolean hasText = text != null || info.getError() != null;
+ if (hasText) {
+ structure.setText(text, info.getTextSelectionStart(), info.getTextSelectionEnd());
+ }
+ if (forAutofill) {
+ if (info.isEditable()) {
+ structure.setDataIsSensitive(true);
+ if (hasText) {
+ structure.setAutofillType(AUTOFILL_TYPE_TEXT);
+ structure.setAutofillValue(AutofillValue.forText(text));
+ }
+ int inputType = info.getInputType();
+ if (inputType == 0 && info.isPassword()) {
+ inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ structure.setInputType(inputType);
+ } else {
+ structure.setDataIsSensitive(false);
+ }
+ }
+ final int NCHILDREN = info.getChildCount();
+ if (NCHILDREN > 0) {
+ structure.setChildCount(NCHILDREN);
+ for (int i=0; i<NCHILDREN; i++) {
+ if (AccessibilityNodeInfo.getVirtualDescendantId(info.getChildNodeIds().get(i))
+ == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ Log.e(VIEW_LOG_TAG, "Virtual view pointing to its host. Ignoring");
+ continue;
+ }
+ AccessibilityNodeInfo cinfo = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeInfo.getVirtualDescendantId(info.getChildId(i)));
+ if (cinfo != null) {
+ ViewStructure child = structure.newChild(i);
+ populateVirtualStructure(child, provider, cinfo, forAutofill);
+ cinfo.recycle();
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispatch creation of {@link ViewStructure} down the hierarchy. The default
+ * implementation calls {@link #onProvideStructure} and
+ * {@link #onProvideVirtualStructure}.
+ */
+ public void dispatchProvideStructure(ViewStructure structure) {
+ dispatchProvideStructure(structure, VIEW_STRUCTURE_FOR_ASSIST, /* flags= */ 0);
+ }
+
+ /**
+ * Dispatches creation of a {@link ViewStructure}s for autofill purposes down the hierarchy,
+ * when an Assist structure is being created as part of an autofill request.
+ *
+ * <p>The default implementation does the following:
+ * <ul>
+ * <li>Sets the {@link AutofillId} in the structure.
+ * <li>Calls {@link #onProvideAutofillStructure(ViewStructure, int)}.
+ * <li>Calls {@link #onProvideAutofillVirtualStructure(ViewStructure, int)}.
+ * </ul>
+ *
+ * <p>Typically, this method should only be overridden by subclasses that provide a view
+ * hierarchy (such as {@link ViewGroup}) - other classes should override
+ * {@link #onProvideAutofillStructure(ViewStructure, int)} or
+ * {@link #onProvideAutofillVirtualStructure(ViewStructure, int)} instead.
+ *
+ * <p>When overridden, it must:
+ *
+ * <ul>
+ * <li>Either call
+ * {@code super.dispatchProvideAutofillStructure(structure, flags)} or explicitly
+ * set the {@link AutofillId} in the structure (for example, by calling
+ * {@code structure.setAutofillId(getAutofillId())}).
+ * <li>Decide how to handle the {@link #AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS} flag - when
+ * set, all views in the structure should be considered important for autofill,
+ * regardless of what {@link #isImportantForAutofill()} returns. We encourage you to
+ * respect this flag to provide a better user experience - this flag is typically used
+ * when an user explicitly requested autofill. If the flag is not set,
+ * then only views marked as important for autofill should be included in the
+ * structure - skipping non-important views optimizes the overall autofill performance.
+ * </ul>
+ *
+ * @param structure fill in with structured view data for autofill purposes.
+ * @param flags optional flags.
+ *
+ * @see #AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ */
+ public void dispatchProvideAutofillStructure(@NonNull ViewStructure structure,
+ @AutofillFlags int flags) {
+ dispatchProvideStructure(structure, VIEW_STRUCTURE_FOR_AUTOFILL, flags);
+ }
+
+ private void dispatchProvideStructure(@NonNull ViewStructure structure,
+ @ViewStructureType int viewFor, @AutofillFlags int flags) {
+ if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL) {
+ structure.setAutofillId(getAutofillId());
+ onProvideAutofillStructure(structure, flags);
+ onProvideAutofillVirtualStructure(structure, flags);
+ } else if (!isAssistBlocked()) {
+ onProvideStructure(structure);
+ onProvideVirtualStructure(structure);
+ } else {
+ structure.setClassName(getAccessibilityClassName().toString());
+ structure.setAssistBlocked(true);
+ }
+ }
+
+ /**
+ * Dispatches the initial content capture events for a view structure.
+ *
+ * @hide
+ */
+ public void dispatchInitialProvideContentCaptureStructure() {
+ AttachInfo ai = mAttachInfo;
+ if (ai == null) {
+ Log.w(CONTENT_CAPTURE_LOG_TAG,
+ "dispatchProvideContentCaptureStructure(): no AttachInfo for " + this);
+ return;
+ }
+ ContentCaptureManager ccm = ai.mContentCaptureManager;
+ if (ccm == null) {
+ Log.w(CONTENT_CAPTURE_LOG_TAG, "dispatchProvideContentCaptureStructure(): "
+ + "no ContentCaptureManager for " + this);
+ return;
+ }
+
+ // We must set it before checkign if the view itself is important, because it might
+ // initially not be (for example, if it's empty), although that might change later (for
+ // example, if important views are added)
+ ai.mReadyForContentCaptureUpdates = true;
+
+ if (!isImportantForContentCapture()) {
+ if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.DEBUG)) {
+ Log.d(CONTENT_CAPTURE_LOG_TAG,
+ "dispatchProvideContentCaptureStructure(): decorView is not important");
+ }
+ return;
+ }
+
+ ai.mContentCaptureManager = ccm;
+
+ ContentCaptureSession session = getContentCaptureSession();
+ if (session == null) {
+ if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.DEBUG)) {
+ Log.d(CONTENT_CAPTURE_LOG_TAG,
+ "dispatchProvideContentCaptureStructure(): no session for " + this);
+ }
+ return;
+ }
+
+ session.internalNotifyViewTreeEvent(/* started= */ true);
+ try {
+ dispatchProvideContentCaptureStructure();
+ } finally {
+ session.internalNotifyViewTreeEvent(/* started= */ false);
+ }
+ }
+
+ /** @hide */
+ void dispatchProvideContentCaptureStructure() {
+ ContentCaptureSession session = getContentCaptureSession();
+ if (session != null) {
+ ViewStructure structure = session.newViewStructure(this);
+ onProvideContentCaptureStructure(structure, /* flags= */ 0);
+ setNotifiedContentCaptureAppeared();
+ session.notifyViewAppeared(structure);
+ }
+ }
+
+ /**
+ * @see #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ if (mAttachInfo == null) {
+ return;
+ }
+
+ Rect bounds = mAttachInfo.mTmpInvalRect;
+
+ getDrawingRect(bounds);
+ info.setBoundsInParent(bounds);
+
+ getBoundsOnScreen(bounds, true);
+ info.setBoundsInScreen(bounds);
+
+ ViewParent parent = getParentForAccessibility();
+ if (parent instanceof View) {
+ info.setParent((View) parent);
+ }
+
+ if (mID != View.NO_ID) {
+ View rootView = getRootView();
+ if (rootView == null) {
+ rootView = this;
+ }
+
+ View label = rootView.findLabelForView(this, mID);
+ if (label != null) {
+ info.setLabeledBy(label);
+ }
+
+ if ((mAttachInfo.mAccessibilityFetchFlags
+ & AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS) != 0
+ && Resources.resourceHasPackage(mID)) {
+ try {
+ String viewId = getResources().getResourceName(mID);
+ info.setViewIdResourceName(viewId);
+ } catch (Resources.NotFoundException nfe) {
+ /* ignore */
+ }
+ }
+ }
+
+ if (mLabelForId != View.NO_ID) {
+ View rootView = getRootView();
+ if (rootView == null) {
+ rootView = this;
+ }
+ View labeled = rootView.findViewInsideOutShouldExist(this, mLabelForId);
+ if (labeled != null) {
+ info.setLabelFor(labeled);
+ }
+ }
+
+ if (mAccessibilityTraversalBeforeId != View.NO_ID) {
+ View rootView = getRootView();
+ if (rootView == null) {
+ rootView = this;
+ }
+ View next = rootView.findViewInsideOutShouldExist(this,
+ mAccessibilityTraversalBeforeId);
+ if (next != null && next.includeForAccessibility()) {
+ info.setTraversalBefore(next);
+ }
+ }
+
+ if (mAccessibilityTraversalAfterId != View.NO_ID) {
+ View rootView = getRootView();
+ if (rootView == null) {
+ rootView = this;
+ }
+ View next = rootView.findViewInsideOutShouldExist(this,
+ mAccessibilityTraversalAfterId);
+ if (next != null && next.includeForAccessibility()) {
+ info.setTraversalAfter(next);
+ }
+ }
+
+ info.setVisibleToUser(isVisibleToUser());
+
+ info.setImportantForAccessibility(isImportantForAccessibility());
+ info.setPackageName(mContext.getPackageName());
+ info.setClassName(getAccessibilityClassName());
+ info.setStateDescription(getStateDescription());
+ info.setContentDescription(getContentDescription());
+
+ info.setEnabled(isEnabled());
+ info.setClickable(isClickable());
+ info.setFocusable(isFocusable());
+ info.setScreenReaderFocusable(isScreenReaderFocusable());
+ info.setFocused(isFocused());
+ info.setAccessibilityFocused(isAccessibilityFocused());
+ info.setSelected(isSelected());
+ info.setLongClickable(isLongClickable());
+ info.setContextClickable(isContextClickable());
+ info.setLiveRegion(getAccessibilityLiveRegion());
+ if ((mTooltipInfo != null) && (mTooltipInfo.mTooltipText != null)) {
+ info.setTooltipText(mTooltipInfo.mTooltipText);
+ info.addAction((mTooltipInfo.mTooltipPopup == null)
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TOOLTIP
+ : AccessibilityNodeInfo.AccessibilityAction.ACTION_HIDE_TOOLTIP);
+ }
+
+ // TODO: These make sense only if we are in an AdapterView but all
+ // views can be selected. Maybe from accessibility perspective
+ // we should report as selectable view in an AdapterView.
+ info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
+
+ if (isFocusable()) {
+ if (isFocused()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
+ } else {
+ info.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
+ }
+ }
+
+ if (!isAccessibilityFocused()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ } else {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+
+ if (isClickable() && isEnabled()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ }
+
+ if (isLongClickable() && isEnabled()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ }
+
+ if (isContextClickable() && isEnabled()) {
+ info.addAction(AccessibilityAction.ACTION_CONTEXT_CLICK);
+ }
+
+ CharSequence text = getIterableTextForAccessibility();
+ if (text != null && text.length() > 0) {
+ info.setTextSelection(getAccessibilitySelectionStart(), getAccessibilitySelectionEnd());
+
+ info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+ info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
+ | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
+ | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
+ }
+
+ info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN);
+ populateAccessibilityNodeInfoDrawingOrderInParent(info);
+ info.setPaneTitle(mAccessibilityPaneTitle);
+ info.setHeading(isAccessibilityHeading());
+
+ if (mTouchDelegate != null) {
+ info.setTouchDelegateInfo(mTouchDelegate.getTouchDelegateInfo());
+ }
+ }
+
+ /**
+ * Adds extra data to an {@link AccessibilityNodeInfo} based on an explicit request for the
+ * additional data.
+ * <p>
+ * This method only needs overloading if the node is marked as having extra data available.
+ * </p>
+ *
+ * @param info The info to which to add the extra data. Never {@code null}.
+ * @param extraDataKey A key specifying the type of extra data to add to the info. The
+ * extra data should be added to the {@link Bundle} returned by
+ * the info's {@link AccessibilityNodeInfo#getExtras} method. Never
+ * {@code null}.
+ * @param arguments A {@link Bundle} holding any arguments relevant for this request. May be
+ * {@code null} if the service provided no arguments.
+ *
+ * @see AccessibilityNodeInfo#setAvailableExtraData(List)
+ */
+ public void addExtraDataToAccessibilityNodeInfo(
+ @NonNull AccessibilityNodeInfo info, @NonNull String extraDataKey,
+ @Nullable Bundle arguments) {
+ }
+
+ /**
+ * Determine the order in which this view will be drawn relative to its siblings for a11y
+ *
+ * @param info The info whose drawing order should be populated
+ */
+ private void populateAccessibilityNodeInfoDrawingOrderInParent(AccessibilityNodeInfo info) {
+ /*
+ * If the view's bounds haven't been set yet, layout has not completed. In that situation,
+ * drawing order may not be well-defined, and some Views with custom drawing order may
+ * not be initialized sufficiently to respond properly getChildDrawingOrder.
+ */
+ if ((mPrivateFlags & PFLAG_HAS_BOUNDS) == 0) {
+ info.setDrawingOrder(0);
+ return;
+ }
+ int drawingOrderInParent = 1;
+ // Iterate up the hierarchy if parents are not important for a11y
+ View viewAtDrawingLevel = this;
+ final ViewParent parent = getParentForAccessibility();
+ while (viewAtDrawingLevel != parent) {
+ final ViewParent currentParent = viewAtDrawingLevel.getParent();
+ if (!(currentParent instanceof ViewGroup)) {
+ // Should only happen for the Decor
+ drawingOrderInParent = 0;
+ break;
+ } else {
+ final ViewGroup parentGroup = (ViewGroup) currentParent;
+ final int childCount = parentGroup.getChildCount();
+ if (childCount > 1) {
+ List<View> preorderedList = parentGroup.buildOrderedChildList();
+ if (preorderedList != null) {
+ final int childDrawIndex = preorderedList.indexOf(viewAtDrawingLevel);
+ for (int i = 0; i < childDrawIndex; i++) {
+ drawingOrderInParent += numViewsForAccessibility(preorderedList.get(i));
+ }
+ preorderedList.clear();
+ } else {
+ final int childIndex = parentGroup.indexOfChild(viewAtDrawingLevel);
+ final boolean customOrder = parentGroup.isChildrenDrawingOrderEnabled();
+ final int childDrawIndex = ((childIndex >= 0) && customOrder) ? parentGroup
+ .getChildDrawingOrder(childCount, childIndex) : childIndex;
+ final int numChildrenToIterate = customOrder ? childCount : childDrawIndex;
+ if (childDrawIndex != 0) {
+ for (int i = 0; i < numChildrenToIterate; i++) {
+ final int otherDrawIndex = (customOrder ?
+ parentGroup.getChildDrawingOrder(childCount, i) : i);
+ if (otherDrawIndex < childDrawIndex) {
+ drawingOrderInParent +=
+ numViewsForAccessibility(parentGroup.getChildAt(i));
+ }
+ }
+ }
+ }
+ }
+ }
+ viewAtDrawingLevel = (View) currentParent;
+ }
+ info.setDrawingOrder(drawingOrderInParent);
+ }
+
+ private static int numViewsForAccessibility(View view) {
+ if (view != null) {
+ if (view.includeForAccessibility()) {
+ return 1;
+ } else if (view instanceof ViewGroup) {
+ return ((ViewGroup) view).getNumChildrenForAccessibility();
+ }
+ }
+ return 0;
+ }
+
+ private View findLabelForView(View view, int labeledId) {
+ if (mMatchLabelForPredicate == null) {
+ mMatchLabelForPredicate = new MatchLabelForPredicate();
+ }
+ mMatchLabelForPredicate.mLabeledId = labeledId;
+ return findViewByPredicateInsideOut(view, mMatchLabelForPredicate);
+ }
+
+ /**
+ * Computes whether this virtual autofill view is visible to the user.
+ *
+ * <p><b>Note: </b>By default it returns {@code true}, but views providing a virtual hierarchy
+ * view must override it.
+ *
+ * @return Whether the view is visible on the screen.
+ */
+ public boolean isVisibleToUserForAutofill(int virtualId) {
+ if (mContext.isAutofillCompatibilityEnabled()) {
+ final AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider != null) {
+ final AccessibilityNodeInfo node = provider.createAccessibilityNodeInfo(virtualId);
+ if (node != null) {
+ return node.isVisibleToUser();
+ }
+ // if node is null, assume it's not visible anymore
+ } else {
+ Log.w(VIEW_LOG_TAG, "isVisibleToUserForAutofill(" + virtualId + "): no provider");
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Computes whether this view is visible to the user. Such a view is
+ * attached, visible, all its predecessors are visible, it is not clipped
+ * entirely by its predecessors, and has an alpha greater than zero.
+ *
+ * @return Whether the view is visible on the screen.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean isVisibleToUser() {
+ return isVisibleToUser(null);
+ }
+
+ /**
+ * Computes whether the given portion of this view is visible to the user.
+ * Such a view is attached, visible, all its predecessors are visible,
+ * has an alpha greater than zero, and the specified portion is not
+ * clipped entirely by its predecessors.
+ *
+ * @param boundInView the portion of the view to test; coordinates should be relative; may be
+ * <code>null</code>, and the entire view will be tested in this case.
+ * When <code>true</code> is returned by the function, the actual visible
+ * region will be stored in this parameter; that is, if boundInView is fully
+ * contained within the view, no modification will be made, otherwise regions
+ * outside of the visible area of the view will be clipped.
+ *
+ * @return Whether the specified portion of the view is visible on the screen.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(trackingBug = 171933273)
+ protected boolean isVisibleToUser(Rect boundInView) {
+ if (mAttachInfo != null) {
+ // Attached to invisible window means this view is not visible.
+ if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
+ return false;
+ }
+ // An invisible predecessor or one with alpha zero means
+ // that this view is not visible to the user.
+ Object current = this;
+ while (current instanceof View) {
+ View view = (View) current;
+ // We have attach info so this view is attached and there is no
+ // need to check whether we reach to ViewRootImpl on the way up.
+ if (view.getAlpha() <= 0 || view.getTransitionAlpha() <= 0 ||
+ view.getVisibility() != VISIBLE) {
+ return false;
+ }
+ current = view.mParent;
+ }
+ // Check if the view is entirely covered by its predecessors.
+ Rect visibleRect = mAttachInfo.mTmpInvalRect;
+ Point offset = mAttachInfo.mPoint;
+ if (!getGlobalVisibleRect(visibleRect, offset)) {
+ return false;
+ }
+ // Check if the visible portion intersects the rectangle of interest.
+ if (boundInView != null) {
+ visibleRect.offset(-offset.x, -offset.y);
+ return boundInView.intersect(visibleRect);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the delegate for implementing accessibility support via
+ * composition. For more details see {@link AccessibilityDelegate}.
+ *
+ * @return The delegate, or null if none set.
+ */
+ public AccessibilityDelegate getAccessibilityDelegate() {
+ return mAccessibilityDelegate;
+ }
+
+ /**
+ * Sets a delegate for implementing accessibility support via composition
+ * (as opposed to inheritance). For more details, see
+ * {@link AccessibilityDelegate}.
+ * <p>
+ * <strong>Note:</strong> On platform versions prior to
+ * {@link android.os.Build.VERSION_CODES#M API 23}, delegate methods on
+ * views in the {@code android.widget.*} package are called <i>before</i>
+ * host methods. This prevents certain properties such as class name from
+ * being modified by overriding
+ * {@link AccessibilityDelegate#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfo)},
+ * as any changes will be overwritten by the host class.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#M API 23}, delegate
+ * methods are called <i>after</i> host methods, which all properties to be
+ * modified without being overwritten by the host class.
+ *
+ * @param delegate the object to which accessibility method calls should be
+ * delegated
+ * @see AccessibilityDelegate
+ */
+ public void setAccessibilityDelegate(@Nullable AccessibilityDelegate delegate) {
+ mAccessibilityDelegate = delegate;
+ }
+
+ /**
+ * Gets the provider for managing a virtual view hierarchy rooted at this View
+ * and reported to {@link android.accessibilityservice.AccessibilityService}s
+ * that explore the window content.
+ * <p>
+ * If this method returns an instance, this instance is responsible for managing
+ * {@link AccessibilityNodeInfo}s describing the virtual sub-tree rooted at this
+ * View including the one representing the View itself. Similarly the returned
+ * instance is responsible for performing accessibility actions on any virtual
+ * view or the root view itself.
+ * </p>
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#getAccessibilityNodeProvider(View)}
+ * is responsible for handling this call.
+ * </p>
+ *
+ * @return The provider.
+ *
+ * @see AccessibilityNodeProvider
+ */
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ if (mAccessibilityDelegate != null) {
+ return mAccessibilityDelegate.getAccessibilityNodeProvider(this);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the unique identifier of this view on the screen for accessibility purposes.
+ *
+ * @return The view accessibility id.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getAccessibilityViewId() {
+ if (mAccessibilityViewId == NO_ID) {
+ mAccessibilityViewId = sNextAccessibilityViewId++;
+ }
+ return mAccessibilityViewId;
+ }
+
+ /**
+ * Gets the unique identifier of this view on the screen for autofill purposes.
+ *
+ * @return The view autofill id.
+ *
+ * @hide
+ */
+ public int getAutofillViewId() {
+ if (mAutofillViewId == NO_ID) {
+ mAutofillViewId = mContext.getNextAutofillId();
+ }
+ return mAutofillViewId;
+ }
+
+ /**
+ * Gets the unique identifier of the window in which this View resides.
+ *
+ * @return The window accessibility id.
+ *
+ * @hide
+ */
+ public int getAccessibilityWindowId() {
+ return mAttachInfo != null ? mAttachInfo.mAccessibilityWindowId
+ : AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ }
+
+ /**
+ * Returns the {@link View}'s state description.
+ * <p>
+ * <strong>Note:</strong> Do not override this method, as it will have no
+ * effect on the state description presented to accessibility services.
+ * You must call {@link #setStateDescription(CharSequence)} to modify the
+ * state description.
+ *
+ * @return the state description
+ * @see #setStateDescription(CharSequence)
+ */
+ @ViewDebug.ExportedProperty(category = "accessibility")
+ public final @Nullable CharSequence getStateDescription() {
+ return mStateDescription;
+ }
+
+ /**
+ * Returns the {@link View}'s content description.
+ * <p>
+ * <strong>Note:</strong> Do not override this method, as it will have no
+ * effect on the content description presented to accessibility services.
+ * You must call {@link #setContentDescription(CharSequence)} to modify the
+ * content description.
+ *
+ * @return the content description
+ * @see #setContentDescription(CharSequence)
+ * @attr ref android.R.styleable#View_contentDescription
+ */
+ @ViewDebug.ExportedProperty(category = "accessibility")
+ @InspectableProperty
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * Sets the {@link View}'s state description.
+ * <p>
+ * A state description briefly describes the states of the view and is primarily used
+ * for accessibility support to determine how the states of a view should be presented to
+ * the user. It is a supplement to the boolean states (for example, checked/unchecked) and
+ * it is used for customized state description (for example, "wifi, connected, three bars").
+ * State description changes frequently while content description should change less often.
+ * State description should be localized. For android widgets which have default state
+ * descriptions, app developers can call this method to override the state descriptions.
+ * Setting state description to null restores the default behavior.
+ *
+ * @param stateDescription The state description.
+ * @see #getStateDescription()
+ */
+ @RemotableViewMethod
+ public void setStateDescription(@Nullable CharSequence stateDescription) {
+ if (mStateDescription == null) {
+ if (stateDescription == null) {
+ return;
+ }
+ } else if (mStateDescription.equals(stateDescription)) {
+ return;
+ }
+ mStateDescription = stateDescription;
+ if (!TextUtils.isEmpty(stateDescription)
+ && getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION);
+ sendAccessibilityEventUnchecked(event);
+ }
+ }
+
+ /**
+ * Sets the {@link View}'s content description.
+ * <p>
+ * A content description briefly describes the view and is primarily used
+ * for accessibility support to determine how a view should be presented to
+ * the user. In the case of a view with no textual representation, such as
+ * {@link android.widget.ImageButton}, a useful content description
+ * explains what the view does. For example, an image button with a phone
+ * icon that is used to place a call may use "Call" as its content
+ * description. An image of a floppy disk that is used to save a file may
+ * use "Save".
+ *
+ * @param contentDescription The content description.
+ * @see #getContentDescription()
+ * @attr ref android.R.styleable#View_contentDescription
+ */
+ @RemotableViewMethod
+ public void setContentDescription(CharSequence contentDescription) {
+ if (mContentDescription == null) {
+ if (contentDescription == null) {
+ return;
+ }
+ } else if (mContentDescription.equals(contentDescription)) {
+ return;
+ }
+ mContentDescription = contentDescription;
+ final boolean nonEmptyDesc = contentDescription != null && contentDescription.length() > 0;
+ if (nonEmptyDesc && getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ } else {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION);
+ }
+ }
+
+ /**
+ * Sets the id of a view before which this one is visited in accessibility traversal.
+ * A screen-reader must visit the content of this view before the content of the one
+ * it precedes. For example, if view B is set to be before view A, then a screen-reader
+ * will traverse the entire content of B before traversing the entire content of A,
+ * regardles of what traversal strategy it is using.
+ * <p>
+ * Views that do not have specified before/after relationships are traversed in order
+ * determined by the screen-reader.
+ * </p>
+ * <p>
+ * Setting that this view is before a view that is not important for accessibility
+ * or if this view is not important for accessibility will have no effect as the
+ * screen-reader is not aware of unimportant views.
+ * </p>
+ *
+ * @param beforeId The id of a view this one precedes in accessibility traversal.
+ *
+ * @attr ref android.R.styleable#View_accessibilityTraversalBefore
+ *
+ * @see #setImportantForAccessibility(int)
+ */
+ @RemotableViewMethod
+ public void setAccessibilityTraversalBefore(@IdRes int beforeId) {
+ if (mAccessibilityTraversalBeforeId == beforeId) {
+ return;
+ }
+ mAccessibilityTraversalBeforeId = beforeId;
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ /**
+ * Gets the id of a view before which this one is visited in accessibility traversal.
+ *
+ * @return The id of a view this one precedes in accessibility traversal if
+ * specified, otherwise {@link #NO_ID}.
+ *
+ * @see #setAccessibilityTraversalBefore(int)
+ */
+ @IdRes
+ @InspectableProperty
+ public int getAccessibilityTraversalBefore() {
+ return mAccessibilityTraversalBeforeId;
+ }
+
+ /**
+ * Sets the id of a view after which this one is visited in accessibility traversal.
+ * A screen-reader must visit the content of the other view before the content of this
+ * one. For example, if view B is set to be after view A, then a screen-reader
+ * will traverse the entire content of A before traversing the entire content of B,
+ * regardles of what traversal strategy it is using.
+ * <p>
+ * Views that do not have specified before/after relationships are traversed in order
+ * determined by the screen-reader.
+ * </p>
+ * <p>
+ * Setting that this view is after a view that is not important for accessibility
+ * or if this view is not important for accessibility will have no effect as the
+ * screen-reader is not aware of unimportant views.
+ * </p>
+ *
+ * @param afterId The id of a view this one succedees in accessibility traversal.
+ *
+ * @attr ref android.R.styleable#View_accessibilityTraversalAfter
+ *
+ * @see #setImportantForAccessibility(int)
+ */
+ @RemotableViewMethod
+ public void setAccessibilityTraversalAfter(@IdRes int afterId) {
+ if (mAccessibilityTraversalAfterId == afterId) {
+ return;
+ }
+ mAccessibilityTraversalAfterId = afterId;
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ /**
+ * Gets the id of a view after which this one is visited in accessibility traversal.
+ *
+ * @return The id of a view this one succeedes in accessibility traversal if
+ * specified, otherwise {@link #NO_ID}.
+ *
+ * @see #setAccessibilityTraversalAfter(int)
+ */
+ @IdRes
+ @InspectableProperty
+ public int getAccessibilityTraversalAfter() {
+ return mAccessibilityTraversalAfterId;
+ }
+
+ /**
+ * Gets the id of a view for which this view serves as a label for
+ * accessibility purposes.
+ *
+ * @return The labeled view id.
+ */
+ @IdRes
+ @ViewDebug.ExportedProperty(category = "accessibility")
+ @InspectableProperty
+ public int getLabelFor() {
+ return mLabelForId;
+ }
+
+ /**
+ * Sets the id of a view for which this view serves as a label for
+ * accessibility purposes.
+ *
+ * @param id The labeled view id.
+ */
+ @RemotableViewMethod
+ public void setLabelFor(@IdRes int id) {
+ if (mLabelForId == id) {
+ return;
+ }
+ mLabelForId = id;
+ if (mLabelForId != View.NO_ID
+ && mID == View.NO_ID) {
+ mID = generateViewId();
+ }
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ /**
+ * Invoked whenever this view loses focus, either by losing window focus or by losing
+ * focus within its window. This method can be used to clear any state tied to the
+ * focus. For instance, if a button is held pressed with the trackball and the window
+ * loses focus, this method can be used to cancel the press.
+ *
+ * Subclasses of View overriding this method should always call super.onFocusLost().
+ *
+ * @see #onFocusChanged(boolean, int, android.graphics.Rect)
+ * @see #onWindowFocusChanged(boolean)
+ *
+ * @hide pending API council approval
+ */
+ @CallSuper
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void onFocusLost() {
+ resetPressedState();
+ }
+
+ private void resetPressedState() {
+ if ((mViewFlags & ENABLED_MASK) == DISABLED) {
+ return;
+ }
+
+ if (isPressed()) {
+ setPressed(false);
+
+ if (!mHasPerformedLongPress) {
+ removeLongPressCallback();
+ }
+ }
+ }
+
+ /**
+ * Returns true if this view has focus
+ *
+ * @return True if this view has focus, false otherwise.
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ @InspectableProperty(hasAttributeId = false)
+ public boolean isFocused() {
+ return (mPrivateFlags & PFLAG_FOCUSED) != 0;
+ }
+
+ /**
+ * Find the view in the hierarchy rooted at this view that currently has
+ * focus.
+ *
+ * @return The view that currently has focus, or null if no focused view can
+ * be found.
+ */
+ public View findFocus() {
+ return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
+ }
+
+ /**
+ * Indicates whether this view is one of the set of scrollable containers in
+ * its window.
+ *
+ * @return whether this view is one of the set of scrollable containers in
+ * its window
+ *
+ * @attr ref android.R.styleable#View_isScrollContainer
+ */
+ @InspectableProperty(name = "isScrollContainer")
+ public boolean isScrollContainer() {
+ return (mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0;
+ }
+
+ /**
+ * Change whether this view is one of the set of scrollable containers in
+ * its window. This will be used to determine whether the window can
+ * resize or must pan when a soft input area is open -- scrollable
+ * containers allow the window to use resize mode since the container
+ * will appropriately shrink.
+ *
+ * @attr ref android.R.styleable#View_isScrollContainer
+ */
+ public void setScrollContainer(boolean isScrollContainer) {
+ if (isScrollContainer) {
+ if (mAttachInfo != null && (mPrivateFlags&PFLAG_SCROLL_CONTAINER_ADDED) == 0) {
+ mAttachInfo.mScrollContainers.add(this);
+ mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
+ }
+ mPrivateFlags |= PFLAG_SCROLL_CONTAINER;
+ } else {
+ if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
+ mAttachInfo.mScrollContainers.remove(this);
+ }
+ mPrivateFlags &= ~(PFLAG_SCROLL_CONTAINER|PFLAG_SCROLL_CONTAINER_ADDED);
+ }
+ }
+
+ /**
+ * Returns the quality of the drawing cache.
+ *
+ * @return One of {@link #DRAWING_CACHE_QUALITY_AUTO},
+ * {@link #DRAWING_CACHE_QUALITY_LOW}, or {@link #DRAWING_CACHE_QUALITY_HIGH}
+ *
+ * @see #setDrawingCacheQuality(int)
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #isDrawingCacheEnabled()
+ *
+ * @attr ref android.R.styleable#View_drawingCacheQuality
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ @DrawingCacheQuality
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = DRAWING_CACHE_QUALITY_LOW, name = "low"),
+ @EnumEntry(value = DRAWING_CACHE_QUALITY_HIGH, name = "high"),
+ @EnumEntry(value = DRAWING_CACHE_QUALITY_AUTO, name = "auto")
+ })
+ public int getDrawingCacheQuality() {
+ return mViewFlags & DRAWING_CACHE_QUALITY_MASK;
+ }
+
+ /**
+ * Set the drawing cache quality of this view. This value is used only when the
+ * drawing cache is enabled
+ *
+ * @param quality One of {@link #DRAWING_CACHE_QUALITY_AUTO},
+ * {@link #DRAWING_CACHE_QUALITY_LOW}, or {@link #DRAWING_CACHE_QUALITY_HIGH}
+ *
+ * @see #getDrawingCacheQuality()
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #isDrawingCacheEnabled()
+ *
+ * @attr ref android.R.styleable#View_drawingCacheQuality
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void setDrawingCacheQuality(@DrawingCacheQuality int quality) {
+ setFlags(quality, DRAWING_CACHE_QUALITY_MASK);
+ }
+
+ /**
+ * Returns whether the screen should remain on, corresponding to the current
+ * value of {@link #KEEP_SCREEN_ON}.
+ *
+ * @return Returns true if {@link #KEEP_SCREEN_ON} is set.
+ *
+ * @see #setKeepScreenOn(boolean)
+ *
+ * @attr ref android.R.styleable#View_keepScreenOn
+ */
+ @InspectableProperty
+ public boolean getKeepScreenOn() {
+ return (mViewFlags & KEEP_SCREEN_ON) != 0;
+ }
+
+ /**
+ * Controls whether the screen should remain on, modifying the
+ * value of {@link #KEEP_SCREEN_ON}.
+ *
+ * @param keepScreenOn Supply true to set {@link #KEEP_SCREEN_ON}.
+ *
+ * @see #getKeepScreenOn()
+ *
+ * @attr ref android.R.styleable#View_keepScreenOn
+ */
+ public void setKeepScreenOn(boolean keepScreenOn) {
+ setFlags(keepScreenOn ? KEEP_SCREEN_ON : 0, KEEP_SCREEN_ON);
+ }
+
+ /**
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusLeft
+ */
+ @IdRes
+ @InspectableProperty(name = "nextFocusLeft")
+ public int getNextFocusLeftId() {
+ return mNextFocusLeftId;
+ }
+
+ /**
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
+ * @param nextFocusLeftId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusLeft
+ */
+ public void setNextFocusLeftId(@IdRes int nextFocusLeftId) {
+ mNextFocusLeftId = nextFocusLeftId;
+ }
+
+ /**
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusRight
+ */
+ @IdRes
+ @InspectableProperty(name = "nextFocusRight")
+ public int getNextFocusRightId() {
+ return mNextFocusRightId;
+ }
+
+ /**
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
+ * @param nextFocusRightId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusRight
+ */
+ public void setNextFocusRightId(@IdRes int nextFocusRightId) {
+ mNextFocusRightId = nextFocusRightId;
+ }
+
+ /**
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_UP}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusUp
+ */
+ @IdRes
+ @InspectableProperty(name = "nextFocusUp")
+ public int getNextFocusUpId() {
+ return mNextFocusUpId;
+ }
+
+ /**
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_UP}.
+ * @param nextFocusUpId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusUp
+ */
+ public void setNextFocusUpId(@IdRes int nextFocusUpId) {
+ mNextFocusUpId = nextFocusUpId;
+ }
+
+ /**
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusDown
+ */
+ @IdRes
+ @InspectableProperty(name = "nextFocusDown")
+ public int getNextFocusDownId() {
+ return mNextFocusDownId;
+ }
+
+ /**
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
+ * @param nextFocusDownId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusDown
+ */
+ public void setNextFocusDownId(@IdRes int nextFocusDownId) {
+ mNextFocusDownId = nextFocusDownId;
+ }
+
+ /**
+ * Gets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
+ * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusForward
+ */
+ @IdRes
+ @InspectableProperty(name = "nextFocusForward")
+ public int getNextFocusForwardId() {
+ return mNextFocusForwardId;
+ }
+
+ /**
+ * Sets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
+ * @param nextFocusForwardId The next focus ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextFocusForward
+ */
+ public void setNextFocusForwardId(@IdRes int nextFocusForwardId) {
+ mNextFocusForwardId = nextFocusForwardId;
+ }
+
+ /**
+ * Gets the id of the root of the next keyboard navigation cluster.
+ * @return The next keyboard navigation cluster ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextClusterForward
+ */
+ @IdRes
+ @InspectableProperty(name = "nextClusterForward")
+ public int getNextClusterForwardId() {
+ return mNextClusterForwardId;
+ }
+
+ /**
+ * Sets the id of the view to use as the root of the next keyboard navigation cluster.
+ * @param nextClusterForwardId The next cluster ID, or {@link #NO_ID} if the framework should
+ * decide automatically.
+ *
+ * @attr ref android.R.styleable#View_nextClusterForward
+ */
+ public void setNextClusterForwardId(@IdRes int nextClusterForwardId) {
+ mNextClusterForwardId = nextClusterForwardId;
+ }
+
+ /**
+ * Returns the visibility of this view and all of its ancestors
+ *
+ * @return True if this view and all of its ancestors are {@link #VISIBLE}
+ */
+ public boolean isShown() {
+ View current = this;
+ //noinspection ConstantConditions
+ do {
+ if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+ ViewParent parent = current.mParent;
+ if (parent == null) {
+ return false; // We are not attached to the view root
+ }
+ if (!(parent instanceof View)) {
+ return true;
+ }
+ current = (View) parent;
+ } while (current != null);
+
+ return false;
+ }
+
+ private boolean detached() {
+ View current = this;
+ //noinspection ConstantConditions
+ do {
+ if ((current.mPrivateFlags4 & PFLAG4_DETACHED) != 0) {
+ return true;
+ }
+ ViewParent parent = current.mParent;
+ if (parent == null) {
+ return false;
+ }
+ if (!(parent instanceof View)) {
+ return false;
+ }
+ current = (View) parent;
+ } while (current != null);
+
+ return false;
+ }
+
+ /**
+ * Called by the view hierarchy when the content insets for a window have
+ * changed, to allow it to adjust its content to fit within those windows.
+ * The content insets tell you the space that the status bar, input method,
+ * and other system windows infringe on the application's window.
+ *
+ * <p>You do not normally need to deal with this function, since the default
+ * window decoration given to applications takes care of applying it to the
+ * content of the window. If you use {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}
+ * or {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION} this will not be the case,
+ * and your content can be placed under those system elements. You can then
+ * use this method within your view hierarchy if you have parts of your UI
+ * which you would like to ensure are not being covered.
+ *
+ * <p>The default implementation of this method simply applies the content
+ * insets to the view's padding, consuming that content (modifying the
+ * insets to be 0), and returning true. This behavior is off by default, but can
+ * be enabled through {@link #setFitsSystemWindows(boolean)}.
+ *
+ * <p>This function's traversal down the hierarchy is depth-first. The same content
+ * insets object is propagated down the hierarchy, so any changes made to it will
+ * be seen by all following views (including potentially ones above in
+ * the hierarchy since this is a depth-first traversal). The first view
+ * that returns true will abort the entire traversal.
+ *
+ * <p>The default implementation works well for a situation where it is
+ * used with a container that covers the entire window, allowing it to
+ * apply the appropriate insets to its content on all edges. If you need
+ * a more complicated layout (such as two different views fitting system
+ * windows, one on the top of the window, and one on the bottom),
+ * you can override the method and handle the insets however you would like.
+ * Note that the insets provided by the framework are always relative to the
+ * far edges of the window, not accounting for the location of the called view
+ * within that window. (In fact when this method is called you do not yet know
+ * where the layout will place the view, as it is done before layout happens.)
+ *
+ * <p>Note: unlike many View methods, there is no dispatch phase to this
+ * call. If you are overriding it in a ViewGroup and want to allow the
+ * call to continue to your children, you must be sure to call the super
+ * implementation.
+ *
+ * <p>Here is a sample layout that makes use of fitting system windows
+ * to have controls for a video view placed inside of the window decorations
+ * that it hides and shows. This can be used with code like the second
+ * sample (video player) shown in {@link #setSystemUiVisibility(int)}.
+ *
+ * {@sample development/samples/ApiDemos/res/layout/video_player.xml complete}
+ *
+ * @param insets Current content insets of the window. Prior to
+ * {@link android.os.Build.VERSION_CODES#JELLY_BEAN} you must not modify
+ * the insets or else you and Android will be unhappy.
+ *
+ * @return {@code true} if this view applied the insets and it should not
+ * continue propagating further down the hierarchy, {@code false} otherwise.
+ * @see #getFitsSystemWindows()
+ * @see #setFitsSystemWindows(boolean)
+ * @see #setSystemUiVisibility(int)
+ *
+ * @deprecated As of API 20 use {@link #dispatchApplyWindowInsets(WindowInsets)} to apply
+ * insets to views. Views should override {@link #onApplyWindowInsets(WindowInsets)} or use
+ * {@link #setOnApplyWindowInsetsListener(android.view.View.OnApplyWindowInsetsListener)}
+ * to implement handling their own insets.
+ */
+ @Deprecated
+ protected boolean fitSystemWindows(Rect insets) {
+ if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
+ if (insets == null) {
+ // Null insets by definition have already been consumed.
+ // This call cannot apply insets since there are none to apply,
+ // so return false.
+ return false;
+ }
+ // If we're not in the process of dispatching the newer apply insets call,
+ // that means we're not in the compatibility path. Dispatch into the newer
+ // apply insets path and take things from there.
+ try {
+ mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
+ return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
+ } finally {
+ mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
+ }
+ } else {
+ // We're being called from the newer apply insets path.
+ // Perform the standard fallback behavior.
+ return fitSystemWindowsInt(insets);
+ }
+ }
+
+ private boolean fitSystemWindowsInt(Rect insets) {
+ if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
+ Rect localInsets = sThreadLocal.get();
+ boolean res = computeFitSystemWindows(insets, localInsets);
+ applyInsets(localInsets);
+ return res;
+ }
+ return false;
+ }
+
+ private void applyInsets(Rect insets) {
+ mUserPaddingStart = UNDEFINED_PADDING;
+ mUserPaddingEnd = UNDEFINED_PADDING;
+ mUserPaddingLeftInitial = insets.left;
+ mUserPaddingRightInitial = insets.right;
+ internalSetPadding(insets.left, insets.top, insets.right, insets.bottom);
+ }
+
+ /**
+ * Called when the view should apply {@link WindowInsets} according to its internal policy.
+ *
+ * <p>This method should be overridden by views that wish to apply a policy different from or
+ * in addition to the default behavior. Clients that wish to force a view subtree
+ * to apply insets should call {@link #dispatchApplyWindowInsets(WindowInsets)}.</p>
+ *
+ * <p>Clients may supply an {@link OnApplyWindowInsetsListener} to a view. If one is set
+ * it will be called during dispatch instead of this method. The listener may optionally
+ * call this method from its own implementation if it wishes to apply the view's default
+ * insets policy in addition to its own.</p>
+ *
+ * <p>Implementations of this method should either return the insets parameter unchanged
+ * or a new {@link WindowInsets} cloned from the supplied insets with any insets consumed
+ * that this view applied itself. This allows new inset types added in future platform
+ * versions to pass through existing implementations unchanged without being erroneously
+ * consumed.</p>
+ *
+ * <p>By default if a view's {@link #setFitsSystemWindows(boolean) fitsSystemWindows}
+ * property is set then the view will consume the system window insets and apply them
+ * as padding for the view.</p>
+ *
+ * @param insets Insets to apply
+ * @return The supplied insets with any applied insets consumed
+ */
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if ((mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0
+ && (mViewFlags & FITS_SYSTEM_WINDOWS) != 0) {
+ return onApplyFrameworkOptionalFitSystemWindows(insets);
+ }
+ if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
+ // We weren't called from within a direct call to fitSystemWindows,
+ // call into it as a fallback in case we're in a class that overrides it
+ // and has logic to perform.
+ if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
+ return insets.consumeSystemWindowInsets();
+ }
+ } else {
+ // We were called from within a direct call to fitSystemWindows.
+ if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
+ return insets.consumeSystemWindowInsets();
+ }
+ }
+ return insets;
+ }
+
+ private WindowInsets onApplyFrameworkOptionalFitSystemWindows(WindowInsets insets) {
+ Rect localInsets = sThreadLocal.get();
+ WindowInsets result = computeSystemWindowInsets(insets, localInsets);
+ applyInsets(localInsets);
+ return result;
+ }
+
+ /**
+ * Set an {@link OnApplyWindowInsetsListener} to take over the policy for applying
+ * window insets to this view. The listener's
+ * {@link OnApplyWindowInsetsListener#onApplyWindowInsets(View, WindowInsets) onApplyWindowInsets}
+ * method will be called instead of the view's
+ * {@link #onApplyWindowInsets(WindowInsets) onApplyWindowInsets} method.
+ *
+ * @param listener Listener to set
+ *
+ * @see #onApplyWindowInsets(WindowInsets)
+ */
+ public void setOnApplyWindowInsetsListener(OnApplyWindowInsetsListener listener) {
+ getListenerInfo().mOnApplyWindowInsetsListener = listener;
+ }
+
+ /**
+ * Request to apply the given window insets to this view or another view in its subtree.
+ *
+ * <p>This method should be called by clients wishing to apply insets corresponding to areas
+ * obscured by window decorations or overlays. This can include the status and navigation bars,
+ * action bars, input methods and more. New inset categories may be added in the future.
+ * The method returns the insets provided minus any that were applied by this view or its
+ * children.</p>
+ *
+ * <p>Clients wishing to provide custom behavior should override the
+ * {@link #onApplyWindowInsets(WindowInsets)} method or alternatively provide a
+ * {@link OnApplyWindowInsetsListener} via the
+ * {@link #setOnApplyWindowInsetsListener(View.OnApplyWindowInsetsListener) setOnApplyWindowInsetsListener}
+ * method.</p>
+ *
+ * <p>This method replaces the older {@link #fitSystemWindows(Rect) fitSystemWindows} method.
+ * </p>
+ *
+ * @param insets Insets to apply
+ * @return The provided insets minus the insets that were consumed
+ */
+ public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
+ try {
+ mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
+ if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
+ return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
+ } else {
+ return onApplyWindowInsets(insets);
+ }
+ } finally {
+ mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
+ }
+ }
+
+ /**
+ * Sets a {@link WindowInsetsAnimation.Callback} to be notified about animations of windows that
+ * cause insets.
+ * <p>
+ * The callback's {@link WindowInsetsAnimation.Callback#getDispatchMode()
+ * dispatch mode} will affect whether animation callbacks are dispatched to the children of
+ * this view.
+ * </p>
+ * @param callback The callback to set.
+ */
+ public void setWindowInsetsAnimationCallback(
+ @Nullable WindowInsetsAnimation.Callback callback) {
+ getListenerInfo().mWindowInsetsAnimationCallback = callback;
+ }
+
+ /**
+ * @return {@code true} if any {@link WindowInsetsAnimation.Callback} is registered on the view
+ * or view tree of the sub-hierarchy {@code false} otherwise.
+ * @hide
+ */
+ public boolean hasWindowInsetsAnimationCallback() {
+ return getListenerInfo().mWindowInsetsAnimationCallback != null;
+ }
+
+ /**
+ * Dispatches {@link WindowInsetsAnimation.Callback#onPrepare(WindowInsetsAnimation)}
+ * when Window Insets animation is being prepared.
+ * @param animation current animation
+ *
+ * @see WindowInsetsAnimation.Callback#onPrepare(WindowInsetsAnimation)
+ */
+ public void dispatchWindowInsetsAnimationPrepare(
+ @NonNull WindowInsetsAnimation animation) {
+ if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) {
+ mListenerInfo.mWindowInsetsAnimationCallback.onPrepare(animation);
+ }
+ }
+
+ /**
+ * Dispatches {@link WindowInsetsAnimation.Callback#onStart(WindowInsetsAnimation, Bounds)}
+ * when Window Insets animation is started.
+ * @param animation current animation
+ * @param bounds the upper and lower {@link Bounds} that provides range of
+ * {@link WindowInsetsAnimation}.
+ * @return the upper and lower {@link Bounds}.
+ */
+ @NonNull
+ public Bounds dispatchWindowInsetsAnimationStart(
+ @NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds) {
+ if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) {
+ return mListenerInfo.mWindowInsetsAnimationCallback.onStart(animation, bounds);
+ }
+ return bounds;
+ }
+
+ /**
+ * Dispatches {@link WindowInsetsAnimation.Callback#onProgress(WindowInsets, List)}
+ * when Window Insets animation makes progress.
+ * @param insets The current {@link WindowInsets}.
+ * @param runningAnimations The currently running {@link WindowInsetsAnimation}s.
+ * @return current {@link WindowInsets}.
+ */
+ @NonNull
+ public WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets,
+ @NonNull List<WindowInsetsAnimation> runningAnimations) {
+ if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) {
+ return mListenerInfo.mWindowInsetsAnimationCallback.onProgress(insets,
+ runningAnimations);
+ } else {
+ return insets;
+ }
+ }
+
+ /**
+ * Dispatches {@link WindowInsetsAnimation.Callback#onEnd(WindowInsetsAnimation)}
+ * when Window Insets animation ends.
+ * @param animation The current ongoing {@link WindowInsetsAnimation}.
+ */
+ public void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation) {
+ if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) {
+ mListenerInfo.mWindowInsetsAnimationCallback.onEnd(animation);
+ }
+ }
+
+ /**
+ * Sets a list of areas within this view's post-layout coordinate space where the system
+ * should not intercept touch or other pointing device gestures. <em>This method should
+ * be called by {@link #onLayout(boolean, int, int, int, int)} or {@link #onDraw(Canvas)}.</em>
+ *
+ * <p>Use this to tell the system which specific sub-areas of a view need to receive gesture
+ * input in order to function correctly in the presence of global system gestures that may
+ * conflict. For example, if the system wishes to capture swipe-in-from-screen-edge gestures
+ * to provide system-level navigation functionality, a view such as a navigation drawer
+ * container can mark the left (or starting) edge of itself as requiring gesture capture
+ * priority using this API. The system may then choose to relax its own gesture recognition
+ * to allow the app to consume the user's gesture. It is not necessary for an app to register
+ * exclusion rects for broadly spanning regions such as the entirety of a
+ * <code>ScrollView</code> or for simple press and release click targets such as
+ * <code>Button</code>. Mark an exclusion rect when interacting with a view requires
+ * a precision touch gesture in a small area in either the X or Y dimension, such as
+ * an edge swipe or dragging a <code>SeekBar</code> thumb.</p>
+ *
+ * <p>Note: the system will put a limit of <code>200dp</code> on the vertical extent of the
+ * exclusions it takes into account. The limit does not apply while the navigation
+ * bar is {@link #SYSTEM_UI_FLAG_IMMERSIVE_STICKY stickily} hidden, nor to the
+ * {@link android.inputmethodservice.InputMethodService input method} and
+ * {@link Intent#CATEGORY_HOME home activity}.
+ * </p>
+ *
+ * @param rects A list of precision gesture regions that this view needs to function correctly
+ */
+ public void setSystemGestureExclusionRects(@NonNull List<Rect> rects) {
+ if (rects.isEmpty() && mListenerInfo == null) return;
+
+ final ListenerInfo info = getListenerInfo();
+ if (info.mSystemGestureExclusionRects != null) {
+ info.mSystemGestureExclusionRects.clear();
+ info.mSystemGestureExclusionRects.addAll(rects);
+ } else {
+ info.mSystemGestureExclusionRects = new ArrayList<>(rects);
+ }
+ if (rects.isEmpty()) {
+ if (info.mPositionUpdateListener != null) {
+ mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
+ }
+ } else {
+ if (info.mPositionUpdateListener == null) {
+ info.mPositionUpdateListener = new RenderNode.PositionUpdateListener() {
+ @Override
+ public void positionChanged(long n, int l, int t, int r, int b) {
+ postUpdateSystemGestureExclusionRects();
+ }
+
+ @Override
+ public void positionLost(long frameNumber) {
+ postUpdateSystemGestureExclusionRects();
+ }
+ };
+ mRenderNode.addPositionUpdateListener(info.mPositionUpdateListener);
+ }
+ }
+ postUpdateSystemGestureExclusionRects();
+ }
+
+ /**
+ * WARNING: this can be called by a hwui worker thread, not just the UI thread!
+ */
+ void postUpdateSystemGestureExclusionRects() {
+ // Potentially racey from a background thread. It's ok if it's not perfect.
+ final Handler h = getHandler();
+ if (h != null) {
+ h.postAtFrontOfQueue(this::updateSystemGestureExclusionRects);
+ }
+ }
+
+ void updateSystemGestureExclusionRects() {
+ final AttachInfo ai = mAttachInfo;
+ if (ai != null) {
+ ai.mViewRootImpl.updateSystemGestureExclusionRectsForView(this);
+ }
+ }
+
+ /**
+ * Retrieve the list of areas within this view's post-layout coordinate space where the system
+ * should not intercept touch or other pointing device gestures.
+ *
+ * <p>Do not modify the returned list.</p>
+ *
+ * @return the list set by {@link #setSystemGestureExclusionRects(List)}
+ */
+ @NonNull
+ public List<Rect> getSystemGestureExclusionRects() {
+ final ListenerInfo info = mListenerInfo;
+ if (info != null) {
+ final List<Rect> list = info.mSystemGestureExclusionRects;
+ if (list != null) {
+ return list;
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Compute the view's coordinate within the surface.
+ *
+ * <p>Computes the coordinates of this view in its surface. The argument
+ * must be an array of two integers. After the method returns, the array
+ * contains the x and y location in that order.</p>
+ *
+ * @param location an array of two integers in which to hold the coordinates
+ */
+ public void getLocationInSurface(@NonNull @Size(2) int[] location) {
+ getLocationInWindow(location);
+ if (mAttachInfo != null && mAttachInfo.mViewRootImpl != null) {
+ location[0] += mAttachInfo.mViewRootImpl.mWindowAttributes.surfaceInsets.left;
+ location[1] += mAttachInfo.mViewRootImpl.mWindowAttributes.surfaceInsets.top;
+ }
+ }
+
+ /**
+ * Provide original WindowInsets that are dispatched to the view hierarchy. The insets are
+ * only available if the view is attached.
+ *
+ * @return WindowInsets from the top of the view hierarchy or null if View is detached
+ */
+ public WindowInsets getRootWindowInsets() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mViewRootImpl.getWindowInsets(false /* forceConstruct */);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the single {@link WindowInsetsController} of the window this view is attached to.
+ *
+ * @return The {@link WindowInsetsController} or {@code null} if the view is neither attached to
+ * a window nor a view tree with a decor.
+ * @see Window#getInsetsController()
+ */
+ public @Nullable WindowInsetsController getWindowInsetsController() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mViewRootImpl.getInsetsController();
+ }
+ ViewParent parent = getParent();
+ if (parent instanceof View) {
+ return ((View) parent).getWindowInsetsController();
+ } else if (parent instanceof ViewRootImpl) {
+ // Between WindowManager.addView() and the first traversal AttachInfo isn't set yet.
+ return ((ViewRootImpl) parent).getInsetsController();
+ }
+ return null;
+ }
+
+ /**
+ * @hide Compute the insets that should be consumed by this view and the ones
+ * that should propagate to those under it.
+ *
+ * Note: This is used by appcompat's ActionBarOverlayLayout through reflection.
+ *
+ * @param inoutInsets the insets given to this view
+ * @param outLocalInsets the insets that should be applied to this view
+ * @deprecated use {@link #computeSystemWindowInsets}
+ * @return
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
+ WindowInsets innerInsets = computeSystemWindowInsets(new WindowInsets(inoutInsets),
+ outLocalInsets);
+ inoutInsets.set(innerInsets.getSystemWindowInsetsAsRect());
+ return innerInsets.isSystemWindowInsetsConsumed();
+ }
+
+ /**
+ * Compute insets that should be consumed by this view and the ones that should propagate
+ * to those under it.
+ *
+ * @param in Insets currently being processed by this View, likely received as a parameter
+ * to {@link #onApplyWindowInsets(WindowInsets)}.
+ * @param outLocalInsets A Rect that will receive the insets that should be consumed
+ * by this view
+ * @return Insets that should be passed along to views under this one
+ */
+ public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) {
+ boolean isOptionalFitSystemWindows = (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) != 0
+ || (mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0;
+ if (isOptionalFitSystemWindows && mAttachInfo != null) {
+ OnContentApplyWindowInsetsListener listener =
+ mAttachInfo.mContentOnApplyWindowInsetsListener;
+ if (listener == null) {
+ // The application wants to take care of fitting system window for
+ // the content.
+ outLocalInsets.setEmpty();
+ return in;
+ }
+ Pair<Insets, WindowInsets> result = listener.onContentApplyWindowInsets(this, in);
+ outLocalInsets.set(result.first.toRect());
+ return result.second;
+ } else {
+ outLocalInsets.set(in.getSystemWindowInsetsAsRect());
+ return in.consumeSystemWindowInsets().inset(outLocalInsets);
+ }
+ }
+
+ /**
+ * Sets whether or not this view should account for system screen decorations
+ * such as the status bar and inset its content; that is, controlling whether
+ * the default implementation of {@link #fitSystemWindows(Rect)} will be
+ * executed. See that method for more details.
+ *
+ * <p>Note that if you are providing your own implementation of
+ * {@link #fitSystemWindows(Rect)}, then there is no need to set this
+ * flag to true -- your implementation will be overriding the default
+ * implementation that checks this flag.
+ *
+ * @param fitSystemWindows If true, then the default implementation of
+ * {@link #fitSystemWindows(Rect)} will be executed.
+ *
+ * @attr ref android.R.styleable#View_fitsSystemWindows
+ * @see #getFitsSystemWindows()
+ * @see #fitSystemWindows(Rect)
+ * @see #setSystemUiVisibility(int)
+ */
+ public void setFitsSystemWindows(boolean fitSystemWindows) {
+ setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
+ }
+
+ /**
+ * Check for state of {@link #setFitsSystemWindows(boolean)}. If this method
+ * returns {@code true}, the default implementation of {@link #fitSystemWindows(Rect)}
+ * will be executed.
+ *
+ * @return {@code true} if the default implementation of
+ * {@link #fitSystemWindows(Rect)} will be executed.
+ *
+ * @attr ref android.R.styleable#View_fitsSystemWindows
+ * @see #setFitsSystemWindows(boolean)
+ * @see #fitSystemWindows(Rect)
+ * @see #setSystemUiVisibility(int)
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public boolean getFitsSystemWindows() {
+ return (mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean fitsSystemWindows() {
+ return getFitsSystemWindows();
+ }
+
+ /**
+ * Ask that a new dispatch of {@link #fitSystemWindows(Rect)} be performed.
+ * @deprecated Use {@link #requestApplyInsets()} for newer platform versions.
+ */
+ @Deprecated
+ public void requestFitSystemWindows() {
+ if (mParent != null) {
+ mParent.requestFitSystemWindows();
+ }
+ }
+
+ /**
+ * Ask that a new dispatch of {@link #onApplyWindowInsets(WindowInsets)} be performed.
+ */
+ public void requestApplyInsets() {
+ requestFitSystemWindows();
+ }
+
+ /**
+ * @see #OPTIONAL_FITS_SYSTEM_WINDOWS
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void makeOptionalFitsSystemWindows() {
+ setFlags(OPTIONAL_FITS_SYSTEM_WINDOWS, OPTIONAL_FITS_SYSTEM_WINDOWS);
+ }
+
+ /**
+ * @see #PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS
+ * @hide
+ */
+ public void makeFrameworkOptionalFitsSystemWindows() {
+ mPrivateFlags4 |= PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isFrameworkOptionalFitsSystemWindows() {
+ return (mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0;
+ }
+
+ /**
+ * Returns the visibility status for this view.
+ *
+ * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ * @attr ref android.R.styleable#View_visibility
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = VISIBLE, to = "VISIBLE"),
+ @ViewDebug.IntToString(from = INVISIBLE, to = "INVISIBLE"),
+ @ViewDebug.IntToString(from = GONE, to = "GONE")
+ })
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = VISIBLE, name = "visible"),
+ @EnumEntry(value = INVISIBLE, name = "invisible"),
+ @EnumEntry(value = GONE, name = "gone")
+ })
+ @Visibility
+ public int getVisibility() {
+ return mViewFlags & VISIBILITY_MASK;
+ }
+
+ /**
+ * Set the visibility state of this view.
+ *
+ * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ * @attr ref android.R.styleable#View_visibility
+ */
+ @RemotableViewMethod
+ public void setVisibility(@Visibility int visibility) {
+ setFlags(visibility, VISIBILITY_MASK);
+ }
+
+ /**
+ * Returns the enabled status for this view. The interpretation of the
+ * enabled state varies by subclass.
+ *
+ * @return True if this view is enabled, false otherwise.
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public boolean isEnabled() {
+ return (mViewFlags & ENABLED_MASK) == ENABLED;
+ }
+
+ /**
+ * Set the enabled state of this view. The interpretation of the enabled
+ * state varies by subclass.
+ *
+ * @param enabled True if this view is enabled, false otherwise.
+ */
+ @RemotableViewMethod
+ public void setEnabled(boolean enabled) {
+ if (enabled == isEnabled()) return;
+
+ setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK);
+
+ /*
+ * The View most likely has to change its appearance, so refresh
+ * the drawable state.
+ */
+ refreshDrawableState();
+
+ // Invalidate too, since the default behavior for views is to be
+ // be drawn at 50% alpha rather than to change the drawable.
+ invalidate(true);
+
+ if (!enabled) {
+ cancelPendingInputEvents();
+ }
+ }
+
+ /**
+ * Set whether this view can receive the focus.
+ * <p>
+ * Setting this to false will also ensure that this view is not focusable
+ * in touch mode.
+ *
+ * @param focusable If true, this view can receive the focus.
+ *
+ * @see #setFocusableInTouchMode(boolean)
+ * @see #setFocusable(int)
+ * @attr ref android.R.styleable#View_focusable
+ */
+ @RemotableViewMethod
+ public void setFocusable(boolean focusable) {
+ setFocusable(focusable ? FOCUSABLE : NOT_FOCUSABLE);
+ }
+
+ /**
+ * Sets whether this view can receive focus.
+ * <p>
+ * Setting this to {@link #FOCUSABLE_AUTO} tells the framework to determine focusability
+ * automatically based on the view's interactivity. This is the default.
+ * <p>
+ * Setting this to NOT_FOCUSABLE will ensure that this view is also not focusable
+ * in touch mode.
+ *
+ * @param focusable One of {@link #NOT_FOCUSABLE}, {@link #FOCUSABLE},
+ * or {@link #FOCUSABLE_AUTO}.
+ * @see #setFocusableInTouchMode(boolean)
+ * @attr ref android.R.styleable#View_focusable
+ */
+ @RemotableViewMethod
+ public void setFocusable(@Focusable int focusable) {
+ if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
+ setFlags(0, FOCUSABLE_IN_TOUCH_MODE);
+ }
+ setFlags(focusable, FOCUSABLE_MASK);
+ }
+
+ /**
+ * Set whether this view can receive focus while in touch mode.
+ *
+ * Setting this to true will also ensure that this view is focusable.
+ *
+ * @param focusableInTouchMode If true, this view can receive the focus while
+ * in touch mode.
+ *
+ * @see #setFocusable(boolean)
+ * @attr ref android.R.styleable#View_focusableInTouchMode
+ */
+ @RemotableViewMethod
+ public void setFocusableInTouchMode(boolean focusableInTouchMode) {
+ // Focusable in touch mode should always be set before the focusable flag
+ // otherwise, setting the focusable flag will trigger a focusableViewAvailable()
+ // which, in touch mode, will not successfully request focus on this view
+ // because the focusable in touch mode flag is not set
+ setFlags(focusableInTouchMode ? FOCUSABLE_IN_TOUCH_MODE : 0, FOCUSABLE_IN_TOUCH_MODE);
+
+ // Clear FOCUSABLE_AUTO if set.
+ if (focusableInTouchMode) {
+ // Clears FOCUSABLE_AUTO if set.
+ setFlags(FOCUSABLE, FOCUSABLE_MASK);
+ }
+ }
+
+ /**
+ * Sets the hints that help an {@link android.service.autofill.AutofillService} determine how
+ * to autofill the view with the user's data.
+ *
+ * <p>Typically, there is only one way to autofill a view, but there could be more than one.
+ * For example, if the application accepts either an username or email address to identify
+ * an user.
+ *
+ * <p>These hints are not validated by the Android System, but passed "as is" to the service.
+ * Hence, they can have any value, but it's recommended to use the {@code AUTOFILL_HINT_}
+ * constants such as:
+ * {@link #AUTOFILL_HINT_USERNAME}, {@link #AUTOFILL_HINT_PASSWORD},
+ * {@link #AUTOFILL_HINT_EMAIL_ADDRESS},
+ * {@link #AUTOFILL_HINT_NAME},
+ * {@link #AUTOFILL_HINT_PHONE},
+ * {@link #AUTOFILL_HINT_POSTAL_ADDRESS}, {@link #AUTOFILL_HINT_POSTAL_CODE},
+ * {@link #AUTOFILL_HINT_CREDIT_CARD_NUMBER}, {@link #AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE},
+ * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE},
+ * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY},
+ * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH} or
+ * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR}.
+ *
+ * @param autofillHints The autofill hints to set. If the array is emtpy, {@code null} is set.
+ * @attr ref android.R.styleable#View_autofillHints
+ */
+ public void setAutofillHints(@Nullable String... autofillHints) {
+ if (autofillHints == null || autofillHints.length == 0) {
+ mAutofillHints = null;
+ } else {
+ mAutofillHints = autofillHints;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public void setAutofilled(boolean isAutofilled, boolean hideHighlight) {
+ boolean wasChanged = isAutofilled != isAutofilled();
+
+ if (wasChanged) {
+ if (isAutofilled) {
+ mPrivateFlags3 |= PFLAG3_IS_AUTOFILLED;
+ } else {
+ mPrivateFlags3 &= ~PFLAG3_IS_AUTOFILLED;
+ }
+
+ if (hideHighlight) {
+ mPrivateFlags4 |= PFLAG4_AUTOFILL_HIDE_HIGHLIGHT;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_AUTOFILL_HIDE_HIGHLIGHT;
+ }
+
+ invalidate();
+ }
+ }
+
+ /**
+ * Set whether this view should have sound effects enabled for events such as
+ * clicking and touching.
+ *
+ * <p>You may wish to disable sound effects for a view if you already play sounds,
+ * for instance, a dial key that plays dtmf tones.
+ *
+ * @param soundEffectsEnabled whether sound effects are enabled for this view.
+ * @see #isSoundEffectsEnabled()
+ * @see #playSoundEffect(int)
+ * @attr ref android.R.styleable#View_soundEffectsEnabled
+ */
+ public void setSoundEffectsEnabled(boolean soundEffectsEnabled) {
+ setFlags(soundEffectsEnabled ? SOUND_EFFECTS_ENABLED: 0, SOUND_EFFECTS_ENABLED);
+ }
+
+ /**
+ * @return whether this view should have sound effects enabled for events such as
+ * clicking and touching.
+ *
+ * @see #setSoundEffectsEnabled(boolean)
+ * @see #playSoundEffect(int)
+ * @attr ref android.R.styleable#View_soundEffectsEnabled
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public boolean isSoundEffectsEnabled() {
+ return SOUND_EFFECTS_ENABLED == (mViewFlags & SOUND_EFFECTS_ENABLED);
+ }
+
+ /**
+ * Set whether this view should have haptic feedback for events such as
+ * long presses.
+ *
+ * <p>You may wish to disable haptic feedback if your view already controls
+ * its own haptic feedback.
+ *
+ * @param hapticFeedbackEnabled whether haptic feedback enabled for this view.
+ * @see #isHapticFeedbackEnabled()
+ * @see #performHapticFeedback(int)
+ * @attr ref android.R.styleable#View_hapticFeedbackEnabled
+ */
+ public void setHapticFeedbackEnabled(boolean hapticFeedbackEnabled) {
+ setFlags(hapticFeedbackEnabled ? HAPTIC_FEEDBACK_ENABLED: 0, HAPTIC_FEEDBACK_ENABLED);
+ }
+
+ /**
+ * @return whether this view should have haptic feedback enabled for events
+ * long presses.
+ *
+ * @see #setHapticFeedbackEnabled(boolean)
+ * @see #performHapticFeedback(int)
+ * @attr ref android.R.styleable#View_hapticFeedbackEnabled
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public boolean isHapticFeedbackEnabled() {
+ return HAPTIC_FEEDBACK_ENABLED == (mViewFlags & HAPTIC_FEEDBACK_ENABLED);
+ }
+
+ /**
+ * Returns the layout direction for this view.
+ *
+ * @return One of {@link #LAYOUT_DIRECTION_LTR},
+ * {@link #LAYOUT_DIRECTION_RTL},
+ * {@link #LAYOUT_DIRECTION_INHERIT} or
+ * {@link #LAYOUT_DIRECTION_LOCALE}.
+ *
+ * @attr ref android.R.styleable#View_layoutDirection
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "layout", mapping = {
+ @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LTR, to = "LTR"),
+ @ViewDebug.IntToString(from = LAYOUT_DIRECTION_RTL, to = "RTL"),
+ @ViewDebug.IntToString(from = LAYOUT_DIRECTION_INHERIT, to = "INHERIT"),
+ @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LOCALE, to = "LOCALE")
+ })
+ @InspectableProperty(hasAttributeId = false, enumMapping = {
+ @EnumEntry(value = LAYOUT_DIRECTION_LTR, name = "ltr"),
+ @EnumEntry(value = LAYOUT_DIRECTION_RTL, name = "rtl"),
+ @EnumEntry(value = LAYOUT_DIRECTION_INHERIT, name = "inherit"),
+ @EnumEntry(value = LAYOUT_DIRECTION_LOCALE, name = "locale")
+ })
+ @LayoutDir
+ public int getRawLayoutDirection() {
+ return (mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_MASK) >> PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT;
+ }
+
+ /**
+ * Set the layout direction for this view. This will propagate a reset of layout direction
+ * resolution to the view's children and resolve layout direction for this view.
+ *
+ * @param layoutDirection the layout direction to set. Should be one of:
+ *
+ * {@link #LAYOUT_DIRECTION_LTR},
+ * {@link #LAYOUT_DIRECTION_RTL},
+ * {@link #LAYOUT_DIRECTION_INHERIT},
+ * {@link #LAYOUT_DIRECTION_LOCALE}.
+ *
+ * Resolution will be done if the value is set to LAYOUT_DIRECTION_INHERIT. The resolution
+ * proceeds up the parent chain of the view to get the value. If there is no parent, then it
+ * will return the default {@link #LAYOUT_DIRECTION_LTR}.
+ *
+ * @attr ref android.R.styleable#View_layoutDirection
+ */
+ @RemotableViewMethod
+ public void setLayoutDirection(@LayoutDir int layoutDirection) {
+ if (getRawLayoutDirection() != layoutDirection) {
+ // Reset the current layout direction and the resolved one
+ mPrivateFlags2 &= ~PFLAG2_LAYOUT_DIRECTION_MASK;
+ resetRtlProperties();
+ // Set the new layout direction (filtered)
+ mPrivateFlags2 |=
+ ((layoutDirection << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT) & PFLAG2_LAYOUT_DIRECTION_MASK);
+ // We need to resolve all RTL properties as they all depend on layout direction
+ resolveRtlPropertiesIfNeeded();
+ requestLayout();
+ invalidate(true);
+ }
+ }
+
+ /**
+ * Returns the resolved layout direction for this view.
+ *
+ * @return {@link #LAYOUT_DIRECTION_RTL} if the layout direction is RTL or returns
+ * {@link #LAYOUT_DIRECTION_LTR} if the layout direction is not RTL.
+ *
+ * For compatibility, this will return {@link #LAYOUT_DIRECTION_LTR} if API version
+ * is lower than {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}.
+ *
+ * @attr ref android.R.styleable#View_layoutDirection
+ */
+ @ViewDebug.ExportedProperty(category = "layout", mapping = {
+ @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LTR, to = "RESOLVED_DIRECTION_LTR"),
+ @ViewDebug.IntToString(from = LAYOUT_DIRECTION_RTL, to = "RESOLVED_DIRECTION_RTL")
+ })
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = LAYOUT_DIRECTION_LTR, name = "ltr"),
+ @EnumEntry(value = LAYOUT_DIRECTION_RTL, name = "rtl")
+ })
+ @ResolvedLayoutDir
+ public int getLayoutDirection() {
+ final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+ if (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED;
+ return LAYOUT_DIRECTION_RESOLVED_DEFAULT;
+ }
+ return ((mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL) ==
+ PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL) ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR;
+ }
+
+ /**
+ * Indicates whether or not this view's layout is right-to-left. This is resolved from
+ * layout attribute and/or the inherited value from the parent
+ *
+ * @return true if the layout is right-to-left.
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage
+ public boolean isLayoutRtl() {
+ return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+ }
+
+ /**
+ * Indicates whether the view is currently tracking transient state that the
+ * app should not need to concern itself with saving and restoring, but that
+ * the framework should take special note to preserve when possible.
+ *
+ * <p>A view with transient state cannot be trivially rebound from an external
+ * data source, such as an adapter binding item views in a list. This may be
+ * because the view is performing an animation, tracking user selection
+ * of content, or similar.</p>
+ *
+ * @return true if the view has transient state
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public boolean hasTransientState() {
+ return (mPrivateFlags2 & PFLAG2_HAS_TRANSIENT_STATE) == PFLAG2_HAS_TRANSIENT_STATE;
+ }
+
+ /**
+ * Set whether this view is currently tracking transient state that the
+ * framework should attempt to preserve when possible. This flag is reference counted,
+ * so every call to setHasTransientState(true) should be paired with a later call
+ * to setHasTransientState(false).
+ *
+ * <p>A view with transient state cannot be trivially rebound from an external
+ * data source, such as an adapter binding item views in a list. This may be
+ * because the view is performing an animation, tracking user selection
+ * of content, or similar.</p>
+ *
+ * @param hasTransientState true if this view has transient state
+ */
+ public void setHasTransientState(boolean hasTransientState) {
+ final boolean oldHasTransientState = hasTransientState();
+ mTransientStateCount = hasTransientState ? mTransientStateCount + 1 :
+ mTransientStateCount - 1;
+ if (mTransientStateCount < 0) {
+ mTransientStateCount = 0;
+ Log.e(VIEW_LOG_TAG, "hasTransientState decremented below 0: " +
+ "unmatched pair of setHasTransientState calls");
+ } else if ((hasTransientState && mTransientStateCount == 1) ||
+ (!hasTransientState && mTransientStateCount == 0)) {
+ // update flag if we've just incremented up from 0 or decremented down to 0
+ mPrivateFlags2 = (mPrivateFlags2 & ~PFLAG2_HAS_TRANSIENT_STATE) |
+ (hasTransientState ? PFLAG2_HAS_TRANSIENT_STATE : 0);
+ final boolean newHasTransientState = hasTransientState();
+ if (mParent != null && newHasTransientState != oldHasTransientState) {
+ try {
+ mParent.childHasTransientStateChanged(this, newHasTransientState);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the view is tracking translation transient state. This flag is used to check if the view
+ * need to call setHasTransientState(false) to reset transient state that set when starting
+ * translation.
+ *
+ * @param hasTranslationTransientState true if this view has translation transient state
+ * @hide
+ */
+ public void setHasTranslationTransientState(boolean hasTranslationTransientState) {
+ if (hasTranslationTransientState) {
+ mPrivateFlags4 |= PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean hasTranslationTransientState() {
+ return (mPrivateFlags4 & PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE)
+ == PFLAG4_HAS_TRANSLATION_TRANSIENT_STATE;
+ }
+
+ /**
+ * Returns true if this view is currently attached to a window.
+ */
+ public boolean isAttachedToWindow() {
+ return mAttachInfo != null;
+ }
+
+ /**
+ * Returns true if this view has been through at least one layout since it
+ * was last attached to or detached from a window.
+ */
+ public boolean isLaidOut() {
+ return (mPrivateFlags3 & PFLAG3_IS_LAID_OUT) == PFLAG3_IS_LAID_OUT;
+ }
+
+ /**
+ * @return {@code true} if laid-out and not about to do another layout.
+ */
+ boolean isLayoutValid() {
+ return isLaidOut() && ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == 0);
+ }
+
+ /**
+ * If this view doesn't do any drawing on its own, set this flag to
+ * allow further optimizations. By default, this flag is not set on
+ * View, but could be set on some View subclasses such as ViewGroup.
+ *
+ * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
+ * you should clear this flag.
+ *
+ * @param willNotDraw whether or not this View draw on its own
+ */
+ public void setWillNotDraw(boolean willNotDraw) {
+ setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
+ }
+
+ /**
+ * Returns whether or not this View draws on its own.
+ *
+ * @return true if this view has nothing to draw, false otherwise
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public boolean willNotDraw() {
+ return (mViewFlags & DRAW_MASK) == WILL_NOT_DRAW;
+ }
+
+ /**
+ * When a View's drawing cache is enabled, drawing is redirected to an
+ * offscreen bitmap. Some views, like an ImageView, must be able to
+ * bypass this mechanism if they already draw a single bitmap, to avoid
+ * unnecessary usage of the memory.
+ *
+ * @param willNotCacheDrawing true if this view does not cache its
+ * drawing, false otherwise
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void setWillNotCacheDrawing(boolean willNotCacheDrawing) {
+ setFlags(willNotCacheDrawing ? WILL_NOT_CACHE_DRAWING : 0, WILL_NOT_CACHE_DRAWING);
+ }
+
+ /**
+ * Returns whether or not this View can cache its drawing or not.
+ *
+ * @return true if this view does not cache its drawing, false otherwise
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @Deprecated
+ public boolean willNotCacheDrawing() {
+ return (mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING;
+ }
+
+ /**
+ * Indicates whether this view reacts to click events or not.
+ *
+ * @return true if the view is clickable, false otherwise
+ *
+ * @see #setClickable(boolean)
+ * @attr ref android.R.styleable#View_clickable
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public boolean isClickable() {
+ return (mViewFlags & CLICKABLE) == CLICKABLE;
+ }
+
+ /**
+ * Enables or disables click events for this view. When a view
+ * is clickable it will change its state to "pressed" on every click.
+ * Subclasses should set the view clickable to visually react to
+ * user's clicks.
+ *
+ * @param clickable true to make the view clickable, false otherwise
+ *
+ * @see #isClickable()
+ * @attr ref android.R.styleable#View_clickable
+ */
+ public void setClickable(boolean clickable) {
+ setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
+ }
+
+ /**
+ * Enables or disables click events for this view when disabled.
+ *
+ * @param clickableWhenDisabled true to make the view clickable, false otherwise
+ *
+ * @attr ref android.R.styleable#View_allowClickWhenDisabled
+ */
+ public void setAllowClickWhenDisabled(boolean clickableWhenDisabled) {
+ if (clickableWhenDisabled) {
+ mPrivateFlags4 |= PFLAG4_ALLOW_CLICK_WHEN_DISABLED;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_ALLOW_CLICK_WHEN_DISABLED;
+ }
+ }
+
+ /**
+ * Indicates whether this view reacts to long click events or not.
+ *
+ * @return true if the view is long clickable, false otherwise
+ *
+ * @see #setLongClickable(boolean)
+ * @attr ref android.R.styleable#View_longClickable
+ */
+ @InspectableProperty
+ public boolean isLongClickable() {
+ return (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
+ }
+
+ /**
+ * Enables or disables long click events for this view. When a view is long
+ * clickable it reacts to the user holding down the button for a longer
+ * duration than a tap. This event can either launch the listener or a
+ * context menu.
+ *
+ * @param longClickable true to make the view long clickable, false otherwise
+ * @see #isLongClickable()
+ * @attr ref android.R.styleable#View_longClickable
+ */
+ public void setLongClickable(boolean longClickable) {
+ setFlags(longClickable ? LONG_CLICKABLE : 0, LONG_CLICKABLE);
+ }
+
+ /**
+ * Indicates whether this view reacts to context clicks or not.
+ *
+ * @return true if the view is context clickable, false otherwise
+ * @see #setContextClickable(boolean)
+ * @attr ref android.R.styleable#View_contextClickable
+ */
+ @InspectableProperty
+ public boolean isContextClickable() {
+ return (mViewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
+ }
+
+ /**
+ * Enables or disables context clicking for this view. This event can launch the listener.
+ *
+ * @param contextClickable true to make the view react to a context click, false otherwise
+ * @see #isContextClickable()
+ * @attr ref android.R.styleable#View_contextClickable
+ */
+ public void setContextClickable(boolean contextClickable) {
+ setFlags(contextClickable ? CONTEXT_CLICKABLE : 0, CONTEXT_CLICKABLE);
+ }
+
+ /**
+ * Sets the pressed state for this view and provides a touch coordinate for
+ * animation hinting.
+ *
+ * @param pressed Pass true to set the View's internal state to "pressed",
+ * or false to reverts the View's internal state from a
+ * previously set "pressed" state.
+ * @param x The x coordinate of the touch that caused the press
+ * @param y The y coordinate of the touch that caused the press
+ */
+ private void setPressed(boolean pressed, float x, float y) {
+ if (pressed) {
+ drawableHotspotChanged(x, y);
+ }
+
+ setPressed(pressed);
+ }
+
+ /**
+ * Sets the pressed state for this view.
+ *
+ * @see #isClickable()
+ * @see #setClickable(boolean)
+ *
+ * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
+ * the View's internal state from a previously set "pressed" state.
+ */
+ public void setPressed(boolean pressed) {
+ final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
+
+ if (pressed) {
+ mPrivateFlags |= PFLAG_PRESSED;
+ } else {
+ mPrivateFlags &= ~PFLAG_PRESSED;
+ }
+
+ if (needsRefresh) {
+ refreshDrawableState();
+ }
+ dispatchSetPressed(pressed);
+ }
+
+ /**
+ * Dispatch setPressed to all of this View's children.
+ *
+ * @see #setPressed(boolean)
+ *
+ * @param pressed The new pressed state
+ */
+ protected void dispatchSetPressed(boolean pressed) {
+ }
+
+ /**
+ * Indicates whether the view is currently in pressed state. Unless
+ * {@link #setPressed(boolean)} is explicitly called, only clickable views can enter
+ * the pressed state.
+ *
+ * @see #setPressed(boolean)
+ * @see #isClickable()
+ * @see #setClickable(boolean)
+ *
+ * @return true if the view is currently pressed, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty(hasAttributeId = false)
+ public boolean isPressed() {
+ return (mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED;
+ }
+
+ /**
+ * @hide
+ * Indicates whether this view will participate in data collection through
+ * {@link ViewStructure}. If true, it will not provide any data
+ * for itself or its children. If false, the normal data collection will be allowed.
+ *
+ * @return Returns false if assist data collection is not blocked, else true.
+ *
+ * @see #setAssistBlocked(boolean)
+ * @attr ref android.R.styleable#View_assistBlocked
+ */
+ public boolean isAssistBlocked() {
+ return (mPrivateFlags3 & PFLAG3_ASSIST_BLOCKED) != 0;
+ }
+
+ /**
+ * @hide
+ * Controls whether assist data collection from this view and its children is enabled
+ * (that is, whether {@link #onProvideStructure} and
+ * {@link #onProvideVirtualStructure} will be called). The default value is false,
+ * allowing normal assist collection. Setting this to false will disable assist collection.
+ *
+ * @param enabled Set to true to <em>disable</em> assist data collection, or false
+ * (the default) to allow it.
+ *
+ * @see #isAssistBlocked()
+ * @see #onProvideStructure
+ * @see #onProvideVirtualStructure
+ * @attr ref android.R.styleable#View_assistBlocked
+ */
+ @UnsupportedAppUsage
+ public void setAssistBlocked(boolean enabled) {
+ if (enabled) {
+ mPrivateFlags3 |= PFLAG3_ASSIST_BLOCKED;
+ } else {
+ mPrivateFlags3 &= ~PFLAG3_ASSIST_BLOCKED;
+ }
+ }
+
+ /**
+ * Indicates whether this view will save its state (that is,
+ * whether its {@link #onSaveInstanceState} method will be called).
+ *
+ * @return Returns true if the view state saving is enabled, else false.
+ *
+ * @see #setSaveEnabled(boolean)
+ * @attr ref android.R.styleable#View_saveEnabled
+ */
+ @InspectableProperty
+ public boolean isSaveEnabled() {
+ return (mViewFlags & SAVE_DISABLED_MASK) != SAVE_DISABLED;
+ }
+
+ /**
+ * Controls whether the saving of this view's state is
+ * enabled (that is, whether its {@link #onSaveInstanceState} method
+ * will be called). Note that even if freezing is enabled, the
+ * view still must have an id assigned to it (via {@link #setId(int)})
+ * for its state to be saved. This flag can only disable the
+ * saving of this view; any child views may still have their state saved.
+ *
+ * @param enabled Set to false to <em>disable</em> state saving, or true
+ * (the default) to allow it.
+ *
+ * @see #isSaveEnabled()
+ * @see #setId(int)
+ * @see #onSaveInstanceState()
+ * @attr ref android.R.styleable#View_saveEnabled
+ */
+ public void setSaveEnabled(boolean enabled) {
+ setFlags(enabled ? 0 : SAVE_DISABLED, SAVE_DISABLED_MASK);
+ }
+
+ /**
+ * Gets whether the framework should discard touches when the view's
+ * window is obscured by another visible window.
+ * Refer to the {@link View} security documentation for more details.
+ *
+ * @return True if touch filtering is enabled.
+ *
+ * @see #setFilterTouchesWhenObscured(boolean)
+ * @attr ref android.R.styleable#View_filterTouchesWhenObscured
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public boolean getFilterTouchesWhenObscured() {
+ return (mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0;
+ }
+
+ /**
+ * Sets whether the framework should discard touches when the view's
+ * window is obscured by another visible window.
+ * Refer to the {@link View} security documentation for more details.
+ *
+ * @param enabled True if touch filtering should be enabled.
+ *
+ * @see #getFilterTouchesWhenObscured
+ * @attr ref android.R.styleable#View_filterTouchesWhenObscured
+ */
+ public void setFilterTouchesWhenObscured(boolean enabled) {
+ setFlags(enabled ? FILTER_TOUCHES_WHEN_OBSCURED : 0,
+ FILTER_TOUCHES_WHEN_OBSCURED);
+ }
+
+ /**
+ * Indicates whether the entire hierarchy under this view will save its
+ * state when a state saving traversal occurs from its parent. The default
+ * is true; if false, these views will not be saved unless
+ * {@link #saveHierarchyState(SparseArray)} is called directly on this view.
+ *
+ * @return Returns true if the view state saving from parent is enabled, else false.
+ *
+ * @see #setSaveFromParentEnabled(boolean)
+ */
+ public boolean isSaveFromParentEnabled() {
+ return (mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED;
+ }
+
+ /**
+ * Controls whether the entire hierarchy under this view will save its
+ * state when a state saving traversal occurs from its parent. The default
+ * is true; if false, these views will not be saved unless
+ * {@link #saveHierarchyState(SparseArray)} is called directly on this view.
+ *
+ * @param enabled Set to false to <em>disable</em> state saving, or true
+ * (the default) to allow it.
+ *
+ * @see #isSaveFromParentEnabled()
+ * @see #setId(int)
+ * @see #onSaveInstanceState()
+ */
+ public void setSaveFromParentEnabled(boolean enabled) {
+ setFlags(enabled ? 0 : PARENT_SAVE_DISABLED, PARENT_SAVE_DISABLED_MASK);
+ }
+
+
+ /**
+ * Returns whether this View is currently able to take focus.
+ *
+ * @return True if this view can take focus, or false otherwise.
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ public final boolean isFocusable() {
+ return FOCUSABLE == (mViewFlags & FOCUSABLE);
+ }
+
+ /**
+ * Returns the focusable setting for this view.
+ *
+ * @return One of {@link #NOT_FOCUSABLE}, {@link #FOCUSABLE}, or {@link #FOCUSABLE_AUTO}.
+ * @attr ref android.R.styleable#View_focusable
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = NOT_FOCUSABLE, to = "NOT_FOCUSABLE"),
+ @ViewDebug.IntToString(from = FOCUSABLE, to = "FOCUSABLE"),
+ @ViewDebug.IntToString(from = FOCUSABLE_AUTO, to = "FOCUSABLE_AUTO")
+ }, category = "focus")
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = NOT_FOCUSABLE, name = "false"),
+ @EnumEntry(value = FOCUSABLE, name = "true"),
+ @EnumEntry(value = FOCUSABLE_AUTO, name = "auto")
+ })
+ @Focusable
+ public int getFocusable() {
+ return (mViewFlags & FOCUSABLE_AUTO) > 0 ? FOCUSABLE_AUTO : mViewFlags & FOCUSABLE;
+ }
+
+ /**
+ * When a view is focusable, it may not want to take focus when in touch mode.
+ * For example, a button would like focus when the user is navigating via a D-pad
+ * so that the user can click on it, but once the user starts touching the screen,
+ * the button shouldn't take focus
+ * @return Whether the view is focusable in touch mode.
+ * @attr ref android.R.styleable#View_focusableInTouchMode
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ @InspectableProperty
+ public final boolean isFocusableInTouchMode() {
+ return FOCUSABLE_IN_TOUCH_MODE == (mViewFlags & FOCUSABLE_IN_TOUCH_MODE);
+ }
+
+ /**
+ * Returns whether the view should be treated as a focusable unit by screen reader
+ * accessibility tools.
+ * @see #setScreenReaderFocusable(boolean)
+ *
+ * @return Whether the view should be treated as a focusable unit by screen reader.
+ *
+ * @attr ref android.R.styleable#View_screenReaderFocusable
+ */
+ @InspectableProperty
+ public boolean isScreenReaderFocusable() {
+ return (mPrivateFlags3 & PFLAG3_SCREEN_READER_FOCUSABLE) != 0;
+ }
+
+ /**
+ * Sets whether this View should be a focusable element for screen readers
+ * and include non-focusable Views from its subtree when providing feedback.
+ * <p>
+ * Note: this is similar to using <a href="#attr_android:focusable">{@code android:focusable},
+ * but does not impact input focus behavior.
+ *
+ * @param screenReaderFocusable Whether the view should be treated as a unit by screen reader
+ * accessibility tools.
+ *
+ * @attr ref android.R.styleable#View_screenReaderFocusable
+ */
+ public void setScreenReaderFocusable(boolean screenReaderFocusable) {
+ updatePflags3AndNotifyA11yIfChanged(PFLAG3_SCREEN_READER_FOCUSABLE, screenReaderFocusable);
+ }
+
+ /**
+ * Gets whether this view is a heading for accessibility purposes.
+ *
+ * @return {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#View_accessibilityHeading
+ */
+ @InspectableProperty
+ public boolean isAccessibilityHeading() {
+ return (mPrivateFlags3 & PFLAG3_ACCESSIBILITY_HEADING) != 0;
+ }
+
+ /**
+ * Set if view is a heading for a section of content for accessibility purposes.
+ *
+ * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#View_accessibilityHeading
+ */
+ public void setAccessibilityHeading(boolean isHeading) {
+ updatePflags3AndNotifyA11yIfChanged(PFLAG3_ACCESSIBILITY_HEADING, isHeading);
+ }
+
+ private void updatePflags3AndNotifyA11yIfChanged(int mask, boolean newValue) {
+ int pflags3 = mPrivateFlags3;
+ if (newValue) {
+ pflags3 |= mask;
+ } else {
+ pflags3 &= ~mask;
+ }
+
+ if (pflags3 != mPrivateFlags3) {
+ mPrivateFlags3 = pflags3;
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+
+ /**
+ * Find the nearest view in the specified direction that can take focus.
+ * This does not actually give focus to that view.
+ *
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ *
+ * @return The nearest focusable in the specified direction, or null if none
+ * can be found.
+ */
+ public View focusSearch(@FocusRealDirection int direction) {
+ if (mParent != null) {
+ return mParent.focusSearch(this, direction);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns whether this View is a root of a keyboard navigation cluster.
+ *
+ * @return True if this view is a root of a cluster, or false otherwise.
+ * @attr ref android.R.styleable#View_keyboardNavigationCluster
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ @InspectableProperty
+ public final boolean isKeyboardNavigationCluster() {
+ return (mPrivateFlags3 & PFLAG3_CLUSTER) != 0;
+ }
+
+ /**
+ * Searches up the view hierarchy to find the top-most cluster. All deeper/nested clusters
+ * will be ignored.
+ *
+ * @return the keyboard navigation cluster that this view is in (can be this view)
+ * or {@code null} if not in one
+ */
+ View findKeyboardNavigationCluster() {
+ if (mParent instanceof View) {
+ View cluster = ((View) mParent).findKeyboardNavigationCluster();
+ if (cluster != null) {
+ return cluster;
+ } else if (isKeyboardNavigationCluster()) {
+ return this;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set whether this view is a root of a keyboard navigation cluster.
+ *
+ * @param isCluster If true, this view is a root of a cluster.
+ *
+ * @attr ref android.R.styleable#View_keyboardNavigationCluster
+ */
+ public void setKeyboardNavigationCluster(boolean isCluster) {
+ if (isCluster) {
+ mPrivateFlags3 |= PFLAG3_CLUSTER;
+ } else {
+ mPrivateFlags3 &= ~PFLAG3_CLUSTER;
+ }
+ }
+
+ /**
+ * Sets this View as the one which receives focus the next time cluster navigation jumps
+ * to the cluster containing this View. This does NOT change focus even if the cluster
+ * containing this view is current.
+ *
+ * @hide
+ */
+ @TestApi
+ public final void setFocusedInCluster() {
+ setFocusedInCluster(findKeyboardNavigationCluster());
+ }
+
+ private void setFocusedInCluster(View cluster) {
+ if (this instanceof ViewGroup) {
+ ((ViewGroup) this).mFocusedInCluster = null;
+ }
+ if (cluster == this) {
+ return;
+ }
+ ViewParent parent = mParent;
+ View child = this;
+ while (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).mFocusedInCluster = child;
+ if (parent == cluster) {
+ break;
+ }
+ child = (View) parent;
+ parent = parent.getParent();
+ }
+ }
+
+ private void updateFocusedInCluster(View oldFocus, @FocusDirection int direction) {
+ if (oldFocus != null) {
+ View oldCluster = oldFocus.findKeyboardNavigationCluster();
+ View cluster = findKeyboardNavigationCluster();
+ if (oldCluster != cluster) {
+ // Going from one cluster to another, so save last-focused.
+ // This covers cluster jumps because they are always FOCUS_DOWN
+ oldFocus.setFocusedInCluster(oldCluster);
+ if (!(oldFocus.mParent instanceof ViewGroup)) {
+ return;
+ }
+ if (direction == FOCUS_FORWARD || direction == FOCUS_BACKWARD) {
+ // This is a result of ordered navigation so consider navigation through
+ // the previous cluster "complete" and clear its last-focused memory.
+ ((ViewGroup) oldFocus.mParent).clearFocusedInCluster(oldFocus);
+ } else if (oldFocus instanceof ViewGroup
+ && ((ViewGroup) oldFocus).getDescendantFocusability()
+ == ViewGroup.FOCUS_AFTER_DESCENDANTS
+ && ViewRootImpl.isViewDescendantOf(this, oldFocus)) {
+ // This means oldFocus is not focusable since it obviously has a focusable
+ // child (this). Don't restore focus to it in the future.
+ ((ViewGroup) oldFocus.mParent).clearFocusedInCluster(oldFocus);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether this View should receive focus when the focus is restored for the view
+ * hierarchy containing this view.
+ * <p>
+ * Focus gets restored for a view hierarchy when the root of the hierarchy gets added to a
+ * window or serves as a target of cluster navigation.
+ *
+ * @see #restoreDefaultFocus()
+ *
+ * @return {@code true} if this view is the default-focus view, {@code false} otherwise
+ * @attr ref android.R.styleable#View_focusedByDefault
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ @InspectableProperty
+ public final boolean isFocusedByDefault() {
+ return (mPrivateFlags3 & PFLAG3_FOCUSED_BY_DEFAULT) != 0;
+ }
+
+ /**
+ * Sets whether this View should receive focus when the focus is restored for the view
+ * hierarchy containing this view.
+ * <p>
+ * Focus gets restored for a view hierarchy when the root of the hierarchy gets added to a
+ * window or serves as a target of cluster navigation.
+ *
+ * @param isFocusedByDefault {@code true} to set this view as the default-focus view,
+ * {@code false} otherwise.
+ *
+ * @see #restoreDefaultFocus()
+ *
+ * @attr ref android.R.styleable#View_focusedByDefault
+ */
+ @RemotableViewMethod
+ public void setFocusedByDefault(boolean isFocusedByDefault) {
+ if (isFocusedByDefault == ((mPrivateFlags3 & PFLAG3_FOCUSED_BY_DEFAULT) != 0)) {
+ return;
+ }
+
+ if (isFocusedByDefault) {
+ mPrivateFlags3 |= PFLAG3_FOCUSED_BY_DEFAULT;
+ } else {
+ mPrivateFlags3 &= ~PFLAG3_FOCUSED_BY_DEFAULT;
+ }
+
+ if (mParent instanceof ViewGroup) {
+ if (isFocusedByDefault) {
+ ((ViewGroup) mParent).setDefaultFocus(this);
+ } else {
+ ((ViewGroup) mParent).clearDefaultFocus(this);
+ }
+ }
+ }
+
+ /**
+ * Returns whether the view hierarchy with this view as a root contain a default-focus view.
+ *
+ * @return {@code true} if this view has default focus, {@code false} otherwise
+ */
+ boolean hasDefaultFocus() {
+ return isFocusedByDefault();
+ }
+
+ /**
+ * Find the nearest keyboard navigation cluster in the specified direction.
+ * This does not actually give focus to that cluster.
+ *
+ * @param currentCluster The starting point of the search. Null means the current cluster is not
+ * found yet
+ * @param direction Direction to look
+ *
+ * @return The nearest keyboard navigation cluster in the specified direction, or null if none
+ * can be found
+ */
+ public View keyboardNavigationClusterSearch(View currentCluster,
+ @FocusDirection int direction) {
+ if (isKeyboardNavigationCluster()) {
+ currentCluster = this;
+ }
+ if (isRootNamespace()) {
+ // Root namespace means we should consider ourselves the top of the
+ // tree for group searching; otherwise we could be group searching
+ // into other tabs. see LocalActivityManager and TabHost for more info.
+ return FocusFinder.getInstance().findNextKeyboardNavigationCluster(
+ this, currentCluster, direction);
+ } else if (mParent != null) {
+ return mParent.keyboardNavigationClusterSearch(currentCluster, direction);
+ }
+ return null;
+ }
+
+ /**
+ * This method is the last chance for the focused view and its ancestors to
+ * respond to an arrow key. This is called when the focused view did not
+ * consume the key internally, nor could the view system find a new view in
+ * the requested direction to give focus to.
+ *
+ * @param focused The currently focused view.
+ * @param direction The direction focus wants to move. One of FOCUS_UP,
+ * FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT.
+ * @return True if the this view consumed this unhandled move.
+ */
+ public boolean dispatchUnhandledMove(View focused, @FocusRealDirection int direction) {
+ return false;
+ }
+
+ /**
+ * Sets whether this View should use a default focus highlight when it gets focused but doesn't
+ * have {@link android.R.attr#state_focused} defined in its background.
+ *
+ * @param defaultFocusHighlightEnabled {@code true} to set this view to use a default focus
+ * highlight, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#View_defaultFocusHighlightEnabled
+ */
+ public void setDefaultFocusHighlightEnabled(boolean defaultFocusHighlightEnabled) {
+ mDefaultFocusHighlightEnabled = defaultFocusHighlightEnabled;
+ }
+
+ /**
+
+ /**
+ * Returns whether this View should use a default focus highlight when it gets focused but
+ * doesn't have {@link android.R.attr#state_focused} defined in its background.
+ *
+ * @return True if this View should use a default focus highlight.
+ * @attr ref android.R.styleable#View_defaultFocusHighlightEnabled
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ @InspectableProperty
+ public final boolean getDefaultFocusHighlightEnabled() {
+ return mDefaultFocusHighlightEnabled;
+ }
+
+ /**
+ * If a user manually specified the next view id for a particular direction,
+ * use the root to look up the view.
+ * @param root The root view of the hierarchy containing this view.
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
+ * or FOCUS_BACKWARD.
+ * @return The user specified next view, or null if there is none.
+ */
+ View findUserSetNextFocus(View root, @FocusDirection int direction) {
+ switch (direction) {
+ case FOCUS_LEFT:
+ if (mNextFocusLeftId == View.NO_ID) return null;
+ return findViewInsideOutShouldExist(root, mNextFocusLeftId);
+ case FOCUS_RIGHT:
+ if (mNextFocusRightId == View.NO_ID) return null;
+ return findViewInsideOutShouldExist(root, mNextFocusRightId);
+ case FOCUS_UP:
+ if (mNextFocusUpId == View.NO_ID) return null;
+ return findViewInsideOutShouldExist(root, mNextFocusUpId);
+ case FOCUS_DOWN:
+ if (mNextFocusDownId == View.NO_ID) return null;
+ return findViewInsideOutShouldExist(root, mNextFocusDownId);
+ case FOCUS_FORWARD:
+ if (mNextFocusForwardId == View.NO_ID) return null;
+ return findViewInsideOutShouldExist(root, mNextFocusForwardId);
+ case FOCUS_BACKWARD: {
+ if (mID == View.NO_ID) return null;
+ final View rootView = root;
+ final View startView = this;
+ // Since we have forward links but no backward links, we need to find the view that
+ // forward links to this view. We can't just find the view with the specified ID
+ // because view IDs need not be unique throughout the tree.
+ return root.findViewByPredicateInsideOut(startView,
+ t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId)
+ == startView);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * If a user manually specified the next keyboard-navigation cluster for a particular direction,
+ * use the root to look up the view.
+ *
+ * @param root the root view of the hierarchy containing this view
+ * @param direction {@link #FOCUS_FORWARD} or {@link #FOCUS_BACKWARD}
+ * @return the user-specified next cluster, or {@code null} if there is none
+ */
+ View findUserSetNextKeyboardNavigationCluster(View root, @FocusDirection int direction) {
+ switch (direction) {
+ case FOCUS_FORWARD:
+ if (mNextClusterForwardId == View.NO_ID) return null;
+ return findViewInsideOutShouldExist(root, mNextClusterForwardId);
+ case FOCUS_BACKWARD: {
+ if (mID == View.NO_ID) return null;
+ final int id = mID;
+ return root.findViewByPredicateInsideOut(this,
+ (Predicate<View>) t -> t.mNextClusterForwardId == id);
+ }
+ }
+ return null;
+ }
+
+ private View findViewInsideOutShouldExist(View root, int id) {
+ return findViewInsideOutShouldExist(root, this, id);
+ }
+
+ private View findViewInsideOutShouldExist(View root, View start, int id) {
+ if (mMatchIdPredicate == null) {
+ mMatchIdPredicate = new MatchIdPredicate();
+ }
+ mMatchIdPredicate.mId = id;
+ View result = root.findViewByPredicateInsideOut(start, mMatchIdPredicate);
+ if (result == null) {
+ Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
+ }
+ return result;
+ }
+
+ /**
+ * Find and return all focusable views that are descendants of this view,
+ * possibly including this view if it is focusable itself.
+ *
+ * @param direction The direction of the focus
+ * @return A list of focusable views
+ */
+ public ArrayList<View> getFocusables(@FocusDirection int direction) {
+ ArrayList<View> result = new ArrayList<View>(24);
+ addFocusables(result, direction);
+ return result;
+ }
+
+ /**
+ * Add any focusable views that are descendants of this view (possibly
+ * including this view if it is focusable itself) to views. If we are in touch mode,
+ * only add views that are also focusable in touch mode.
+ *
+ * @param views Focusable views found so far
+ * @param direction The direction of the focus
+ */
+ public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
+ addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
+ }
+
+ /**
+ * Adds any focusable views that are descendants of this view (possibly
+ * including this view if it is focusable itself) to views. This method
+ * adds all focusable views regardless if we are in touch mode or
+ * only views focusable in touch mode if we are in touch mode or
+ * only views that can take accessibility focus if accessibility is enabled
+ * depending on the focusable mode parameter.
+ *
+ * @param views Focusable views found so far or null if all we are interested is
+ * the number of focusables.
+ * @param direction The direction of the focus.
+ * @param focusableMode The type of focusables to be added.
+ *
+ * @see #FOCUSABLES_ALL
+ * @see #FOCUSABLES_TOUCH_MODE
+ */
+ public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
+ @FocusableMode int focusableMode) {
+ if (views == null) {
+ return;
+ }
+ if (!canTakeFocus()) {
+ return;
+ }
+ if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
+ && !isFocusableInTouchMode()) {
+ return;
+ }
+ views.add(this);
+ }
+
+ /**
+ * Adds any keyboard navigation cluster roots that are descendants of this view (possibly
+ * including this view if it is a cluster root itself) to views.
+ *
+ * @param views Keyboard navigation cluster roots found so far
+ * @param direction Direction to look
+ */
+ public void addKeyboardNavigationClusters(
+ @NonNull Collection<View> views,
+ int direction) {
+ if (!isKeyboardNavigationCluster()) {
+ return;
+ }
+ if (!hasFocusable()) {
+ return;
+ }
+ views.add(this);
+ }
+
+ /**
+ * Finds the Views that contain given text. The containment is case insensitive.
+ * The search is performed by either the text that the View renders or the content
+ * description that describes the view for accessibility purposes and the view does
+ * not render or both. Clients can specify how the search is to be performed via
+ * passing the {@link #FIND_VIEWS_WITH_TEXT} and
+ * {@link #FIND_VIEWS_WITH_CONTENT_DESCRIPTION} flags.
+ *
+ * @param outViews The output list of matching Views.
+ * @param searched The text to match against.
+ *
+ * @see #FIND_VIEWS_WITH_TEXT
+ * @see #FIND_VIEWS_WITH_CONTENT_DESCRIPTION
+ * @see #setContentDescription(CharSequence)
+ */
+ public void findViewsWithText(ArrayList<View> outViews, CharSequence searched,
+ @FindViewFlags int flags) {
+ if (getAccessibilityNodeProvider() != null) {
+ if ((flags & FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS) != 0) {
+ outViews.add(this);
+ }
+ } else if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0
+ && (searched != null && searched.length() > 0)
+ && (mContentDescription != null && mContentDescription.length() > 0)) {
+ String searchedLowerCase = searched.toString().toLowerCase();
+ String contentDescriptionLowerCase = mContentDescription.toString().toLowerCase();
+ if (contentDescriptionLowerCase.contains(searchedLowerCase)) {
+ outViews.add(this);
+ }
+ }
+ }
+
+ /**
+ * Find and return all touchable views that are descendants of this view,
+ * possibly including this view if it is touchable itself.
+ *
+ * @return A list of touchable views
+ */
+ public ArrayList<View> getTouchables() {
+ ArrayList<View> result = new ArrayList<View>();
+ addTouchables(result);
+ return result;
+ }
+
+ /**
+ * Add any touchable views that are descendants of this view (possibly
+ * including this view if it is touchable itself) to views.
+ *
+ * @param views Touchable views found so far
+ */
+ public void addTouchables(ArrayList<View> views) {
+ final int viewFlags = mViewFlags;
+
+ if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE
+ || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
+ && (viewFlags & ENABLED_MASK) == ENABLED) {
+ views.add(this);
+ }
+ }
+
+ /**
+ * Returns whether this View is accessibility focused.
+ *
+ * @return True if this View is accessibility focused.
+ */
+ @InspectableProperty(hasAttributeId = false)
+ public boolean isAccessibilityFocused() {
+ return (mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) != 0;
+ }
+
+ /**
+ * Call this to try to give accessibility focus to this view.
+ *
+ * A view will not actually take focus if {@link AccessibilityManager#isEnabled()}
+ * returns false or the view is no visible or the view already has accessibility
+ * focus.
+ *
+ * See also {@link #focusSearch(int)}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * @return Whether this view actually took accessibility focus.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean requestAccessibilityFocus() {
+ AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
+ if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) {
+ return false;
+ }
+ if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+ if ((mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) == 0) {
+ mPrivateFlags2 |= PFLAG2_ACCESSIBILITY_FOCUSED;
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.setAccessibilityFocus(this, null);
+ }
+ invalidate();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Call this to try to clear accessibility focus of this view.
+ *
+ * See also {@link #focusSearch(int)}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void clearAccessibilityFocus() {
+ clearAccessibilityFocusNoCallbacks(0);
+
+ // Clear the global reference of accessibility focus if this view or
+ // any of its descendants had accessibility focus. This will NOT send
+ // an event or update internal state if focus is cleared from a
+ // descendant view, which may leave views in inconsistent states.
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
+ if (focusHost != null && ViewRootImpl.isViewDescendantOf(focusHost, this)) {
+ viewRootImpl.setAccessibilityFocus(null, null);
+ }
+ }
+ }
+
+ private void sendAccessibilityHoverEvent(int eventType) {
+ // Since we are not delivering to a client accessibility events from not
+ // important views (unless the clinet request that) we need to fire the
+ // event from the deepest view exposed to the client. As a consequence if
+ // the user crosses a not exposed view the client will see enter and exit
+ // of the exposed predecessor followed by and enter and exit of that same
+ // predecessor when entering and exiting the not exposed descendant. This
+ // is fine since the client has a clear idea which view is hovered at the
+ // price of a couple more events being sent. This is a simple and
+ // working solution.
+ View source = this;
+ while (true) {
+ if (source.includeForAccessibility()) {
+ source.sendAccessibilityEvent(eventType);
+ return;
+ }
+ ViewParent parent = source.getParent();
+ if (parent instanceof View) {
+ source = (View) parent;
+ } else {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Clears accessibility focus without calling any callback methods
+ * normally invoked in {@link #clearAccessibilityFocus()}. This method
+ * is used separately from that one for clearing accessibility focus when
+ * giving this focus to another view.
+ *
+ * @param action The action, if any, that led to focus being cleared. Set to
+ * AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS to specify that focus is moving within
+ * the window.
+ */
+ void clearAccessibilityFocusNoCallbacks(int action) {
+ if ((mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) != 0) {
+ mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_FOCUSED;
+ invalidate();
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ event.setAction(action);
+ if (mAccessibilityDelegate != null) {
+ mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event);
+ } else {
+ sendAccessibilityEventUnchecked(event);
+ }
+ }
+ }
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its
+ * descendants.
+ *
+ * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns
+ * false), or if it can't be focused due to other conditions (not focusable in touch mode
+ * ({@link #isFocusableInTouchMode}) while the device is in touch mode, not visible, not
+ * enabled, or has no size).
+ *
+ * See also {@link #focusSearch(int)}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * This is equivalent to calling {@link #requestFocus(int, Rect)} with arguments
+ * {@link #FOCUS_DOWN} and <code>null</code>.
+ *
+ * @return Whether this view or one of its descendants actually took focus.
+ */
+ public final boolean requestFocus() {
+ return requestFocus(View.FOCUS_DOWN);
+ }
+
+ /**
+ * This will request focus for whichever View was last focused within this
+ * cluster before a focus-jump out of it.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean restoreFocusInCluster(@FocusRealDirection int direction) {
+ // Prioritize focusableByDefault over algorithmic focus selection.
+ if (restoreDefaultFocus()) {
+ return true;
+ }
+ return requestFocus(direction);
+ }
+
+ /**
+ * This will request focus for whichever View not in a cluster was last focused before a
+ * focus-jump to a cluster. If no non-cluster View has previously had focus, this will focus
+ * the "first" focusable view it finds.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean restoreFocusNotInCluster() {
+ return requestFocus(View.FOCUS_DOWN);
+ }
+
+ /**
+ * Gives focus to the default-focus view in the view hierarchy that has this view as a root.
+ * If the default-focus view cannot be found, falls back to calling {@link #requestFocus(int)}.
+ *
+ * @return Whether this view or one of its descendants actually took focus
+ */
+ public boolean restoreDefaultFocus() {
+ return requestFocus(View.FOCUS_DOWN);
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its
+ * descendants and give it a hint about what direction focus is heading.
+ *
+ * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns
+ * false), or if it is focusable and it is not focusable in touch mode
+ * ({@link #isFocusableInTouchMode}) while the device is in touch mode.
+ *
+ * See also {@link #focusSearch(int)}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * This is equivalent to calling {@link #requestFocus(int, Rect)} with
+ * <code>null</code> set for the previously focused rectangle.
+ *
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @return Whether this view or one of its descendants actually took focus.
+ */
+ public final boolean requestFocus(int direction) {
+ return requestFocus(direction, null);
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its descendants
+ * and give it hints about the direction and a specific rectangle that the focus
+ * is coming from. The rectangle can help give larger views a finer grained hint
+ * about where focus is coming from, and therefore, where to show selection, or
+ * forward focus change internally.
+ *
+ * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns
+ * false), or if it is focusable and it is not focusable in touch mode
+ * ({@link #isFocusableInTouchMode}) while the device is in touch mode.
+ *
+ * A View will not take focus if it is not visible.
+ *
+ * A View will not take focus if one of its parents has
+ * {@link android.view.ViewGroup#getDescendantFocusability()} equal to
+ * {@link ViewGroup#FOCUS_BLOCK_DESCENDANTS}.
+ *
+ * See also {@link #focusSearch(int)}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * You may wish to override this method if your custom {@link View} has an internal
+ * {@link View} that it wishes to forward the request to.
+ *
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @param previouslyFocusedRect The rectangle (in this View's coordinate system)
+ * to give a finer grained hint about where focus is coming from. May be null
+ * if there is no hint.
+ * @return Whether this view or one of its descendants actually took focus.
+ */
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ return requestFocusNoSearch(direction, previouslyFocusedRect);
+ }
+
+ private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
+ // need to be focusable
+ if (!canTakeFocus()) {
+ return false;
+ }
+
+ // need to be focusable in touch mode if in touch mode
+ if (isInTouchMode() &&
+ (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
+ return false;
+ }
+
+ // need to not have any parents blocking us
+ if (hasAncestorThatBlocksDescendantFocus()) {
+ return false;
+ }
+
+ if (!isLayoutValid()) {
+ mPrivateFlags |= PFLAG_WANTS_FOCUS;
+ } else {
+ clearParentsWantFocus();
+ }
+
+ handleFocusGainInternal(direction, previouslyFocusedRect);
+ return true;
+ }
+
+ void clearParentsWantFocus() {
+ if (mParent instanceof View) {
+ ((View) mParent).mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ ((View) mParent).clearParentsWantFocus();
+ }
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its descendants. This is a
+ * special variant of {@link #requestFocus() } that will allow views that are not focusable in
+ * touch mode to request focus when they are touched.
+ *
+ * @return Whether this view or one of its descendants actually took focus.
+ *
+ * @see #isInTouchMode()
+ *
+ */
+ public final boolean requestFocusFromTouch() {
+ // Leave touch mode if we need to
+ if (isInTouchMode()) {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null) {
+ viewRoot.ensureTouchMode(false);
+ }
+ }
+ return requestFocus(View.FOCUS_DOWN);
+ }
+
+ /**
+ * @return Whether any ancestor of this view blocks descendant focus.
+ */
+ private boolean hasAncestorThatBlocksDescendantFocus() {
+ final boolean focusableInTouchMode = isFocusableInTouchMode();
+ ViewParent ancestor = mParent;
+ while (ancestor instanceof ViewGroup) {
+ final ViewGroup vgAncestor = (ViewGroup) ancestor;
+ if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
+ || (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {
+ return true;
+ } else {
+ ancestor = vgAncestor.getParent();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the mode for determining whether this View is important for accessibility.
+ * A view is important for accessibility if it fires accessibility events and if it
+ * is reported to accessibility services that query the screen.
+ *
+ * @return The mode for determining whether a view is important for accessibility, one
+ * of {@link #IMPORTANT_FOR_ACCESSIBILITY_AUTO}, {@link #IMPORTANT_FOR_ACCESSIBILITY_YES},
+ * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO}, or
+ * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS}.
+ *
+ * @attr ref android.R.styleable#View_importantForAccessibility
+ *
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_YES
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_NO
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ */
+ @ViewDebug.ExportedProperty(category = "accessibility", mapping = {
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_AUTO, to = "auto"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_YES, to = "yes"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_NO, to = "no"),
+ @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
+ to = "noHideDescendants")
+ })
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = IMPORTANT_FOR_ACCESSIBILITY_AUTO, name = "auto"),
+ @EnumEntry(value = IMPORTANT_FOR_ACCESSIBILITY_YES, name = "yes"),
+ @EnumEntry(value = IMPORTANT_FOR_ACCESSIBILITY_NO, name = "no"),
+ @EnumEntry(value = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
+ name = "noHideDescendants"),
+ })
+ public int getImportantForAccessibility() {
+ return (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK)
+ >> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
+ }
+
+ /**
+ * Sets the live region mode for this view. This indicates to accessibility
+ * services whether they should automatically notify the user about changes
+ * to the view's content description or text, or to the content descriptions
+ * or text of the view's children (where applicable).
+ * <p>
+ * For example, in a login screen with a TextView that displays an "incorrect
+ * password" notification, that view should be marked as a live region with
+ * mode {@link #ACCESSIBILITY_LIVE_REGION_POLITE}.
+ * <p>
+ * To disable change notifications for this view, use
+ * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. This is the default live region
+ * mode for most views.
+ * <p>
+ * To indicate that the user should be notified of changes, use
+ * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}.
+ * <p>
+ * If the view's changes should interrupt ongoing speech and notify the user
+ * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}.
+ *
+ * @param mode The live region mode for this view, one of:
+ * <ul>
+ * <li>{@link #ACCESSIBILITY_LIVE_REGION_NONE}
+ * <li>{@link #ACCESSIBILITY_LIVE_REGION_POLITE}
+ * <li>{@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}
+ * </ul>
+ * @attr ref android.R.styleable#View_accessibilityLiveRegion
+ */
+ public void setAccessibilityLiveRegion(int mode) {
+ if (mode != getAccessibilityLiveRegion()) {
+ mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
+ mPrivateFlags2 |= (mode << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT)
+ & PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+
+ /**
+ * Gets the live region mode for this View.
+ *
+ * @return The live region mode for the view.
+ *
+ * @attr ref android.R.styleable#View_accessibilityLiveRegion
+ *
+ * @see #setAccessibilityLiveRegion(int)
+ */
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = ACCESSIBILITY_LIVE_REGION_NONE, name = "none"),
+ @EnumEntry(value = ACCESSIBILITY_LIVE_REGION_POLITE, name = "polite"),
+ @EnumEntry(value = ACCESSIBILITY_LIVE_REGION_ASSERTIVE, name = "assertive")
+ })
+ public int getAccessibilityLiveRegion() {
+ return (mPrivateFlags2 & PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK)
+ >> PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT;
+ }
+
+ /**
+ * Sets how to determine whether this view is important for accessibility
+ * which is if it fires accessibility events and if it is reported to
+ * accessibility services that query the screen.
+ *
+ * @param mode How to determine whether this view is important for accessibility.
+ *
+ * @attr ref android.R.styleable#View_importantForAccessibility
+ *
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_YES
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_NO
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ */
+ public void setImportantForAccessibility(int mode) {
+ final int oldMode = getImportantForAccessibility();
+ if (mode != oldMode) {
+ final boolean hideDescendants =
+ mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
+
+ // If this node or its descendants are no longer important, try to
+ // clear accessibility focus.
+ if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO || hideDescendants) {
+ final View focusHost = findAccessibilityFocusHost(hideDescendants);
+ if (focusHost != null) {
+ focusHost.clearAccessibilityFocus();
+ }
+ }
+
+ // If we're moving between AUTO and another state, we might not need
+ // to send a subtree changed notification. We'll store the computed
+ // importance, since we'll need to check it later to make sure.
+ final boolean maySkipNotify = oldMode == IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ || mode == IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+ final boolean oldIncludeForAccessibility = maySkipNotify && includeForAccessibility();
+ mPrivateFlags2 &= ~PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK;
+ mPrivateFlags2 |= (mode << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT)
+ & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK;
+ if (!maySkipNotify || oldIncludeForAccessibility != includeForAccessibility()) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ } else {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+ }
+
+ /**
+ * Returns the view within this view's hierarchy that is hosting
+ * accessibility focus.
+ *
+ * @param searchDescendants whether to search for focus in descendant views
+ * @return the view hosting accessibility focus, or {@code null}
+ */
+ private View findAccessibilityFocusHost(boolean searchDescendants) {
+ if (isAccessibilityFocusedViewOrHost()) {
+ return this;
+ }
+
+ if (searchDescendants) {
+ final ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null) {
+ final View focusHost = viewRoot.getAccessibilityFocusedHost();
+ if (focusHost != null && ViewRootImpl.isViewDescendantOf(focusHost, this)) {
+ return focusHost;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes whether this view should be exposed for accessibility. In
+ * general, views that are interactive or provide information are exposed
+ * while views that serve only as containers are hidden.
+ * <p>
+ * If an ancestor of this view has importance
+ * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS}, this method
+ * returns <code>false</code>.
+ * <p>
+ * Otherwise, the value is computed according to the view's
+ * {@link #getImportantForAccessibility()} value:
+ * <ol>
+ * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_NO} or
+ * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS}, return <code>false
+ * </code>
+ * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_YES}, return <code>true</code>
+ * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_AUTO}, return <code>true</code> if
+ * view satisfies any of the following:
+ * <ul>
+ * <li>Is actionable, e.g. {@link #isClickable()},
+ * {@link #isLongClickable()}, or {@link #isFocusable()}
+ * <li>Has an {@link AccessibilityDelegate}
+ * <li>Has an interaction listener, e.g. {@link OnTouchListener},
+ * {@link OnKeyListener}, etc.
+ * <li>Is an accessibility live region, e.g.
+ * {@link #getAccessibilityLiveRegion()} is not
+ * {@link #ACCESSIBILITY_LIVE_REGION_NONE}.
+ * </ul>
+ * <li>Has an accessibility pane title, see {@link #setAccessibilityPaneTitle}</li>
+ * </ol>
+ *
+ * @return Whether the view is exposed for accessibility.
+ * @see #setImportantForAccessibility(int)
+ * @see #getImportantForAccessibility()
+ */
+ public boolean isImportantForAccessibility() {
+ final int mode = getImportantForAccessibility();
+ if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO
+ || mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
+ return false;
+ }
+
+ // Check parent mode to ensure we're not hidden.
+ ViewParent parent = mParent;
+ while (parent instanceof View) {
+ if (((View) parent).getImportantForAccessibility()
+ == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
+ return false;
+ }
+ parent = parent.getParent();
+ }
+
+ return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
+ || hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
+ || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE
+ || isAccessibilityPane();
+ }
+
+ /**
+ * Gets the parent for accessibility purposes. Note that the parent for
+ * accessibility is not necessary the immediate parent. It is the first
+ * predecessor that is important for accessibility.
+ *
+ * @return The parent for accessibility purposes.
+ */
+ public ViewParent getParentForAccessibility() {
+ if (mParent instanceof View) {
+ View parentView = (View) mParent;
+ if (parentView.includeForAccessibility()) {
+ return mParent;
+ } else {
+ return mParent.getParentForAccessibility();
+ }
+ }
+ return null;
+ }
+
+ /** @hide */
+ View getSelfOrParentImportantForA11y() {
+ if (isImportantForAccessibility()) return this;
+ ViewParent parent = getParentForAccessibility();
+ if (parent instanceof View) return (View) parent;
+ return null;
+ }
+
+ /**
+ * Adds the children of this View relevant for accessibility to the given list
+ * as output. Since some Views are not important for accessibility the added
+ * child views are not necessarily direct children of this view, rather they are
+ * the first level of descendants important for accessibility.
+ *
+ * @param outChildren The output list that will receive children for accessibility.
+ */
+ public void addChildrenForAccessibility(ArrayList<View> outChildren) {
+
+ }
+
+ /**
+ * Whether to regard this view for accessibility. A view is regarded for
+ * accessibility if it is important for accessibility or the querying
+ * accessibility service has explicitly requested that view not
+ * important for accessibility are regarded.
+ *
+ * @return Whether to regard the view for accessibility.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean includeForAccessibility() {
+ if (mAttachInfo != null) {
+ return (mAttachInfo.mAccessibilityFetchFlags
+ & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0
+ || isImportantForAccessibility();
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the View is considered actionable from
+ * accessibility perspective. Such view are important for
+ * accessibility.
+ *
+ * @return True if the view is actionable for accessibility.
+ *
+ * @hide
+ */
+ public boolean isActionableForAccessibility() {
+ return (isClickable() || isLongClickable() || isFocusable());
+ }
+
+ /**
+ * Returns whether the View has registered callbacks which makes it
+ * important for accessibility.
+ *
+ * @return True if the view is actionable for accessibility.
+ */
+ private boolean hasListenersForAccessibility() {
+ ListenerInfo info = getListenerInfo();
+ return mTouchDelegate != null || info.mOnKeyListener != null
+ || info.mOnTouchListener != null || info.mOnGenericMotionListener != null
+ || info.mOnHoverListener != null || info.mOnDragListener != null;
+ }
+
+ /**
+ * Notifies that the accessibility state of this view changed. The change
+ * is local to this view and does not represent structural changes such
+ * as children and parent. For example, the view became focusable. The
+ * notification is at at most once every
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}
+ * to avoid unnecessary load to the system. Also once a view has a pending
+ * notification this method is a NOP until the notification has been sent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void notifyViewAccessibilityStateChangedIfNeeded(int changeType) {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
+ return;
+ }
+
+ // Changes to views with a pane title count as window state changes, as the pane title
+ // marks them as significant parts of the UI.
+ if ((changeType != AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE)
+ && isAccessibilityPane()) {
+ // If the pane isn't visible, content changed events are sufficient unless we're
+ // reporting that the view just disappeared
+ if ((getVisibility() == VISIBLE)
+ || (changeType == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED)) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
+ onInitializeAccessibilityEvent(event);
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ event.setContentChangeTypes(changeType);
+ event.setSource(this);
+ onPopulateAccessibilityEvent(event);
+ if (mParent != null) {
+ try {
+ mParent.requestSendAccessibilityEvent(this, event);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName()
+ + " does not fully implement ViewParent", e);
+ }
+ }
+ return;
+ }
+ }
+
+ // If this is a live region, we should send a subtree change event
+ // from this view immediately. Otherwise, we can let it propagate up.
+ if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(changeType);
+ sendAccessibilityEventUnchecked(event);
+ } else if (mParent != null) {
+ try {
+ mParent.notifySubtreeAccessibilityStateChanged(this, this, changeType);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ }
+
+ /**
+ * Notifies that the accessibility state of this view changed. The change
+ * is *not* local to this view and does represent structural changes such
+ * as children and parent. For example, the view size changed. The
+ * notification is at at most once every
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}
+ * to avoid unnecessary load to the system. Also once a view has a pending
+ * notification this method is a NOP until the notification has been sent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
+ return;
+ }
+
+ if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
+ mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+ if (mParent != null) {
+ try {
+ mParent.notifySubtreeAccessibilityStateChanged(
+ this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ }
+ }
+
+ private void notifySubtreeAccessibilityStateChangedByParentIfNeeded() {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return;
+ }
+
+ final View sendA11yEventView = (View) getParentForAccessibility();
+ if (sendA11yEventView != null && sendA11yEventView.isShown()) {
+ sendA11yEventView.notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * Changes the visibility of this View without triggering any other changes. This should only
+ * be used by animation frameworks, such as {@link android.transition.Transition}, where
+ * visibility changes should not adjust focus or trigger a new layout. Application developers
+ * should use {@link #setVisibility} instead to ensure that the hierarchy is correctly updated.
+ *
+ * <p>Only call this method when a temporary visibility must be applied during an
+ * animation and the original visibility value is guaranteed to be reset after the
+ * animation completes. Use {@link #setVisibility} in all other cases.</p>
+ *
+ * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ * @see #setVisibility(int)
+ */
+ public void setTransitionVisibility(@Visibility int visibility) {
+ mViewFlags = (mViewFlags & ~View.VISIBILITY_MASK) | visibility;
+ }
+
+ /**
+ * Reset the flag indicating the accessibility state of the subtree rooted
+ * at this view changed.
+ */
+ void resetSubtreeAccessibilityStateChanged() {
+ mPrivateFlags2 &= ~PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+ }
+
+ /**
+ * Report an accessibility action to this view's parents for delegated processing.
+ *
+ * <p>Implementations of {@link #performAccessibilityAction(int, Bundle)} may internally
+ * call this method to delegate an accessibility action to a supporting parent. If the parent
+ * returns true from its
+ * {@link ViewParent#onNestedPrePerformAccessibilityAction(View, int, android.os.Bundle)}
+ * method this method will return true to signify that the action was consumed.</p>
+ *
+ * <p>This method is useful for implementing nested scrolling child views. If
+ * {@link #isNestedScrollingEnabled()} returns true and the action is a scrolling action
+ * a custom view implementation may invoke this method to allow a parent to consume the
+ * scroll first. If this method returns true the custom view should skip its own scrolling
+ * behavior.</p>
+ *
+ * @param action Accessibility action to delegate
+ * @param arguments Optional action arguments
+ * @return true if the action was consumed by a parent
+ */
+ public boolean dispatchNestedPrePerformAccessibilityAction(int action, Bundle arguments) {
+ for (ViewParent p = getParent(); p != null; p = p.getParent()) {
+ if (p.onNestedPrePerformAccessibilityAction(this, action, arguments)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Performs the specified accessibility action on the view. For
+ * possible accessibility actions look at {@link AccessibilityNodeInfo}.
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#performAccessibilityAction(View, int, Bundle)}
+ * is responsible for handling this call.
+ * </p>
+ *
+ * <p>The default implementation will delegate
+ * {@link AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD} and
+ * {@link AccessibilityNodeInfo#ACTION_SCROLL_FORWARD} to nested scrolling parents if
+ * {@link #isNestedScrollingEnabled() nested scrolling is enabled} on this view.</p>
+ *
+ * @param action The action to perform.
+ * @param arguments Optional action arguments.
+ * @return Whether the action was performed.
+ */
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (mAccessibilityDelegate != null) {
+ return mAccessibilityDelegate.performAccessibilityAction(this, action, arguments);
+ } else {
+ return performAccessibilityActionInternal(action, arguments);
+ }
+ }
+
+ /**
+ * @see #performAccessibilityAction(int, Bundle)
+ *
+ * Note: Called from the default {@link AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (isNestedScrollingEnabled()
+ && (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
+ || action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
+ || action == R.id.accessibilityActionScrollUp
+ || action == R.id.accessibilityActionScrollLeft
+ || action == R.id.accessibilityActionScrollDown
+ || action == R.id.accessibilityActionScrollRight)) {
+ if (dispatchNestedPrePerformAccessibilityAction(action, arguments)) {
+ return true;
+ }
+ }
+
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK: {
+ if (isClickable()) {
+ performClickInternal();
+ return true;
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
+ if (isLongClickable()) {
+ performLongClick();
+ return true;
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_FOCUS: {
+ if (!hasFocus()) {
+ // Get out of touch mode since accessibility
+ // wants to move focus around.
+ getViewRootImpl().ensureTouchMode(false);
+ return requestFocus();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
+ if (hasFocus()) {
+ clearFocus();
+ return !isFocused();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_SELECT: {
+ if (!isSelected()) {
+ setSelected(true);
+ return isSelected();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: {
+ if (isSelected()) {
+ setSelected(false);
+ return !isSelected();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+ if (!isAccessibilityFocused()) {
+ return requestAccessibilityFocus();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+ if (isAccessibilityFocused()) {
+ clearAccessibilityFocus();
+ return true;
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: {
+ if (arguments != null) {
+ final int granularity = arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ final boolean extendSelection = arguments.getBoolean(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
+ return traverseAtGranularity(granularity, true, extendSelection);
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
+ if (arguments != null) {
+ final int granularity = arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ final boolean extendSelection = arguments.getBoolean(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
+ return traverseAtGranularity(granularity, false, extendSelection);
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
+ CharSequence text = getIterableTextForAccessibility();
+ if (text == null) {
+ return false;
+ }
+ final int start = (arguments != null) ? arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1;
+ final int end = (arguments != null) ? arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1;
+ // Only cursor position can be specified (selection length == 0)
+ if ((getAccessibilitySelectionStart() != start
+ || getAccessibilitySelectionEnd() != end)
+ && (start == end)) {
+ setAccessibilitySelection(start, end);
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ return true;
+ }
+ } break;
+ case R.id.accessibilityActionShowOnScreen: {
+ if (mAttachInfo != null) {
+ final Rect r = mAttachInfo.mTmpInvalRect;
+ getDrawingRect(r);
+ return requestRectangleOnScreen(r, true);
+ }
+ } break;
+ case R.id.accessibilityActionContextClick: {
+ if (isContextClickable()) {
+ performContextClick();
+ return true;
+ }
+ } break;
+ case R.id.accessibilityActionShowTooltip: {
+ if ((mTooltipInfo != null) && (mTooltipInfo.mTooltipPopup != null)) {
+ // Tooltip already showing
+ return false;
+ }
+ return showLongClickTooltip(0, 0);
+ }
+ case R.id.accessibilityActionHideTooltip: {
+ if ((mTooltipInfo == null) || (mTooltipInfo.mTooltipPopup == null)) {
+ // No tooltip showing
+ return false;
+ }
+ hideTooltip();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean traverseAtGranularity(int granularity, boolean forward,
+ boolean extendSelection) {
+ CharSequence text = getIterableTextForAccessibility();
+ if (text == null || text.length() == 0) {
+ return false;
+ }
+ TextSegmentIterator iterator = getIteratorForGranularity(granularity);
+ if (iterator == null) {
+ return false;
+ }
+ int current = getAccessibilitySelectionEnd();
+ if (current == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) {
+ current = forward ? 0 : text.length();
+ }
+ final int[] range = forward ? iterator.following(current) : iterator.preceding(current);
+ if (range == null) {
+ return false;
+ }
+ final int segmentStart = range[0];
+ final int segmentEnd = range[1];
+ int selectionStart;
+ int selectionEnd;
+ if (extendSelection && isAccessibilitySelectionExtendable()) {
+ selectionStart = getAccessibilitySelectionStart();
+ if (selectionStart == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) {
+ selectionStart = forward ? segmentStart : segmentEnd;
+ }
+ selectionEnd = forward ? segmentEnd : segmentStart;
+ } else {
+ selectionStart = selectionEnd= forward ? segmentEnd : segmentStart;
+ }
+ setAccessibilitySelection(selectionStart, selectionEnd);
+ final int action = forward ? AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+ : AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY;
+ sendViewTextTraversedAtGranularityEvent(action, granularity, segmentStart, segmentEnd);
+ return true;
+ }
+
+ /**
+ * Gets the text reported for accessibility purposes.
+ *
+ * @return The accessibility text.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public CharSequence getIterableTextForAccessibility() {
+ return getContentDescription();
+ }
+
+ /**
+ * Gets whether accessibility selection can be extended.
+ *
+ * @return If selection is extensible.
+ *
+ * @hide
+ */
+ public boolean isAccessibilitySelectionExtendable() {
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public int getAccessibilitySelectionStart() {
+ return mAccessibilityCursorPosition;
+ }
+
+ /**
+ * @hide
+ */
+ public int getAccessibilitySelectionEnd() {
+ return getAccessibilitySelectionStart();
+ }
+
+ /**
+ * @hide
+ */
+ public void setAccessibilitySelection(int start, int end) {
+ if (start == end && end == mAccessibilityCursorPosition) {
+ return;
+ }
+ if (start >= 0 && start == end && end <= getIterableTextForAccessibility().length()) {
+ mAccessibilityCursorPosition = start;
+ } else {
+ mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
+ }
+
+ private void sendViewTextTraversedAtGranularityEvent(int action, int granularity,
+ int fromIndex, int toIndex) {
+ if (mParent == null) {
+ return;
+ }
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
+ onInitializeAccessibilityEvent(event);
+ onPopulateAccessibilityEvent(event);
+ event.setFromIndex(fromIndex);
+ event.setToIndex(toIndex);
+ event.setAction(action);
+ event.setMovementGranularity(granularity);
+ mParent.requestSendAccessibilityEvent(this, event);
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public TextSegmentIterator getIteratorForGranularity(int granularity) {
+ switch (granularity) {
+ case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: {
+ CharSequence text = getIterableTextForAccessibility();
+ if (text != null && text.length() > 0) {
+ CharacterTextSegmentIterator iterator =
+ CharacterTextSegmentIterator.getInstance(
+ mContext.getResources().getConfiguration().locale);
+ iterator.initialize(text.toString());
+ return iterator;
+ }
+ } break;
+ case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: {
+ CharSequence text = getIterableTextForAccessibility();
+ if (text != null && text.length() > 0) {
+ WordTextSegmentIterator iterator =
+ WordTextSegmentIterator.getInstance(
+ mContext.getResources().getConfiguration().locale);
+ iterator.initialize(text.toString());
+ return iterator;
+ }
+ } break;
+ case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH: {
+ CharSequence text = getIterableTextForAccessibility();
+ if (text != null && text.length() > 0) {
+ ParagraphTextSegmentIterator iterator =
+ ParagraphTextSegmentIterator.getInstance();
+ iterator.initialize(text.toString());
+ return iterator;
+ }
+ } break;
+ }
+ return null;
+ }
+
+ /**
+ * Tells whether the {@link View} is in the state between {@link #onStartTemporaryDetach()}
+ * and {@link #onFinishTemporaryDetach()}.
+ *
+ * <p>This method always returns {@code true} when called directly or indirectly from
+ * {@link #onStartTemporaryDetach()}. The return value when called directly or indirectly from
+ * {@link #onFinishTemporaryDetach()}, however, depends on the OS version.
+ * <ul>
+ * <li>{@code true} on {@link android.os.Build.VERSION_CODES#N API 24}</li>
+ * <li>{@code false} on {@link android.os.Build.VERSION_CODES#N_MR1 API 25}} and later</li>
+ * </ul>
+ * </p>
+ *
+ * @return {@code true} when the View is in the state between {@link #onStartTemporaryDetach()}
+ * and {@link #onFinishTemporaryDetach()}.
+ */
+ public final boolean isTemporarilyDetached() {
+ return (mPrivateFlags3 & PFLAG3_TEMPORARY_DETACH) != 0;
+ }
+
+ /**
+ * Dispatch {@link #onStartTemporaryDetach()} to this View and its direct children if this is
+ * a container View.
+ */
+ @CallSuper
+ public void dispatchStartTemporaryDetach() {
+ mPrivateFlags3 |= PFLAG3_TEMPORARY_DETACH;
+ notifyEnterOrExitForAutoFillIfNeeded(false);
+ notifyAppearedOrDisappearedForContentCaptureIfNeeded(false);
+ onStartTemporaryDetach();
+ }
+
+ /**
+ * This is called when a container is going to temporarily detach a child, with
+ * {@link ViewGroup#detachViewFromParent(View) ViewGroup.detachViewFromParent}.
+ * It will either be followed by {@link #onFinishTemporaryDetach()} or
+ * {@link #onDetachedFromWindow()} when the container is done.
+ */
+ public void onStartTemporaryDetach() {
+ removeUnsetPressCallback();
+ mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT;
+ }
+
+ /**
+ * Dispatch {@link #onFinishTemporaryDetach()} to this View and its direct children if this is
+ * a container View.
+ */
+ @CallSuper
+ public void dispatchFinishTemporaryDetach() {
+ mPrivateFlags3 &= ~PFLAG3_TEMPORARY_DETACH;
+ onFinishTemporaryDetach();
+ if (hasWindowFocus() && hasFocus()) {
+ notifyFocusChangeToImeFocusController(true /* hasFocus */);
+ }
+ notifyEnterOrExitForAutoFillIfNeeded(true);
+ notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
+ }
+
+ /**
+ * Called after {@link #onStartTemporaryDetach} when the container is done
+ * changing the view.
+ */
+ public void onFinishTemporaryDetach() {
+ }
+
+ /**
+ * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState}
+ * for this view's window. Returns null if the view is not currently attached
+ * to the window. Normally you will not need to use this directly, but
+ * just use the standard high-level event callbacks like
+ * {@link #onKeyDown(int, KeyEvent)}.
+ */
+ public KeyEvent.DispatcherState getKeyDispatcherState() {
+ return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null;
+ }
+
+ /**
+ * Dispatch a key event before it is processed by any input method
+ * associated with the view hierarchy. This can be used to intercept
+ * key events in special situations before the IME consumes them; a
+ * typical example would be handling the BACK key to update the application's
+ * UI instead of allowing the IME to see it and close itself.
+ *
+ * @param event The key event to be dispatched.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ return onKeyPreIme(event.getKeyCode(), event);
+ }
+
+ /**
+ * Dispatch a key event to the next view on the focus path. This path runs
+ * from the top of the view tree down to the currently focused view. If this
+ * view has focus, it will dispatch to itself. Otherwise it will dispatch
+ * the next node down the focus path. This method also fires any key
+ * listeners.
+ *
+ * @param event The key event to be dispatched.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onKeyEvent(event, 0);
+ }
+
+ // Give any attached key listener a first crack at the event.
+ //noinspection SimplifiableIfStatement
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
+ && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
+ return true;
+ }
+
+ if (event.dispatch(this, mAttachInfo != null
+ ? mAttachInfo.mKeyDispatchState : null, this)) {
+ return true;
+ }
+
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
+ }
+ return false;
+ }
+
+ /**
+ * Dispatches a key shortcut event.
+ *
+ * @param event The key event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return onKeyShortcut(event.getKeyCode(), event);
+ }
+
+ /**
+ * Pass the touch screen motion event down to the target view, or this
+ * view if it is the target.
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ // If the event should be handled by accessibility focus first.
+ if (event.isTargetAccessibilityFocus()) {
+ // We don't have focus or no virtual descendant has it, do not handle the event.
+ if (!isAccessibilityFocusedViewOrHost()) {
+ return false;
+ }
+ // We have focus and got the event, then use normal event dispatch.
+ event.setTargetAccessibilityFocus(false);
+ }
+ boolean result = false;
+
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTouchEvent(event, 0);
+ }
+
+ final int actionMasked = event.getActionMasked();
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ // Defensive cleanup for new gesture
+ stopNestedScroll();
+ }
+
+ if (onFilterTouchEventForSecurity(event)) {
+ if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
+ result = true;
+ }
+ //noinspection SimplifiableIfStatement
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnTouchListener != null
+ && (mViewFlags & ENABLED_MASK) == ENABLED
+ && li.mOnTouchListener.onTouch(this, event)) {
+ result = true;
+ }
+
+ if (!result && onTouchEvent(event)) {
+ result = true;
+ }
+ }
+
+ if (!result && mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
+ }
+
+ // Clean up after nested scrolls if this is the end of a gesture;
+ // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
+ // of the gesture.
+ if (actionMasked == MotionEvent.ACTION_UP ||
+ actionMasked == MotionEvent.ACTION_CANCEL ||
+ (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
+ stopNestedScroll();
+ }
+
+ return result;
+ }
+
+ boolean isAccessibilityFocusedViewOrHost() {
+ return isAccessibilityFocused() || (getViewRootImpl() != null && getViewRootImpl()
+ .getAccessibilityFocusedHost() == this);
+ }
+
+ /**
+ * Returns whether this view can receive pointer events.
+ *
+ * @return {@code true} if this view can receive pointer events.
+ * @hide
+ */
+ protected boolean canReceivePointerEvents() {
+ return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
+ }
+
+ /**
+ * Filter the touch event to apply security policies.
+ *
+ * @param event The motion event to be filtered.
+ * @return True if the event should be dispatched, false if the event should be dropped.
+ *
+ * @see #getFilterTouchesWhenObscured
+ */
+ public boolean onFilterTouchEventForSecurity(MotionEvent event) {
+ //noinspection RedundantIfStatement
+ if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
+ && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
+ // Window is obscured, drop this touch.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Pass a trackball motion event down to the focused view.
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTrackballEvent(event, 0);
+ }
+
+ return onTrackballEvent(event);
+ }
+
+ /**
+ * Pass a captured pointer event down to the focused view.
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchCapturedPointerEvent(MotionEvent event) {
+ if (!hasPointerCapture()) {
+ return false;
+ }
+ //noinspection SimplifiableIfStatement
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnCapturedPointerListener != null
+ && li.mOnCapturedPointerListener.onCapturedPointer(this, event)) {
+ return true;
+ }
+ return onCapturedPointerEvent(event);
+ }
+
+ /**
+ * Dispatch a generic motion event.
+ * <p>
+ * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER}
+ * are delivered to the view under the pointer. All other generic motion events are
+ * delivered to the focused view. Hover events are handled specially and are delivered
+ * to {@link #onHoverEvent(MotionEvent)}.
+ * </p>
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0);
+ }
+
+ final int source = event.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ final int action = event.getAction();
+ if (action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_MOVE
+ || action == MotionEvent.ACTION_HOVER_EXIT) {
+ if (dispatchHoverEvent(event)) {
+ return true;
+ }
+ } else if (dispatchGenericPointerEvent(event)) {
+ return true;
+ }
+ } else if (dispatchGenericFocusedEvent(event)) {
+ return true;
+ }
+
+ if (dispatchGenericMotionEventInternal(event)) {
+ return true;
+ }
+
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
+ }
+ return false;
+ }
+
+ private boolean dispatchGenericMotionEventInternal(MotionEvent event) {
+ //noinspection SimplifiableIfStatement
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnGenericMotionListener != null
+ && (mViewFlags & ENABLED_MASK) == ENABLED
+ && li.mOnGenericMotionListener.onGenericMotion(this, event)) {
+ return true;
+ }
+
+ if (onGenericMotionEvent(event)) {
+ return true;
+ }
+
+ final int actionButton = event.getActionButton();
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_BUTTON_PRESS:
+ if (isContextClickable() && !mInContextButtonPress && !mHasPerformedLongPress
+ && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
+ || actionButton == MotionEvent.BUTTON_SECONDARY)) {
+ if (performContextClick(event.getX(), event.getY())) {
+ mInContextButtonPress = true;
+ setPressed(true, event.getX(), event.getY());
+ removeTapCallback();
+ removeLongPressCallback();
+ return true;
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_BUTTON_RELEASE:
+ if (mInContextButtonPress && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
+ || actionButton == MotionEvent.BUTTON_SECONDARY)) {
+ mInContextButtonPress = false;
+ mIgnoreNextUpEvent = true;
+ }
+ break;
+ }
+
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
+ }
+ return false;
+ }
+
+ /**
+ * Dispatch a hover event.
+ * <p>
+ * Do not call this method directly.
+ * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead.
+ * </p>
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ protected boolean dispatchHoverEvent(MotionEvent event) {
+ ListenerInfo li = mListenerInfo;
+ //noinspection SimplifiableIfStatement
+ if (li != null && li.mOnHoverListener != null
+ && (mViewFlags & ENABLED_MASK) == ENABLED
+ && li.mOnHoverListener.onHover(this, event)) {
+ return true;
+ }
+
+ return onHoverEvent(event);
+ }
+
+ /**
+ * Returns true if the view has a child to which it has recently sent
+ * {@link MotionEvent#ACTION_HOVER_ENTER}. If this view is hovered and
+ * it does not have a hovered child, then it must be the innermost hovered view.
+ * @hide
+ */
+ protected boolean hasHoveredChild() {
+ return false;
+ }
+
+ /**
+ * Returns true if the given point, in local coordinates, is inside the hovered child.
+ *
+ * @hide
+ */
+ protected boolean pointInHoveredChild(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Dispatch a generic motion event to the view under the first pointer.
+ * <p>
+ * Do not call this method directly.
+ * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead.
+ * </p>
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ protected boolean dispatchGenericPointerEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Dispatch a generic motion event to the currently focused view.
+ * <p>
+ * Do not call this method directly.
+ * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead.
+ * </p>
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Dispatch a pointer event.
+ * <p>
+ * Dispatches touch related pointer events to {@link #onTouchEvent(MotionEvent)} and all
+ * other events to {@link #onGenericMotionEvent(MotionEvent)}. This separation of concerns
+ * reinforces the invariant that {@link #onTouchEvent(MotionEvent)} is really about touches
+ * and should not be expected to handle other pointing device features.
+ * </p>
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final boolean dispatchPointerEvent(MotionEvent event) {
+ if (event.isTouchEvent()) {
+ return dispatchTouchEvent(event);
+ } else {
+ return dispatchGenericMotionEvent(event);
+ }
+ }
+
+ /**
+ * Called when the window containing this view gains or loses window focus.
+ * ViewGroups should override to route to their children.
+ *
+ * @param hasFocus True if the window containing this view now has focus,
+ * false otherwise.
+ */
+ public void dispatchWindowFocusChanged(boolean hasFocus) {
+ onWindowFocusChanged(hasFocus);
+ }
+
+ /**
+ * Called when the window containing this view gains or loses focus. Note
+ * that this is separate from view focus: to receive key events, both
+ * your view and its window must have focus. If a window is displayed
+ * on top of yours that takes input focus, then your own window will lose
+ * focus but the view focus will remain unchanged.
+ *
+ * @param hasWindowFocus True if the window containing this view now has
+ * focus, false otherwise.
+ */
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (!hasWindowFocus) {
+ if (isPressed()) {
+ setPressed(false);
+ }
+ mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
+ if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
+ notifyFocusChangeToImeFocusController(false /* hasFocus */);
+ }
+ removeLongPressCallback();
+ removeTapCallback();
+ onFocusLost();
+ } else if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
+ notifyFocusChangeToImeFocusController(true /* hasFocus */);
+ }
+
+ refreshDrawableState();
+ }
+
+ /**
+ * Returns true if this view is in a window that currently has window focus.
+ * Note that this is not the same as the view itself having focus.
+ *
+ * @return True if this view is in a window that currently has window focus.
+ */
+ public boolean hasWindowFocus() {
+ return mAttachInfo != null && mAttachInfo.mHasWindowFocus;
+ }
+
+ /**
+ * @return {@code true} if this view is in a window that currently has IME focusable state.
+ * @hide
+ */
+ public boolean hasImeFocus() {
+ return getViewRootImpl() != null && getViewRootImpl().getImeFocusController().hasImeFocus();
+ }
+
+ /**
+ * Dispatch a view visibility change down the view hierarchy.
+ * ViewGroups should override to route to their children.
+ * @param changedView The view whose visibility changed. Could be 'this' or
+ * an ancestor view.
+ * @param visibility The new visibility of changedView: {@link #VISIBLE},
+ * {@link #INVISIBLE} or {@link #GONE}.
+ */
+ protected void dispatchVisibilityChanged(@NonNull View changedView,
+ @Visibility int visibility) {
+ onVisibilityChanged(changedView, visibility);
+ }
+
+ /**
+ * Called when the visibility of the view or an ancestor of the view has
+ * changed.
+ *
+ * @param changedView The view whose visibility changed. May be
+ * {@code this} or an ancestor view.
+ * @param visibility The new visibility, one of {@link #VISIBLE},
+ * {@link #INVISIBLE} or {@link #GONE}.
+ */
+ protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) {
+ }
+
+ /**
+ * Dispatch a hint about whether this view is displayed. For instance, when
+ * a View moves out of the screen, it might receives a display hint indicating
+ * the view is not displayed. Applications should not <em>rely</em> on this hint
+ * as there is no guarantee that they will receive one.
+ *
+ * @param hint A hint about whether or not this view is displayed:
+ * {@link #VISIBLE} or {@link #INVISIBLE}.
+ */
+ public void dispatchDisplayHint(@Visibility int hint) {
+ onDisplayHint(hint);
+ }
+
+ /**
+ * Gives this view a hint about whether is displayed or not. For instance, when
+ * a View moves out of the screen, it might receives a display hint indicating
+ * the view is not displayed. Applications should not <em>rely</em> on this hint
+ * as there is no guarantee that they will receive one.
+ *
+ * @param hint A hint about whether or not this view is displayed:
+ * {@link #VISIBLE} or {@link #INVISIBLE}.
+ */
+ protected void onDisplayHint(@Visibility int hint) {
+ }
+
+ /**
+ * Dispatch a window visibility change down the view hierarchy.
+ * ViewGroups should override to route to their children.
+ *
+ * @param visibility The new visibility of the window.
+ *
+ * @see #onWindowVisibilityChanged(int)
+ */
+ public void dispatchWindowVisibilityChanged(@Visibility int visibility) {
+ onWindowVisibilityChanged(visibility);
+ }
+
+ /**
+ * Called when the window containing has change its visibility
+ * (between {@link #GONE}, {@link #INVISIBLE}, and {@link #VISIBLE}). Note
+ * that this tells you whether or not your window is being made visible
+ * to the window manager; this does <em>not</em> tell you whether or not
+ * your window is obscured by other windows on the screen, even if it
+ * is itself visible.
+ *
+ * @param visibility The new visibility of the window.
+ */
+ protected void onWindowVisibilityChanged(@Visibility int visibility) {
+ if (visibility == VISIBLE) {
+ initialAwakenScrollBars();
+ }
+ }
+
+ /**
+ * @return true if this view and all ancestors are visible as of the last
+ * {@link #onVisibilityAggregated(boolean)} call.
+ */
+ boolean isAggregatedVisible() {
+ return (mPrivateFlags3 & PFLAG3_AGGREGATED_VISIBLE) != 0;
+ }
+
+ /**
+ * Internal dispatching method for {@link #onVisibilityAggregated}. Overridden by
+ * ViewGroup. Intended to only be called when {@link #isAttachedToWindow()},
+ * {@link #getWindowVisibility()} is {@link #VISIBLE} and this view's parent {@link #isShown()}.
+ *
+ * @param isVisible true if this view's visibility to the user is uninterrupted by its
+ * ancestors or by window visibility
+ * @return true if this view is visible to the user, not counting clipping or overlapping
+ */
+ boolean dispatchVisibilityAggregated(boolean isVisible) {
+ final boolean thisVisible = getVisibility() == VISIBLE;
+ // If we're not visible but something is telling us we are, ignore it.
+ if (thisVisible || !isVisible) {
+ onVisibilityAggregated(isVisible);
+ }
+ return thisVisible && isVisible;
+ }
+
+ /**
+ * Called when the user-visibility of this View is potentially affected by a change
+ * to this view itself, an ancestor view or the window this view is attached to.
+ *
+ * @param isVisible true if this view and all of its ancestors are {@link #VISIBLE}
+ * and this view's window is also visible
+ */
+ @CallSuper
+ public void onVisibilityAggregated(boolean isVisible) {
+ // Update our internal visibility tracking so we can detect changes
+ boolean oldVisible = isAggregatedVisible();
+ mPrivateFlags3 = isVisible ? (mPrivateFlags3 | PFLAG3_AGGREGATED_VISIBLE)
+ : (mPrivateFlags3 & ~PFLAG3_AGGREGATED_VISIBLE);
+ if (isVisible && mAttachInfo != null) {
+ initialAwakenScrollBars();
+ }
+
+ final Drawable dr = mBackground;
+ if (dr != null && isVisible != dr.isVisible()) {
+ dr.setVisible(isVisible, false);
+ }
+ final Drawable hl = mDefaultFocusHighlight;
+ if (hl != null && isVisible != hl.isVisible()) {
+ hl.setVisible(isVisible, false);
+ }
+ final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
+ if (fg != null && isVisible != fg.isVisible()) {
+ fg.setVisible(isVisible, false);
+ }
+
+ if (isAutofillable()) {
+ AutofillManager afm = getAutofillManager();
+
+ if (afm != null && getAutofillViewId() > LAST_APP_AUTOFILL_ID) {
+ if (mVisibilityChangeForAutofillHandler != null) {
+ mVisibilityChangeForAutofillHandler.removeMessages(0);
+ }
+
+ // If the view is in the background but still part of the hierarchy this is called
+ // with isVisible=false. Hence visibility==false requires further checks
+ if (isVisible) {
+ afm.notifyViewVisibilityChanged(this, true);
+ } else {
+ if (mVisibilityChangeForAutofillHandler == null) {
+ mVisibilityChangeForAutofillHandler =
+ new VisibilityChangeForAutofillHandler(afm, this);
+ }
+ // Let current operation (e.g. removal of the view from the hierarchy)
+ // finish before checking state
+ mVisibilityChangeForAutofillHandler.obtainMessage(0, this).sendToTarget();
+ }
+ }
+ }
+
+ if (isVisible != oldVisible) {
+ if (isAccessibilityPane()) {
+ notifyViewAccessibilityStateChangedIfNeeded(isVisible
+ ? AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED
+ : AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED);
+ }
+
+ notifyAppearedOrDisappearedForContentCaptureIfNeeded(isVisible);
+
+ if (!getSystemGestureExclusionRects().isEmpty()) {
+ postUpdateSystemGestureExclusionRects();
+ }
+ }
+ }
+
+ /**
+ * Returns the current visibility of the window this view is attached to
+ * (either {@link #GONE}, {@link #INVISIBLE}, or {@link #VISIBLE}).
+ *
+ * @return Returns the current visibility of the view's window.
+ */
+ @Visibility
+ public int getWindowVisibility() {
+ return mAttachInfo != null ? mAttachInfo.mWindowVisibility : GONE;
+ }
+
+ /**
+ * Retrieve the overall visible display size in which the window this view is
+ * attached to has been positioned in. This takes into account screen
+ * decorations above the window, for both cases where the window itself
+ * is being position inside of them or the window is being placed under
+ * then and covered insets are used for the window to position its content
+ * inside. In effect, this tells you the available area where content can
+ * be placed and remain visible to users.
+ *
+ * @param outRect Filled in with the visible display frame. If the view
+ * is not attached to a window, this is simply the raw display size.
+ */
+ public void getWindowVisibleDisplayFrame(Rect outRect) {
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewRootImpl.getWindowVisibleDisplayFrame(outRect);
+ return;
+ }
+ // The view is not attached to a display so we don't have a context.
+ // Make a best guess about the display size.
+ Display d = DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
+ d.getRectSize(outRect);
+ }
+
+ /**
+ * Like {@link #getWindowVisibleDisplayFrame}, but returns the "full" display frame this window
+ * is currently in without any insets.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void getWindowDisplayFrame(Rect outRect) {
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewRootImpl.getDisplayFrame(outRect);
+ return;
+ }
+ // The view is not attached to a display so we don't have a context.
+ // Make a best guess about the display size.
+ Display d = DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
+ d.getRectSize(outRect);
+ }
+
+ /**
+ * Dispatch a notification about a resource configuration change down
+ * the view hierarchy.
+ * ViewGroups should override to route to their children.
+ *
+ * @param newConfig The new resource configuration.
+ *
+ * @see #onConfigurationChanged(android.content.res.Configuration)
+ */
+ public void dispatchConfigurationChanged(Configuration newConfig) {
+ onConfigurationChanged(newConfig);
+ }
+
+ /**
+ * Called when the current configuration of the resources being used
+ * by the application have changed. You can use this to decide when
+ * to reload resources that can changed based on orientation and other
+ * configuration characteristics. You only need to use this if you are
+ * not relying on the normal {@link android.app.Activity} mechanism of
+ * recreating the activity instance upon a configuration change.
+ *
+ * @param newConfig The new resource configuration.
+ */
+ protected void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ /**
+ * Private function to aggregate all per-view attributes in to the view
+ * root.
+ */
+ void dispatchCollectViewAttributes(AttachInfo attachInfo, int visibility) {
+ performCollectViewAttributes(attachInfo, visibility);
+ }
+
+ void performCollectViewAttributes(AttachInfo attachInfo, int visibility) {
+ if ((visibility & VISIBILITY_MASK) == VISIBLE) {
+ if ((mViewFlags & KEEP_SCREEN_ON) == KEEP_SCREEN_ON) {
+ attachInfo.mKeepScreenOn = true;
+ }
+ attachInfo.mSystemUiVisibility |= mSystemUiVisibility;
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnSystemUiVisibilityChangeListener != null) {
+ attachInfo.mHasSystemUiListeners = true;
+ }
+ }
+ }
+
+ void needGlobalAttributesUpdate(boolean force) {
+ final AttachInfo ai = mAttachInfo;
+ if (ai != null && !ai.mRecomputeGlobalAttributes) {
+ if (force || ai.mKeepScreenOn || (ai.mSystemUiVisibility != 0)
+ || ai.mHasSystemUiListeners) {
+ ai.mRecomputeGlobalAttributes = true;
+ }
+ }
+ }
+
+ /**
+ * Returns whether the device is currently in touch mode. Touch mode is entered
+ * once the user begins interacting with the device by touch, and affects various
+ * things like whether focus is always visible to the user.
+ *
+ * @return Whether the device is in touch mode.
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isInTouchMode() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mInTouchMode;
+ } else {
+ return ViewRootImpl.isInTouchMode();
+ }
+ }
+
+ /**
+ * Returns the context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ *
+ * @return The view's Context.
+ */
+ @ViewDebug.CapturedViewProperty
+ @UiContext
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Handle a key event before it is processed by any input method
+ * associated with the view hierarchy. This can be used to intercept
+ * key events in special situations before the IME consumes them; a
+ * typical example would be handling the BACK key to update the application's
+ * UI instead of allowing the IME to see it and close itself.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ * @return If you handled the event, return true. If you want to allow the
+ * event to be handled by the next receiver, return false.
+ */
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)
+ * KeyEvent.Callback.onKeyDown()}: perform press of the view
+ * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER}
+ * is released, if the view is enabled and clickable.
+ * <p>
+ * Key presses in software keyboards will generally NOT trigger this
+ * listener, although some may elect to do so in some situations. Do not
+ * rely on this to catch software key presses.
+ *
+ * @param keyCode a key code that represents the button pressed, from
+ * {@link android.view.KeyEvent}
+ * @param event the KeyEvent object that defines the button action
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (KeyEvent.isConfirmKey(keyCode)) {
+ if ((mViewFlags & ENABLED_MASK) == DISABLED) {
+ return true;
+ }
+
+ if (event.getRepeatCount() == 0) {
+ // Long clickable items don't necessarily have to be clickable.
+ final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE
+ || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
+ if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) {
+ // For the purposes of menu anchoring and drawable hotspots,
+ // key events are considered to be at the center of the view.
+ final float x = getWidth() / 2f;
+ final float y = getHeight() / 2f;
+ if (clickable) {
+ setPressed(true, x, y);
+ }
+ checkForLongClick(
+ ViewConfiguration.getLongPressTimeout(),
+ x,
+ y,
+ // This is not a touch gesture -- do not classify it as one.
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
+ * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle
+ * the event).
+ * <p>Key presses in software keyboards will generally NOT trigger this listener,
+ * although some may elect to do so in some situations. Do not rely on this to
+ * catch software key presses.
+ */
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyUp(int, KeyEvent)
+ * KeyEvent.Callback.onKeyUp()}: perform clicking of the view
+ * when {@link KeyEvent#KEYCODE_DPAD_CENTER}, {@link KeyEvent#KEYCODE_ENTER}
+ * or {@link KeyEvent#KEYCODE_SPACE} is released.
+ * <p>Key presses in software keyboards will generally NOT trigger this listener,
+ * although some may elect to do so in some situations. Do not rely on this to
+ * catch software key presses.
+ *
+ * @param keyCode A key code that represents the button pressed, from
+ * {@link android.view.KeyEvent}.
+ * @param event The KeyEvent object that defines the button action.
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (KeyEvent.isConfirmKey(keyCode)) {
+ if ((mViewFlags & ENABLED_MASK) == DISABLED) {
+ return true;
+ }
+ if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
+ setPressed(false);
+
+ if (!mHasPerformedLongPress) {
+ // This is a tap, so remove the longpress check
+ removeLongPressCallback();
+ if (!event.isCanceled()) {
+ return performClickInternal();
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle
+ * the event).
+ * <p>Key presses in software keyboards will generally NOT trigger this listener,
+ * although some may elect to do so in some situations. Do not rely on this to
+ * catch software key presses.
+ *
+ * @param keyCode A key code that represents the button pressed, from
+ * {@link android.view.KeyEvent}.
+ * @param repeatCount The number of times the action was made.
+ * @param event The KeyEvent object that defines the button action.
+ */
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called on the focused view when a key shortcut event is not handled.
+ * Override this method to implement local key shortcuts for the View.
+ * Key shortcuts can also be implemented by setting the
+ * {@link MenuItem#setShortcut(char, char) shortcut} property of menu items.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ * @return If you handled the event, return true. If you want to allow the
+ * event to be handled by the next receiver, return false.
+ */
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Check whether the called view is a text editor, in which case it
+ * would make sense to automatically display a soft input window for
+ * it. Subclasses should override this if they implement
+ * {@link #onCreateInputConnection(EditorInfo)} to return true if
+ * a call on that method would return a non-null InputConnection, and
+ * they are really a first-class editor that the user would normally
+ * start typing on when the go into a window containing your view.
+ *
+ * <p>The default implementation always returns false. This does
+ * <em>not</em> mean that its {@link #onCreateInputConnection(EditorInfo)}
+ * will not be called or the user can not otherwise perform edits on your
+ * view; it is just a hint to the system that this is not the primary
+ * purpose of this view.
+ *
+ * @return Returns true if this view is a text editor, else false.
+ */
+ public boolean onCheckIsTextEditor() {
+ return false;
+ }
+
+ /**
+ * Create a new InputConnection for an InputMethod to interact
+ * with the view. The default implementation returns null, since it doesn't
+ * support input methods. You can override this to implement such support.
+ * This is only needed for views that take focus and text input.
+ *
+ * <p>When implementing this, you probably also want to implement
+ * {@link #onCheckIsTextEditor()} to indicate you will return a
+ * non-null InputConnection.</p>
+ *
+ * <p>Also, take good care to fill in the {@link android.view.inputmethod.EditorInfo}
+ * object correctly and in its entirety, so that the connected IME can rely
+ * on its values. For example, {@link android.view.inputmethod.EditorInfo#initialSelStart}
+ * and {@link android.view.inputmethod.EditorInfo#initialSelEnd} members
+ * must be filled in with the correct cursor position for IMEs to work correctly
+ * with your application.</p>
+ *
+ * @param outAttrs Fill in with attribute information about the connection.
+ */
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return null;
+ }
+
+ /**
+ * Called by the {@link android.view.inputmethod.InputMethodManager} to notify the application
+ * that the system has successfully initialized an {@link InputConnection} and it is ready for
+ * use.
+ *
+ * <p>The default implementation does nothing, since a view doesn't support input methods by
+ * default (see {@link #onCreateInputConnection}).
+ *
+ * @param inputConnection The {@link InputConnection} from {@link #onCreateInputConnection},
+ * after it's been fully initialized by the system.
+ * @param editorInfo The {@link EditorInfo} that was used to create the {@link InputConnection}.
+ * @param handler The dedicated {@link Handler} on which IPC method calls from input methods
+ * will be dispatched. This is the handler returned by {@link InputConnection#getHandler()}. If
+ * that method returns null, this parameter will be null also.
+ *
+ * @hide
+ */
+ public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection,
+ @NonNull EditorInfo editorInfo, @Nullable Handler handler) {}
+
+ /**
+ * Called by the {@link android.view.inputmethod.InputMethodManager} to notify the application
+ * that the {@link InputConnection} has been closed.
+ *
+ * <p>The default implementation does nothing, since a view doesn't support input methods by
+ * default (see {@link #onCreateInputConnection}).
+ *
+ * <p><strong>Note:</strong> This callback is not invoked if the view is already detached when
+ * the {@link InputConnection} is closed or the connection is not valid and managed by
+ * {@link com.android.server.inputmethod.InputMethodManagerService}.
+ * TODO(b/170645312): Before un-hiding this API, handle the detached view scenario.
+ *
+ * @hide
+ */
+ public void onInputConnectionClosedInternal() {}
+
+ /**
+ * Called by the {@link android.view.inputmethod.InputMethodManager}
+ * when a view who is not the current
+ * input connection target is trying to make a call on the manager. The
+ * default implementation returns false; you can override this to return
+ * true for certain views if you are performing InputConnection proxying
+ * to them.
+ * @param view The View that is making the InputMethodManager call.
+ * @return Return true to allow the call, false to reject.
+ */
+ public boolean checkInputConnectionProxy(View view) {
+ return false;
+ }
+
+ /**
+ * Show the context menu for this view. It is not safe to hold on to the
+ * menu after returning from this method.
+ *
+ * You should normally not overload this method. Overload
+ * {@link #onCreateContextMenu(ContextMenu)} or define an
+ * {@link OnCreateContextMenuListener} to add items to the context menu.
+ *
+ * @param menu The context menu to populate
+ */
+ public void createContextMenu(ContextMenu menu) {
+ ContextMenuInfo menuInfo = getContextMenuInfo();
+
+ // Sets the current menu info so all items added to menu will have
+ // my extra info set.
+ ((MenuBuilder)menu).setCurrentMenuInfo(menuInfo);
+
+ onCreateContextMenu(menu);
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnCreateContextMenuListener != null) {
+ li.mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
+ }
+
+ // Clear the extra information so subsequent items that aren't mine don't
+ // have my extra info.
+ ((MenuBuilder)menu).setCurrentMenuInfo(null);
+
+ if (mParent != null) {
+ mParent.createContextMenu(menu);
+ }
+ }
+
+ /**
+ * Views should implement this if they have extra information to associate
+ * with the context menu. The return result is supplied as a parameter to
+ * the {@link OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo)}
+ * callback.
+ *
+ * @return Extra information about the item for which the context menu
+ * should be shown. This information will vary across different
+ * subclasses of View.
+ */
+ protected ContextMenuInfo getContextMenuInfo() {
+ return null;
+ }
+
+ /**
+ * Views should implement this if the view itself is going to add items to
+ * the context menu.
+ *
+ * @param menu the context menu to populate
+ */
+ protected void onCreateContextMenu(ContextMenu menu) {
+ }
+
+ /**
+ * Implement this method to handle trackball motion events. The
+ * <em>relative</em> movement of the trackball since the last event
+ * can be retrieve with {@link MotionEvent#getX MotionEvent.getX()} and
+ * {@link MotionEvent#getY MotionEvent.getY()}. These are normalized so
+ * that a movement of 1 corresponds to the user pressing one DPAD key (so
+ * they will often be fractional values, representing the more fine-grained
+ * movement information available from a trackball).
+ *
+ * @param event The motion event.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Implement this method to handle generic motion events.
+ * <p>
+ * Generic motion events describe joystick movements, mouse hovers, track pad
+ * touches, scroll wheel movements and other input events. The
+ * {@link MotionEvent#getSource() source} of the motion event specifies
+ * the class of input that was received. Implementations of this method
+ * must examine the bits in the source before processing the event.
+ * The following code example shows how this is done.
+ * </p><p>
+ * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER}
+ * are delivered to the view under the pointer. All other generic motion events are
+ * delivered to the focused view.
+ * </p>
+ * <pre> public boolean onGenericMotionEvent(MotionEvent event) {
+ * if (event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) {
+ * if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ * // process the joystick movement...
+ * return true;
+ * }
+ * }
+ * if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ * switch (event.getAction()) {
+ * case MotionEvent.ACTION_HOVER_MOVE:
+ * // process the mouse hover movement...
+ * return true;
+ * case MotionEvent.ACTION_SCROLL:
+ * // process the scroll wheel movement...
+ * return true;
+ * }
+ * }
+ * return super.onGenericMotionEvent(event);
+ * }</pre>
+ *
+ * @param event The generic motion event being processed.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Dispatching hover events to {@link TouchDelegate} to improve accessibility.
+ * <p>
+ * This method is dispatching hover events to the delegate target to support explore by touch.
+ * Similar to {@link ViewGroup#dispatchTouchEvent}, this method send proper hover events to
+ * the delegate target according to the pointer and the touch area of the delegate while touch
+ * exploration enabled.
+ * </p>
+ *
+ * @param event The motion event dispatch to the delegate target.
+ * @return True if the event was handled, false otherwise.
+ *
+ * @see #onHoverEvent
+ */
+ private boolean dispatchTouchExplorationHoverEvent(MotionEvent event) {
+ final AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
+ if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) {
+ return false;
+ }
+
+ final boolean oldHoveringTouchDelegate = mHoveringTouchDelegate;
+ final int action = event.getActionMasked();
+ boolean pointInDelegateRegion = false;
+ boolean handled = false;
+
+ final AccessibilityNodeInfo.TouchDelegateInfo info = mTouchDelegate.getTouchDelegateInfo();
+ for (int i = 0; i < info.getRegionCount(); i++) {
+ Region r = info.getRegionAt(i);
+ if (r.contains((int) event.getX(), (int) event.getY())) {
+ pointInDelegateRegion = true;
+ }
+ }
+
+ // Explore by touch should dispatch events to children under the pointer first if any
+ // before dispatching to TouchDelegate. For non-hoverable views that do not consume
+ // hover events but receive accessibility focus, it should also not delegate to these
+ // views when hovered.
+ if (!oldHoveringTouchDelegate) {
+ if ((action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_MOVE)
+ && !pointInHoveredChild(event)
+ && pointInDelegateRegion) {
+ mHoveringTouchDelegate = true;
+ }
+ } else {
+ if (action == MotionEvent.ACTION_HOVER_EXIT
+ || (action == MotionEvent.ACTION_HOVER_MOVE
+ && (pointInHoveredChild(event) || !pointInDelegateRegion))) {
+ mHoveringTouchDelegate = false;
+ }
+ }
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if (oldHoveringTouchDelegate && mHoveringTouchDelegate) {
+ // Inside bounds, dispatch as is.
+ handled = mTouchDelegate.onTouchExplorationHoverEvent(event);
+ } else if (!oldHoveringTouchDelegate && mHoveringTouchDelegate) {
+ // Moving inbound, synthesize hover enter.
+ MotionEvent eventNoHistory = (event.getHistorySize() == 0)
+ ? event : MotionEvent.obtainNoHistory(event);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+ handled = mTouchDelegate.onTouchExplorationHoverEvent(eventNoHistory);
+ eventNoHistory.setAction(action);
+ handled |= mTouchDelegate.onTouchExplorationHoverEvent(eventNoHistory);
+ } else if (oldHoveringTouchDelegate && !mHoveringTouchDelegate) {
+ // Moving outbound, synthesize hover exit.
+ final boolean hoverExitPending = event.isHoverExitPending();
+ event.setHoverExitPending(true);
+ mTouchDelegate.onTouchExplorationHoverEvent(event);
+ MotionEvent eventNoHistory = (event.getHistorySize() == 0)
+ ? event : MotionEvent.obtainNoHistory(event);
+ eventNoHistory.setHoverExitPending(hoverExitPending);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ mTouchDelegate.onTouchExplorationHoverEvent(eventNoHistory);
+ } // else: outside bounds, do nothing.
+ break;
+ case MotionEvent.ACTION_HOVER_ENTER:
+ if (!oldHoveringTouchDelegate && mHoveringTouchDelegate) {
+ handled = mTouchDelegate.onTouchExplorationHoverEvent(event);
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (oldHoveringTouchDelegate) {
+ mTouchDelegate.onTouchExplorationHoverEvent(event);
+ }
+ break;
+ }
+ return handled;
+ }
+
+ /**
+ * Implement this method to handle hover events.
+ * <p>
+ * This method is called whenever a pointer is hovering into, over, or out of the
+ * bounds of a view and the view is not currently being touched.
+ * Hover events are represented as pointer events with action
+ * {@link MotionEvent#ACTION_HOVER_ENTER}, {@link MotionEvent#ACTION_HOVER_MOVE},
+ * or {@link MotionEvent#ACTION_HOVER_EXIT}.
+ * </p>
+ * <ul>
+ * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_ENTER}
+ * when the pointer enters the bounds of the view.</li>
+ * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_MOVE}
+ * when the pointer has already entered the bounds of the view and has moved.</li>
+ * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_EXIT}
+ * when the pointer has exited the bounds of the view or when the pointer is
+ * about to go down due to a button click, tap, or similar user action that
+ * causes the view to be touched.</li>
+ * </ul>
+ * <p>
+ * The view should implement this method to return true to indicate that it is
+ * handling the hover event, such as by changing its drawable state.
+ * </p><p>
+ * The default implementation calls {@link #setHovered} to update the hovered state
+ * of the view when a hover enter or hover exit event is received, if the view
+ * is enabled and is clickable. The default implementation also sends hover
+ * accessibility events.
+ * </p>
+ *
+ * @param event The motion event that describes the hover.
+ * @return True if the view handled the hover event.
+ *
+ * @see #isHovered
+ * @see #setHovered
+ * @see #onHoverChanged
+ */
+ public boolean onHoverEvent(MotionEvent event) {
+ if (mTouchDelegate != null && dispatchTouchExplorationHoverEvent(event)) {
+ return true;
+ }
+
+ // The root view may receive hover (or touch) events that are outside the bounds of
+ // the window. This code ensures that we only send accessibility events for
+ // hovers that are actually within the bounds of the root view.
+ final int action = event.getActionMasked();
+ if (!mSendingHoverAccessibilityEvents) {
+ if ((action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_MOVE)
+ && !hasHoveredChild()
+ && pointInView(event.getX(), event.getY())) {
+ sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ mSendingHoverAccessibilityEvents = true;
+ }
+ } else {
+ if (action == MotionEvent.ACTION_HOVER_EXIT
+ || (action == MotionEvent.ACTION_HOVER_MOVE
+ && !pointInView(event.getX(), event.getY()))) {
+ mSendingHoverAccessibilityEvents = false;
+ sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ }
+ }
+
+ if ((action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_MOVE)
+ && event.isFromSource(InputDevice.SOURCE_MOUSE)
+ && isOnScrollbar(event.getX(), event.getY())) {
+ awakenScrollBars();
+ }
+
+ // If we consider ourself hoverable, or if we we're already hovered,
+ // handle changing state in response to ENTER and EXIT events.
+ if (isHoverable() || isHovered()) {
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ setHovered(true);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ setHovered(false);
+ break;
+ }
+
+ // Dispatch the event to onGenericMotionEvent before returning true.
+ // This is to provide compatibility with existing applications that
+ // handled HOVER_MOVE events in onGenericMotionEvent and that would
+ // break because of the new default handling for hoverable views
+ // in onHoverEvent.
+ // Note that onGenericMotionEvent will be called by default when
+ // onHoverEvent returns false (refer to dispatchGenericMotionEvent).
+ dispatchGenericMotionEventInternal(event);
+ // The event was already handled by calling setHovered(), so always
+ // return true.
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the view should handle {@link #onHoverEvent}
+ * by calling {@link #setHovered} to change its hovered state.
+ *
+ * @return True if the view is hoverable.
+ */
+ private boolean isHoverable() {
+ final int viewFlags = mViewFlags;
+ if ((viewFlags & ENABLED_MASK) == DISABLED) {
+ return false;
+ }
+
+ return (viewFlags & CLICKABLE) == CLICKABLE
+ || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE
+ || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
+ }
+
+ /**
+ * Returns true if the view is currently hovered.
+ *
+ * @return True if the view is currently hovered.
+ *
+ * @see #setHovered
+ * @see #onHoverChanged
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isHovered() {
+ return (mPrivateFlags & PFLAG_HOVERED) != 0;
+ }
+
+ /**
+ * Sets whether the view is currently hovered.
+ * <p>
+ * Calling this method also changes the drawable state of the view. This
+ * enables the view to react to hover by using different drawable resources
+ * to change its appearance.
+ * </p><p>
+ * The {@link #onHoverChanged} method is called when the hovered state changes.
+ * </p>
+ *
+ * @param hovered True if the view is hovered.
+ *
+ * @see #isHovered
+ * @see #onHoverChanged
+ */
+ public void setHovered(boolean hovered) {
+ if (hovered) {
+ if ((mPrivateFlags & PFLAG_HOVERED) == 0) {
+ mPrivateFlags |= PFLAG_HOVERED;
+ refreshDrawableState();
+ onHoverChanged(true);
+ }
+ } else {
+ if ((mPrivateFlags & PFLAG_HOVERED) != 0) {
+ mPrivateFlags &= ~PFLAG_HOVERED;
+ refreshDrawableState();
+ onHoverChanged(false);
+ }
+ }
+ }
+
+ /**
+ * Implement this method to handle hover state changes.
+ * <p>
+ * This method is called whenever the hover state changes as a result of a
+ * call to {@link #setHovered}.
+ * </p>
+ *
+ * @param hovered The current hover state, as returned by {@link #isHovered}.
+ *
+ * @see #isHovered
+ * @see #setHovered
+ */
+ public void onHoverChanged(boolean hovered) {
+ }
+
+ /**
+ * Handles scroll bar dragging by mouse input.
+ *
+ * @hide
+ * @param event The motion event.
+ *
+ * @return true if the event was handled as a scroll bar dragging, false otherwise.
+ */
+ protected boolean handleScrollBarDragging(MotionEvent event) {
+ if (mScrollCache == null) {
+ return false;
+ }
+ final float x = event.getX();
+ final float y = event.getY();
+ final int action = event.getAction();
+ if ((mScrollCache.mScrollBarDraggingState == ScrollabilityCache.NOT_DRAGGING
+ && action != MotionEvent.ACTION_DOWN)
+ || !event.isFromSource(InputDevice.SOURCE_MOUSE)
+ || !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
+ mScrollCache.mScrollBarDraggingState = ScrollabilityCache.NOT_DRAGGING;
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ if (mScrollCache.mScrollBarDraggingState == ScrollabilityCache.NOT_DRAGGING) {
+ return false;
+ }
+ if (mScrollCache.mScrollBarDraggingState
+ == ScrollabilityCache.DRAGGING_VERTICAL_SCROLL_BAR) {
+ final Rect bounds = mScrollCache.mScrollBarBounds;
+ getVerticalScrollBarBounds(bounds, null);
+ final int range = computeVerticalScrollRange();
+ final int offset = computeVerticalScrollOffset();
+ final int extent = computeVerticalScrollExtent();
+
+ final int thumbLength = ScrollBarUtils.getThumbLength(
+ bounds.height(), bounds.width(), extent, range);
+ final int thumbOffset = ScrollBarUtils.getThumbOffset(
+ bounds.height(), thumbLength, extent, range, offset);
+
+ final float diff = y - mScrollCache.mScrollBarDraggingPos;
+ final float maxThumbOffset = bounds.height() - thumbLength;
+ final float newThumbOffset =
+ Math.min(Math.max(thumbOffset + diff, 0.0f), maxThumbOffset);
+ final int height = getHeight();
+ if (Math.round(newThumbOffset) != thumbOffset && maxThumbOffset > 0
+ && height > 0 && extent > 0) {
+ final int newY = Math.round((range - extent)
+ / ((float)extent / height) * (newThumbOffset / maxThumbOffset));
+ if (newY != getScrollY()) {
+ mScrollCache.mScrollBarDraggingPos = y;
+ setScrollY(newY);
+ }
+ }
+ return true;
+ }
+ if (mScrollCache.mScrollBarDraggingState
+ == ScrollabilityCache.DRAGGING_HORIZONTAL_SCROLL_BAR) {
+ final Rect bounds = mScrollCache.mScrollBarBounds;
+ getHorizontalScrollBarBounds(bounds, null);
+ final int range = computeHorizontalScrollRange();
+ final int offset = computeHorizontalScrollOffset();
+ final int extent = computeHorizontalScrollExtent();
+
+ final int thumbLength = ScrollBarUtils.getThumbLength(
+ bounds.width(), bounds.height(), extent, range);
+ final int thumbOffset = ScrollBarUtils.getThumbOffset(
+ bounds.width(), thumbLength, extent, range, offset);
+
+ final float diff = x - mScrollCache.mScrollBarDraggingPos;
+ final float maxThumbOffset = bounds.width() - thumbLength;
+ final float newThumbOffset =
+ Math.min(Math.max(thumbOffset + diff, 0.0f), maxThumbOffset);
+ final int width = getWidth();
+ if (Math.round(newThumbOffset) != thumbOffset && maxThumbOffset > 0
+ && width > 0 && extent > 0) {
+ final int newX = Math.round((range - extent)
+ / ((float)extent / width) * (newThumbOffset / maxThumbOffset));
+ if (newX != getScrollX()) {
+ mScrollCache.mScrollBarDraggingPos = x;
+ setScrollX(newX);
+ }
+ }
+ return true;
+ }
+ case MotionEvent.ACTION_DOWN:
+ if (mScrollCache.state == ScrollabilityCache.OFF) {
+ return false;
+ }
+ if (isOnVerticalScrollbarThumb(x, y)) {
+ mScrollCache.mScrollBarDraggingState =
+ ScrollabilityCache.DRAGGING_VERTICAL_SCROLL_BAR;
+ mScrollCache.mScrollBarDraggingPos = y;
+ return true;
+ }
+ if (isOnHorizontalScrollbarThumb(x, y)) {
+ mScrollCache.mScrollBarDraggingState =
+ ScrollabilityCache.DRAGGING_HORIZONTAL_SCROLL_BAR;
+ mScrollCache.mScrollBarDraggingPos = x;
+ return true;
+ }
+ }
+ mScrollCache.mScrollBarDraggingState = ScrollabilityCache.NOT_DRAGGING;
+ return false;
+ }
+
+ /**
+ * Implement this method to handle touch screen motion events.
+ * <p>
+ * If this method is used to detect click actions, it is recommended that
+ * the actions be performed by implementing and calling
+ * {@link #performClick()}. This will ensure consistent system behavior,
+ * including:
+ * <ul>
+ * <li>obeying click sound preferences
+ * <li>dispatching OnClickListener calls
+ * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
+ * accessibility features are enabled
+ * </ul>
+ *
+ * @param event The motion event.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ final float x = event.getX();
+ final float y = event.getY();
+ final int viewFlags = mViewFlags;
+ final int action = event.getAction();
+
+ final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
+ || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
+ || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
+
+ if ((viewFlags & ENABLED_MASK) == DISABLED
+ && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
+ if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
+ setPressed(false);
+ }
+ mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
+ // A disabled view that is clickable still consumes the touch
+ // events, it just doesn't respond to them.
+ return clickable;
+ }
+ if (mTouchDelegate != null) {
+ if (mTouchDelegate.onTouchEvent(event)) {
+ return true;
+ }
+ }
+
+ if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
+ if ((viewFlags & TOOLTIP) == TOOLTIP) {
+ handleTooltipUp();
+ }
+ if (!clickable) {
+ removeTapCallback();
+ removeLongPressCallback();
+ mInContextButtonPress = false;
+ mHasPerformedLongPress = false;
+ mIgnoreNextUpEvent = false;
+ break;
+ }
+ boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
+ if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
+ // take focus if we don't have it already and we should in
+ // touch mode.
+ boolean focusTaken = false;
+ if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
+ focusTaken = requestFocus();
+ }
+
+ if (prepressed) {
+ // The button is being released before we actually
+ // showed it as pressed. Make it show the pressed
+ // state now (before scheduling the click) to ensure
+ // the user sees it.
+ setPressed(true, x, y);
+ }
+
+ if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
+ // This is a tap, so remove the longpress check
+ removeLongPressCallback();
+
+ // Only perform take click actions if we were in the pressed state
+ if (!focusTaken) {
+ // Use a Runnable and post this rather than calling
+ // performClick directly. This lets other visual state
+ // of the view update before click actions start.
+ if (mPerformClick == null) {
+ mPerformClick = new PerformClick();
+ }
+ if (!post(mPerformClick)) {
+ performClickInternal();
+ }
+ }
+ }
+
+ if (mUnsetPressedState == null) {
+ mUnsetPressedState = new UnsetPressedState();
+ }
+
+ if (prepressed) {
+ postDelayed(mUnsetPressedState,
+ ViewConfiguration.getPressedStateDuration());
+ } else if (!post(mUnsetPressedState)) {
+ // If the post failed, unpress right now
+ mUnsetPressedState.run();
+ }
+
+ removeTapCallback();
+ }
+ mIgnoreNextUpEvent = false;
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
+ mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
+ }
+ mHasPerformedLongPress = false;
+
+ if (!clickable) {
+ checkForLongClick(
+ ViewConfiguration.getLongPressTimeout(),
+ x,
+ y,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
+ break;
+ }
+
+ if (performButtonActionOnTouchDown(event)) {
+ break;
+ }
+
+ // Walk up the hierarchy to determine if we're inside a scrolling container.
+ boolean isInScrollingContainer = isInScrollingContainer();
+
+ // For views inside a scrolling container, delay the pressed feedback for
+ // a short period in case this is a scroll.
+ if (isInScrollingContainer) {
+ mPrivateFlags |= PFLAG_PREPRESSED;
+ if (mPendingCheckForTap == null) {
+ mPendingCheckForTap = new CheckForTap();
+ }
+ mPendingCheckForTap.x = event.getX();
+ mPendingCheckForTap.y = event.getY();
+ postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+ } else {
+ // Not inside a scrolling container, so show the feedback right away
+ setPressed(true, x, y);
+ checkForLongClick(
+ ViewConfiguration.getLongPressTimeout(),
+ x,
+ y,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (clickable) {
+ setPressed(false);
+ }
+ removeTapCallback();
+ removeLongPressCallback();
+ mInContextButtonPress = false;
+ mHasPerformedLongPress = false;
+ mIgnoreNextUpEvent = false;
+ mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (clickable) {
+ drawableHotspotChanged(x, y);
+ }
+
+ final int motionClassification = event.getClassification();
+ final boolean ambiguousGesture =
+ motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
+ int touchSlop = mTouchSlop;
+ if (ambiguousGesture && hasPendingLongPressCallback()) {
+ if (!pointInView(x, y, touchSlop)) {
+ // The default action here is to cancel long press. But instead, we
+ // just extend the timeout here, in case the classification
+ // stays ambiguous.
+ removeLongPressCallback();
+ long delay = (long) (ViewConfiguration.getLongPressTimeout()
+ * mAmbiguousGestureMultiplier);
+ // Subtract the time already spent
+ delay -= event.getEventTime() - event.getDownTime();
+ checkForLongClick(
+ delay,
+ x,
+ y,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
+ }
+ touchSlop *= mAmbiguousGestureMultiplier;
+ }
+
+ // Be lenient about moving outside of buttons
+ if (!pointInView(x, y, touchSlop)) {
+ // Outside button
+ // Remove any future long press/tap checks
+ removeTapCallback();
+ removeLongPressCallback();
+ if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
+ setPressed(false);
+ }
+ mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
+ }
+
+ final boolean deepPress =
+ motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
+ if (deepPress && hasPendingLongPressCallback()) {
+ // process the long click action immediately
+ removeLongPressCallback();
+ checkForLongClick(
+ 0 /* send immediately */,
+ x,
+ y,
+ TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
+ }
+
+ break;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean isInScrollingContainer() {
+ ViewParent p = getParent();
+ while (p != null && p instanceof ViewGroup) {
+ if (((ViewGroup) p).shouldDelayChildPressedState()) {
+ return true;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * Remove the longpress detection timer.
+ */
+ private void removeLongPressCallback() {
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+ }
+
+ /**
+ * Return true if the long press callback is scheduled to run sometime in the future.
+ * Return false if there is no scheduled long press callback at the moment.
+ */
+ private boolean hasPendingLongPressCallback() {
+ if (mPendingCheckForLongPress == null) {
+ return false;
+ }
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo == null) {
+ return false;
+ }
+ return attachInfo.mHandler.hasCallbacks(mPendingCheckForLongPress);
+ }
+
+ /**
+ * Remove the pending click action
+ */
+ @UnsupportedAppUsage
+ private void removePerformClickCallback() {
+ if (mPerformClick != null) {
+ removeCallbacks(mPerformClick);
+ }
+ }
+
+ /**
+ * Remove the prepress detection timer.
+ */
+ private void removeUnsetPressCallback() {
+ if ((mPrivateFlags & PFLAG_PRESSED) != 0 && mUnsetPressedState != null) {
+ setPressed(false);
+ removeCallbacks(mUnsetPressedState);
+ }
+ }
+
+ /**
+ * Remove the tap detection timer.
+ */
+ private void removeTapCallback() {
+ if (mPendingCheckForTap != null) {
+ mPrivateFlags &= ~PFLAG_PREPRESSED;
+ removeCallbacks(mPendingCheckForTap);
+ }
+ }
+
+ /**
+ * Cancels a pending long press. Your subclass can use this if you
+ * want the context menu to come up if the user presses and holds
+ * at the same place, but you don't want it to come up if they press
+ * and then move around enough to cause scrolling.
+ */
+ public void cancelLongPress() {
+ removeLongPressCallback();
+
+ /*
+ * The prepressed state handled by the tap callback is a display
+ * construct, but the tap callback will post a long press callback
+ * less its own timeout. Remove it here.
+ */
+ removeTapCallback();
+ }
+
+ /**
+ * Sets the TouchDelegate for this View.
+ */
+ public void setTouchDelegate(TouchDelegate delegate) {
+ mTouchDelegate = delegate;
+ }
+
+ /**
+ * Gets the TouchDelegate for this View.
+ */
+ public TouchDelegate getTouchDelegate() {
+ return mTouchDelegate;
+ }
+
+ /**
+ * Request unbuffered dispatch of the given stream of MotionEvents to this View.
+ *
+ * Until this View receives a corresponding {@link MotionEvent#ACTION_UP}, ask that the input
+ * system not batch {@link MotionEvent}s but instead deliver them as soon as they're
+ * available. This method should only be called for touch events.
+ *
+ * <p class="note">This API is not intended for most applications. Buffered dispatch
+ * provides many of benefits, and just requesting unbuffered dispatch on most MotionEvent
+ * streams will not improve your input latency. Side effects include: increased latency,
+ * jittery scrolls and inability to take advantage of system resampling. Talk to your input
+ * professional to see if {@link #requestUnbufferedDispatch(MotionEvent)} is right for
+ * you.</p>
+ *
+ * To receive unbuffered events for arbitrary input device source classes, use
+ * {@link #requestUnbufferedDispatch(int)},
+ *
+ * @see View#requestUnbufferedDispatch(int)
+ */
+ public final void requestUnbufferedDispatch(MotionEvent event) {
+ final int action = event.getAction();
+ if (mAttachInfo == null
+ || action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_MOVE
+ || !event.isTouchEvent()) {
+ return;
+ }
+ mAttachInfo.mUnbufferedDispatchRequested = true;
+ }
+
+ /**
+ * Request unbuffered dispatch of the given event source class to this view.
+ * This is similar to {@link View#requestUnbufferedDispatch(MotionEvent)}, but does not
+ * automatically terminate, and allows the specification of arbitrary input source classes.
+ *
+ * @param source The combined input source class to request unbuffered dispatch for. All
+ * events coming from these source classes will not be buffered. Set to
+ * {@link InputDevice#SOURCE_CLASS_NONE} in order to return to default behaviour.
+ *
+ * @see View#requestUnbufferedDispatch(MotionEvent)
+ */
+ public final void requestUnbufferedDispatch(@InputSourceClass int source) {
+ if (mUnbufferedInputSource == source) {
+ return;
+ }
+ mUnbufferedInputSource = source;
+ if (mParent != null) {
+ mParent.onDescendantUnbufferedRequested();
+ }
+ }
+
+ private boolean hasSize() {
+ return (mBottom > mTop) && (mRight > mLeft);
+ }
+
+ private boolean canTakeFocus() {
+ return ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
+ && ((mViewFlags & FOCUSABLE) == FOCUSABLE)
+ && ((mViewFlags & ENABLED_MASK) == ENABLED)
+ && (sCanFocusZeroSized || !isLayoutValid() || hasSize());
+ }
+
+ /**
+ * Set flags controlling behavior of this view.
+ *
+ * @param flags Constant indicating the value which should be set
+ * @param mask Constant indicating the bit range that should be changed
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ void setFlags(int flags, int mask) {
+ final boolean accessibilityEnabled =
+ AccessibilityManager.getInstance(mContext).isEnabled();
+ final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();
+
+ int old = mViewFlags;
+ mViewFlags = (mViewFlags & ~mask) | (flags & mask);
+
+ int changed = mViewFlags ^ old;
+ if (changed == 0) {
+ return;
+ }
+ int privateFlags = mPrivateFlags;
+ boolean shouldNotifyFocusableAvailable = false;
+
+ // If focusable is auto, update the FOCUSABLE bit.
+ int focusableChangedByAuto = 0;
+ if (((mViewFlags & FOCUSABLE_AUTO) != 0)
+ && (changed & (FOCUSABLE_MASK | CLICKABLE)) != 0) {
+ // Heuristic only takes into account whether view is clickable.
+ final int newFocus;
+ if ((mViewFlags & CLICKABLE) != 0) {
+ newFocus = FOCUSABLE;
+ } else {
+ newFocus = NOT_FOCUSABLE;
+ }
+ mViewFlags = (mViewFlags & ~FOCUSABLE) | newFocus;
+ focusableChangedByAuto = (old & FOCUSABLE) ^ (newFocus & FOCUSABLE);
+ changed = (changed & ~FOCUSABLE) | focusableChangedByAuto;
+ }
+
+ /* Check if the FOCUSABLE bit has changed */
+ if (((changed & FOCUSABLE) != 0) && ((privateFlags & PFLAG_HAS_BOUNDS) != 0)) {
+ if (((old & FOCUSABLE) == FOCUSABLE)
+ && ((privateFlags & PFLAG_FOCUSED) != 0)) {
+ /* Give up focus if we are no longer focusable */
+ clearFocus();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearFocusedInCluster();
+ }
+ } else if (((old & FOCUSABLE) == NOT_FOCUSABLE)
+ && ((privateFlags & PFLAG_FOCUSED) == 0)) {
+ /*
+ * Tell the view system that we are now available to take focus
+ * if no one else already has it.
+ */
+ if (mParent != null) {
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (!sAutoFocusableOffUIThreadWontNotifyParents
+ || focusableChangedByAuto == 0
+ || viewRootImpl == null
+ || viewRootImpl.mThread == Thread.currentThread()) {
+ shouldNotifyFocusableAvailable = canTakeFocus();
+ }
+ }
+ }
+ }
+
+ final int newVisibility = flags & VISIBILITY_MASK;
+ if (newVisibility == VISIBLE) {
+ if ((changed & VISIBILITY_MASK) != 0) {
+ /*
+ * If this view is becoming visible, invalidate it in case it changed while
+ * it was not visible. Marking it drawn ensures that the invalidation will
+ * go through.
+ */
+ mPrivateFlags |= PFLAG_DRAWN;
+ invalidate(true);
+
+ needGlobalAttributesUpdate(true);
+
+ // a view becoming visible is worth notifying the parent about in case nothing has
+ // focus. Even if this specific view isn't focusable, it may contain something that
+ // is, so let the root view try to give this focus if nothing else does.
+ shouldNotifyFocusableAvailable = hasSize();
+ }
+ }
+
+ if ((changed & ENABLED_MASK) != 0) {
+ if ((mViewFlags & ENABLED_MASK) == ENABLED) {
+ // a view becoming enabled should notify the parent as long as the view is also
+ // visible and the parent wasn't already notified by becoming visible during this
+ // setFlags invocation.
+ shouldNotifyFocusableAvailable = canTakeFocus();
+ } else {
+ if (isFocused()) clearFocus();
+ }
+ }
+
+ if (shouldNotifyFocusableAvailable && mParent != null) {
+ mParent.focusableViewAvailable(this);
+ }
+
+ /* Check if the GONE bit has changed */
+ if ((changed & GONE) != 0) {
+ needGlobalAttributesUpdate(false);
+ requestLayout();
+
+ if (((mViewFlags & VISIBILITY_MASK) == GONE)) {
+ if (hasFocus()) {
+ clearFocus();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearFocusedInCluster();
+ }
+ }
+ clearAccessibilityFocus();
+ destroyDrawingCache();
+ if (mParent instanceof View) {
+ // GONE views noop invalidation, so invalidate the parent
+ ((View) mParent).invalidate(true);
+ }
+ // Mark the view drawn to ensure that it gets invalidated properly the next
+ // time it is visible and gets invalidated
+ mPrivateFlags |= PFLAG_DRAWN;
+ }
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewVisibilityChanged = true;
+ }
+ }
+
+ /* Check if the VISIBLE bit has changed */
+ if ((changed & INVISIBLE) != 0) {
+ needGlobalAttributesUpdate(false);
+ /*
+ * If this view is becoming invisible, set the DRAWN flag so that
+ * the next invalidate() will not be skipped.
+ */
+ mPrivateFlags |= PFLAG_DRAWN;
+
+ if (((mViewFlags & VISIBILITY_MASK) == INVISIBLE)) {
+ // root view becoming invisible shouldn't clear focus and accessibility focus
+ if (getRootView() != this) {
+ if (hasFocus()) {
+ clearFocus();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearFocusedInCluster();
+ }
+ }
+ clearAccessibilityFocus();
+ }
+ }
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewVisibilityChanged = true;
+ }
+ }
+
+ if ((changed & VISIBILITY_MASK) != 0) {
+ // If the view is invisible, cleanup its display list to free up resources
+ if (newVisibility != VISIBLE && mAttachInfo != null) {
+ cleanupDraw();
+ }
+
+ if (mParent instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) mParent;
+ parent.onChildVisibilityChanged(this, (changed & VISIBILITY_MASK),
+ newVisibility);
+ parent.invalidate(true);
+ } else if (mParent != null) {
+ mParent.invalidateChild(this, null);
+ }
+
+ if (mAttachInfo != null) {
+ dispatchVisibilityChanged(this, newVisibility);
+
+ // Aggregated visibility changes are dispatched to attached views
+ // in visible windows where the parent is currently shown/drawn
+ // or the parent is not a ViewGroup (and therefore assumed to be a ViewRoot),
+ // discounting clipping or overlapping. This makes it a good place
+ // to change animation states.
+ if (mParent != null && getWindowVisibility() == VISIBLE &&
+ ((!(mParent instanceof ViewGroup)) || ((ViewGroup) mParent).isShown())) {
+ dispatchVisibilityAggregated(newVisibility == VISIBLE);
+ }
+ // If this view is invisible from visible, then sending the A11y event by its
+ // parent which is shown and has the accessibility important.
+ if ((old & VISIBILITY_MASK) == VISIBLE) {
+ notifySubtreeAccessibilityStateChangedByParentIfNeeded();
+ } else {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+ }
+
+ if ((changed & WILL_NOT_CACHE_DRAWING) != 0) {
+ destroyDrawingCache();
+ }
+
+ if ((changed & DRAWING_CACHE_ENABLED) != 0) {
+ destroyDrawingCache();
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ invalidateParentCaches();
+ }
+
+ if ((changed & DRAWING_CACHE_QUALITY_MASK) != 0) {
+ destroyDrawingCache();
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ }
+
+ if ((changed & DRAW_MASK) != 0) {
+ if ((mViewFlags & WILL_NOT_DRAW) != 0) {
+ if (mBackground != null
+ || mDefaultFocusHighlight != null
+ || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
+ mPrivateFlags &= ~PFLAG_SKIP_DRAW;
+ } else {
+ mPrivateFlags |= PFLAG_SKIP_DRAW;
+ }
+ } else {
+ mPrivateFlags &= ~PFLAG_SKIP_DRAW;
+ }
+ requestLayout();
+ invalidate(true);
+ }
+
+ if ((changed & KEEP_SCREEN_ON) != 0) {
+ if (mParent != null && mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) {
+ mParent.recomputeViewAttributes(this);
+ }
+ }
+
+ if (accessibilityEnabled) {
+ // If we're an accessibility pane and the visibility changed, we already have sent
+ // a state change, so we really don't need to report other changes.
+ if (isAccessibilityPane()) {
+ changed &= ~VISIBILITY_MASK;
+ }
+ if ((changed & FOCUSABLE) != 0 || (changed & VISIBILITY_MASK) != 0
+ || (changed & CLICKABLE) != 0 || (changed & LONG_CLICKABLE) != 0
+ || (changed & CONTEXT_CLICKABLE) != 0) {
+ if (oldIncludeForAccessibility != includeForAccessibility()) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ } else {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ } else if ((changed & ENABLED_MASK) != 0) {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+ }
+
+ /**
+ * Change the view's z order in the tree, so it's on top of other sibling
+ * views. This ordering change may affect layout, if the parent container
+ * uses an order-dependent layout scheme (e.g., LinearLayout). Prior
+ * to {@link android.os.Build.VERSION_CODES#KITKAT} this
+ * method should be followed by calls to {@link #requestLayout()} and
+ * {@link View#invalidate()} on the view's parent to force the parent to redraw
+ * with the new child ordering.
+ *
+ * @see ViewGroup#bringChildToFront(View)
+ */
+ public void bringToFront() {
+ if (mParent != null) {
+ mParent.bringChildToFront(this);
+ }
+ }
+
+ /**
+ * This is called in response to an internal scroll in this view (i.e., the
+ * view scrolled its own contents). This is typically as a result of
+ * {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been
+ * called.
+ *
+ * @param l Current horizontal scroll origin.
+ * @param t Current vertical scroll origin.
+ * @param oldl Previous horizontal scroll origin.
+ * @param oldt Previous vertical scroll origin.
+ */
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
+
+ mBackgroundSizeChanged = true;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (mForegroundInfo != null) {
+ mForegroundInfo.mBoundsChanged = true;
+ }
+
+ final AttachInfo ai = mAttachInfo;
+ if (ai != null) {
+ ai.mViewScrollChanged = true;
+ }
+
+ if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) {
+ mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt);
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the scroll
+ * X or Y positions of a view change.
+ * <p>
+ * <b>Note:</b> Some views handle scrolling independently from View and may
+ * have their own separate listeners for scroll-type events. For example,
+ * {@link android.widget.ListView ListView} allows clients to register an
+ * {@link android.widget.ListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener) AbsListView.OnScrollListener}
+ * to listen for changes in list scroll position.
+ *
+ * @see #setOnScrollChangeListener(View.OnScrollChangeListener)
+ */
+ public interface OnScrollChangeListener {
+ /**
+ * Called when the scroll position of a view changes.
+ *
+ * @param v The view whose scroll position has changed.
+ * @param scrollX Current horizontal scroll origin.
+ * @param scrollY Current vertical scroll origin.
+ * @param oldScrollX Previous horizontal scroll origin.
+ * @param oldScrollY Previous vertical scroll origin.
+ */
+ void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the layout bounds of a view
+ * changes due to layout processing.
+ */
+ public interface OnLayoutChangeListener {
+ /**
+ * Called when the layout bounds of a view changes due to layout processing.
+ *
+ * @param v The view whose bounds have changed.
+ * @param left The new value of the view's left property.
+ * @param top The new value of the view's top property.
+ * @param right The new value of the view's right property.
+ * @param bottom The new value of the view's bottom property.
+ * @param oldLeft The previous value of the view's left property.
+ * @param oldTop The previous value of the view's top property.
+ * @param oldRight The previous value of the view's right property.
+ * @param oldBottom The previous value of the view's bottom property.
+ */
+ void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom);
+ }
+
+ /**
+ * This is called during layout when the size of this view has changed. If
+ * you were just added to the view hierarchy, you're called with the old
+ * values of 0.
+ *
+ * @param w Current width of this view.
+ * @param h Current height of this view.
+ * @param oldw Old width of this view.
+ * @param oldh Old height of this view.
+ */
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ }
+
+ /**
+ * Called by draw to draw the child views. This may be overridden
+ * by derived classes to gain control just before its children are drawn
+ * (but after its own view has been drawn).
+ * @param canvas the canvas on which to draw the view
+ */
+ protected void dispatchDraw(Canvas canvas) {
+
+ }
+
+ /**
+ * Gets the parent of this view. Note that the parent is a
+ * ViewParent and not necessarily a View.
+ *
+ * @return Parent of this view.
+ */
+ public final ViewParent getParent() {
+ return mParent;
+ }
+
+ /**
+ * Set the horizontal scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param value the x position to scroll to
+ */
+ public void setScrollX(int value) {
+ scrollTo(value, mScrollY);
+ }
+
+ /**
+ * Set the vertical scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param value the y position to scroll to
+ */
+ public void setScrollY(int value) {
+ scrollTo(mScrollX, value);
+ }
+
+ /**
+ * Return the scrolled left position of this view. This is the left edge of
+ * the displayed part of your view. You do not need to draw any pixels
+ * farther left, since those are outside of the frame of your view on
+ * screen.
+ *
+ * @return The left edge of the displayed part of your view, in pixels.
+ */
+ @InspectableProperty
+ public final int getScrollX() {
+ return mScrollX;
+ }
+
+ /**
+ * Return the scrolled top position of this view. This is the top edge of
+ * the displayed part of your view. You do not need to draw any pixels above
+ * it, since those are outside of the frame of your view on screen.
+ *
+ * @return The top edge of the displayed part of your view, in pixels.
+ */
+ @InspectableProperty
+ public final int getScrollY() {
+ return mScrollY;
+ }
+
+ /**
+ * Return the width of your view.
+ *
+ * @return The width of your view, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public final int getWidth() {
+ return mRight - mLeft;
+ }
+
+ /**
+ * Return the height of your view.
+ *
+ * @return The height of your view, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public final int getHeight() {
+ return mBottom - mTop;
+ }
+
+ /**
+ * Return the visible drawing bounds of your view. Fills in the output
+ * rectangle with the values from getScrollX(), getScrollY(),
+ * getWidth(), and getHeight(). These bounds do not account for any
+ * transformation properties currently set on the view, such as
+ * {@link #setScaleX(float)} or {@link #setRotation(float)}.
+ *
+ * @param outRect The (scrolled) drawing bounds of the view.
+ */
+ public void getDrawingRect(Rect outRect) {
+ outRect.left = mScrollX;
+ outRect.top = mScrollY;
+ outRect.right = mScrollX + (mRight - mLeft);
+ outRect.bottom = mScrollY + (mBottom - mTop);
+ }
+
+ /**
+ * Like {@link #getMeasuredWidthAndState()}, but only returns the
+ * raw width component (that is the result is masked by
+ * {@link #MEASURED_SIZE_MASK}).
+ *
+ * @return The raw measured width of this view.
+ */
+ public final int getMeasuredWidth() {
+ return mMeasuredWidth & MEASURED_SIZE_MASK;
+ }
+
+ /**
+ * Return the full width measurement information for this view as computed
+ * by the most recent call to {@link #measure(int, int)}. This result is a bit mask
+ * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
+ * This should be used during measurement and layout calculations only. Use
+ * {@link #getWidth()} to see how wide a view is after layout.
+ *
+ * @return The measured width of this view as a bit mask.
+ */
+ @ViewDebug.ExportedProperty(category = "measurement", flagMapping = {
+ @ViewDebug.FlagToString(mask = MEASURED_STATE_MASK, equals = MEASURED_STATE_TOO_SMALL,
+ name = "MEASURED_STATE_TOO_SMALL"),
+ })
+ public final int getMeasuredWidthAndState() {
+ return mMeasuredWidth;
+ }
+
+ /**
+ * Like {@link #getMeasuredHeightAndState()}, but only returns the
+ * raw height component (that is the result is masked by
+ * {@link #MEASURED_SIZE_MASK}).
+ *
+ * @return The raw measured height of this view.
+ */
+ public final int getMeasuredHeight() {
+ return mMeasuredHeight & MEASURED_SIZE_MASK;
+ }
+
+ /**
+ * Return the full height measurement information for this view as computed
+ * by the most recent call to {@link #measure(int, int)}. This result is a bit mask
+ * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
+ * This should be used during measurement and layout calculations only. Use
+ * {@link #getHeight()} to see how high a view is after layout.
+ *
+ * @return The measured height of this view as a bit mask.
+ */
+ @ViewDebug.ExportedProperty(category = "measurement", flagMapping = {
+ @ViewDebug.FlagToString(mask = MEASURED_STATE_MASK, equals = MEASURED_STATE_TOO_SMALL,
+ name = "MEASURED_STATE_TOO_SMALL"),
+ })
+ public final int getMeasuredHeightAndState() {
+ return mMeasuredHeight;
+ }
+
+ /**
+ * Return only the state bits of {@link #getMeasuredWidthAndState()}
+ * and {@link #getMeasuredHeightAndState()}, combined into one integer.
+ * The width component is in the regular bits {@link #MEASURED_STATE_MASK}
+ * and the height component is at the shifted bits
+ * {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}.
+ */
+ public final int getMeasuredState() {
+ return (mMeasuredWidth&MEASURED_STATE_MASK)
+ | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
+ & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ /**
+ * The transform matrix of this view, which is calculated based on the current
+ * rotation, scale, and pivot properties.
+ *
+ * @see #getRotation()
+ * @see #getScaleX()
+ * @see #getScaleY()
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @return The current transform matrix for the view
+ */
+ public Matrix getMatrix() {
+ ensureTransformationInfo();
+ final Matrix matrix = mTransformationInfo.mMatrix;
+ mRenderNode.getMatrix(matrix);
+ return matrix;
+ }
+
+ /**
+ * Returns true if the transform matrix is the identity matrix.
+ * Recomputes the matrix if necessary.
+ *
+ * @return True if the transform matrix is the identity matrix, false otherwise.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final boolean hasIdentityMatrix() {
+ return mRenderNode.hasIdentityMatrix();
+ }
+
+ @UnsupportedAppUsage
+ void ensureTransformationInfo() {
+ if (mTransformationInfo == null) {
+ mTransformationInfo = new TransformationInfo();
+ }
+ }
+
+ /**
+ * Utility method to retrieve the inverse of the current mMatrix property.
+ * We cache the matrix to avoid recalculating it when transform properties
+ * have not changed.
+ *
+ * @return The inverse of the current matrix of this view.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final Matrix getInverseMatrix() {
+ ensureTransformationInfo();
+ if (mTransformationInfo.mInverseMatrix == null) {
+ mTransformationInfo.mInverseMatrix = new Matrix();
+ }
+ final Matrix matrix = mTransformationInfo.mInverseMatrix;
+ mRenderNode.getInverseMatrix(matrix);
+ return matrix;
+ }
+
+ /**
+ * Gets the distance along the Z axis from the camera to this view.
+ *
+ * @see #setCameraDistance(float)
+ *
+ * @return The distance along the Z axis.
+ */
+ public float getCameraDistance() {
+ final float dpi = mResources.getDisplayMetrics().densityDpi;
+ return mRenderNode.getCameraDistance() * dpi;
+ }
+
+ /**
+ * <p>Sets the distance along the Z axis (orthogonal to the X/Y plane on which
+ * views are drawn) from the camera to this view. The camera's distance
+ * affects 3D transformations, for instance rotations around the X and Y
+ * axis. If the rotationX or rotationY properties are changed and this view is
+ * large (more than half the size of the screen), it is recommended to always
+ * use a camera distance that's greater than the height (X axis rotation) or
+ * the width (Y axis rotation) of this view.</p>
+ *
+ * <p>The distance of the camera from the view plane can have an affect on the
+ * perspective distortion of the view when it is rotated around the x or y axis.
+ * For example, a large distance will result in a large viewing angle, and there
+ * will not be much perspective distortion of the view as it rotates. A short
+ * distance may cause much more perspective distortion upon rotation, and can
+ * also result in some drawing artifacts if the rotated view ends up partially
+ * behind the camera (which is why the recommendation is to use a distance at
+ * least as far as the size of the view, if the view is to be rotated.)</p>
+ *
+ * <p>The distance is expressed in "depth pixels." The default distance depends
+ * on the screen density. For instance, on a medium density display, the
+ * default distance is 1280. On a high density display, the default distance
+ * is 1920.</p>
+ *
+ * <p>If you want to specify a distance that leads to visually consistent
+ * results across various densities, use the following formula:</p>
+ * <pre>
+ * float scale = context.getResources().getDisplayMetrics().density;
+ * view.setCameraDistance(distance * scale);
+ * </pre>
+ *
+ * <p>The density scale factor of a high density display is 1.5,
+ * and 1920 = 1280 * 1.5.</p>
+ *
+ * @param distance The distance in "depth pixels", if negative the opposite
+ * value is used
+ *
+ * @see #setRotationX(float)
+ * @see #setRotationY(float)
+ */
+ public void setCameraDistance(float distance) {
+ final float dpi = mResources.getDisplayMetrics().densityDpi;
+
+ invalidateViewProperty(true, false);
+ mRenderNode.setCameraDistance(Math.abs(distance) / dpi);
+ invalidateViewProperty(false, false);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ }
+
+ /**
+ * The degrees that the view is rotated around the pivot point.
+ *
+ * @see #setRotation(float)
+ * @see #getPivotX()
+ * @see #getPivotY()
+ *
+ * @return The degrees of rotation.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getRotation() {
+ return mRenderNode.getRotationZ();
+ }
+
+ /**
+ * Sets the degrees that the view is rotated around the pivot point. Increasing values
+ * result in clockwise rotation.
+ *
+ * @param rotation The degrees of rotation.
+ *
+ * @see #getRotation()
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @see #setRotationX(float)
+ * @see #setRotationY(float)
+ *
+ * @attr ref android.R.styleable#View_rotation
+ */
+ @RemotableViewMethod
+ public void setRotation(float rotation) {
+ if (rotation != getRotation()) {
+ // Double-invalidation is necessary to capture view's old and new areas
+ invalidateViewProperty(true, false);
+ mRenderNode.setRotationZ(rotation);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The degrees that the view is rotated around the vertical axis through the pivot point.
+ *
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @see #setRotationY(float)
+ *
+ * @return The degrees of Y rotation.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getRotationY() {
+ return mRenderNode.getRotationY();
+ }
+
+ /**
+ * Sets the degrees that the view is rotated around the vertical axis through the pivot point.
+ * Increasing values result in counter-clockwise rotation from the viewpoint of looking
+ * down the y axis.
+ *
+ * When rotating large views, it is recommended to adjust the camera distance
+ * accordingly. Refer to {@link #setCameraDistance(float)} for more information.
+ *
+ * @param rotationY The degrees of Y rotation.
+ *
+ * @see #getRotationY()
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @see #setRotation(float)
+ * @see #setRotationX(float)
+ * @see #setCameraDistance(float)
+ *
+ * @attr ref android.R.styleable#View_rotationY
+ */
+ @RemotableViewMethod
+ public void setRotationY(float rotationY) {
+ if (rotationY != getRotationY()) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setRotationY(rotationY);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The degrees that the view is rotated around the horizontal axis through the pivot point.
+ *
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @see #setRotationX(float)
+ *
+ * @return The degrees of X rotation.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getRotationX() {
+ return mRenderNode.getRotationX();
+ }
+
+ /**
+ * Sets the degrees that the view is rotated around the horizontal axis through the pivot point.
+ * Increasing values result in clockwise rotation from the viewpoint of looking down the
+ * x axis.
+ *
+ * When rotating large views, it is recommended to adjust the camera distance
+ * accordingly. Refer to {@link #setCameraDistance(float)} for more information.
+ *
+ * @param rotationX The degrees of X rotation.
+ *
+ * @see #getRotationX()
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @see #setRotation(float)
+ * @see #setRotationY(float)
+ * @see #setCameraDistance(float)
+ *
+ * @attr ref android.R.styleable#View_rotationX
+ */
+ @RemotableViewMethod
+ public void setRotationX(float rotationX) {
+ if (rotationX != getRotationX()) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setRotationX(rotationX);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The amount that the view is scaled in x around the pivot point, as a proportion of
+ * the view's unscaled width. A value of 1, the default, means that no scaling is applied.
+ *
+ * <p>By default, this is 1.0f.
+ *
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @return The scaling factor.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getScaleX() {
+ return mRenderNode.getScaleX();
+ }
+
+ /**
+ * Sets the amount that the view is scaled in x around the pivot point, as a proportion of
+ * the view's unscaled width. A value of 1 means that no scaling is applied.
+ *
+ * @param scaleX The scaling factor.
+ * @see #getPivotX()
+ * @see #getPivotY()
+ *
+ * @attr ref android.R.styleable#View_scaleX
+ */
+ @RemotableViewMethod
+ public void setScaleX(float scaleX) {
+ if (scaleX != getScaleX()) {
+ scaleX = sanitizeFloatPropertyValue(scaleX, "scaleX");
+ invalidateViewProperty(true, false);
+ mRenderNode.setScaleX(scaleX);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The amount that the view is scaled in y around the pivot point, as a proportion of
+ * the view's unscaled height. A value of 1, the default, means that no scaling is applied.
+ *
+ * <p>By default, this is 1.0f.
+ *
+ * @see #getPivotX()
+ * @see #getPivotY()
+ * @return The scaling factor.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getScaleY() {
+ return mRenderNode.getScaleY();
+ }
+
+ /**
+ * Sets the amount that the view is scaled in Y around the pivot point, as a proportion of
+ * the view's unscaled width. A value of 1 means that no scaling is applied.
+ *
+ * @param scaleY The scaling factor.
+ * @see #getPivotX()
+ * @see #getPivotY()
+ *
+ * @attr ref android.R.styleable#View_scaleY
+ */
+ @RemotableViewMethod
+ public void setScaleY(float scaleY) {
+ if (scaleY != getScaleY()) {
+ scaleY = sanitizeFloatPropertyValue(scaleY, "scaleY");
+ invalidateViewProperty(true, false);
+ mRenderNode.setScaleY(scaleY);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The x location of the point around which the view is {@link #setRotation(float) rotated}
+ * and {@link #setScaleX(float) scaled}.
+ *
+ * @see #getRotation()
+ * @see #getScaleX()
+ * @see #getScaleY()
+ * @see #getPivotY()
+ * @return The x location of the pivot point.
+ *
+ * @attr ref android.R.styleable#View_transformPivotX
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty(name = "transformPivotX")
+ public float getPivotX() {
+ return mRenderNode.getPivotX();
+ }
+
+ /**
+ * Sets the x location of the point around which the view is
+ * {@link #setRotation(float) rotated} and {@link #setScaleX(float) scaled}.
+ * By default, the pivot point is centered on the object.
+ * Setting this property disables this behavior and causes the view to use only the
+ * explicitly set pivotX and pivotY values.
+ *
+ * @param pivotX The x location of the pivot point.
+ * @see #getRotation()
+ * @see #getScaleX()
+ * @see #getScaleY()
+ * @see #getPivotY()
+ *
+ * @attr ref android.R.styleable#View_transformPivotX
+ */
+ @RemotableViewMethod
+ public void setPivotX(float pivotX) {
+ if (!mRenderNode.isPivotExplicitlySet() || pivotX != getPivotX()) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setPivotX(pivotX);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ }
+ }
+
+ /**
+ * The y location of the point around which the view is {@link #setRotation(float) rotated}
+ * and {@link #setScaleY(float) scaled}.
+ *
+ * @see #getRotation()
+ * @see #getScaleX()
+ * @see #getScaleY()
+ * @see #getPivotY()
+ * @return The y location of the pivot point.
+ *
+ * @attr ref android.R.styleable#View_transformPivotY
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty(name = "transformPivotY")
+ public float getPivotY() {
+ return mRenderNode.getPivotY();
+ }
+
+ /**
+ * Sets the y location of the point around which the view is {@link #setRotation(float) rotated}
+ * and {@link #setScaleY(float) scaled}. By default, the pivot point is centered on the object.
+ * Setting this property disables this behavior and causes the view to use only the
+ * explicitly set pivotX and pivotY values.
+ *
+ * @param pivotY The y location of the pivot point.
+ * @see #getRotation()
+ * @see #getScaleX()
+ * @see #getScaleY()
+ * @see #getPivotY()
+ *
+ * @attr ref android.R.styleable#View_transformPivotY
+ */
+ @RemotableViewMethod
+ public void setPivotY(float pivotY) {
+ if (!mRenderNode.isPivotExplicitlySet() || pivotY != getPivotY()) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setPivotY(pivotY);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ }
+ }
+
+ /**
+ * Returns whether or not a pivot has been set by a call to {@link #setPivotX(float)} or
+ * {@link #setPivotY(float)}. If no pivot has been set then the pivot will be the center
+ * of the view.
+ *
+ * @return True if a pivot has been set, false if the default pivot is being used
+ */
+ public boolean isPivotSet() {
+ return mRenderNode.isPivotExplicitlySet();
+ }
+
+ /**
+ * Clears any pivot previously set by a call to {@link #setPivotX(float)} or
+ * {@link #setPivotY(float)}. After calling this {@link #isPivotSet()} will be false
+ * and the pivot used for rotation will return to default of being centered on the view.
+ */
+ public void resetPivot() {
+ if (mRenderNode.resetPivot()) {
+ invalidateViewProperty(false, false);
+ }
+ }
+
+ /**
+ * The opacity of the view. This is a value from 0 to 1, where 0 means the view is
+ * completely transparent and 1 means the view is completely opaque.
+ *
+ * <p>By default this is 1.0f.
+ * @return The opacity of the view.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getAlpha() {
+ return mTransformationInfo != null ? mTransformationInfo.mAlpha : 1;
+ }
+
+ /**
+ * Sets the behavior for overlapping rendering for this view (see {@link
+ * #hasOverlappingRendering()} for more details on this behavior). Calling this method
+ * is an alternative to overriding {@link #hasOverlappingRendering()} in a subclass,
+ * providing the value which is then used internally. That is, when {@link
+ * #forceHasOverlappingRendering(boolean)} is called, the value of {@link
+ * #hasOverlappingRendering()} is ignored and the value passed into this method is used
+ * instead.
+ *
+ * @param hasOverlappingRendering The value for overlapping rendering to be used internally
+ * instead of that returned by {@link #hasOverlappingRendering()}.
+ *
+ * @attr ref android.R.styleable#View_forceHasOverlappingRendering
+ */
+ public void forceHasOverlappingRendering(boolean hasOverlappingRendering) {
+ mPrivateFlags3 |= PFLAG3_HAS_OVERLAPPING_RENDERING_FORCED;
+ if (hasOverlappingRendering) {
+ mPrivateFlags3 |= PFLAG3_OVERLAPPING_RENDERING_FORCED_VALUE;
+ } else {
+ mPrivateFlags3 &= ~PFLAG3_OVERLAPPING_RENDERING_FORCED_VALUE;
+ }
+ }
+
+ /**
+ * Returns the value for overlapping rendering that is used internally. This is either
+ * the value passed into {@link #forceHasOverlappingRendering(boolean)}, if called, or
+ * the return value of {@link #hasOverlappingRendering()}, otherwise.
+ *
+ * @return The value for overlapping rendering being used internally.
+ */
+ public final boolean getHasOverlappingRendering() {
+ return (mPrivateFlags3 & PFLAG3_HAS_OVERLAPPING_RENDERING_FORCED) != 0 ?
+ (mPrivateFlags3 & PFLAG3_OVERLAPPING_RENDERING_FORCED_VALUE) != 0 :
+ hasOverlappingRendering();
+ }
+
+ /**
+ * Returns whether this View has content which overlaps.
+ *
+ * <p>This function, intended to be overridden by specific View types, is an optimization when
+ * alpha is set on a view. If rendering overlaps in a view with alpha < 1, that view is drawn to
+ * an offscreen buffer and then composited into place, which can be expensive. If the view has
+ * no overlapping rendering, the view can draw each primitive with the appropriate alpha value
+ * directly. An example of overlapping rendering is a TextView with a background image, such as
+ * a Button. An example of non-overlapping rendering is a TextView with no background, or an
+ * ImageView with only the foreground image. The default implementation returns true; subclasses
+ * should override if they have cases which can be optimized.</p>
+ *
+ * <p><strong>Note:</strong> The return value of this method is ignored if {@link
+ * #forceHasOverlappingRendering(boolean)} has been called on this view.</p>
+ *
+ * @return true if the content in this view might overlap, false otherwise.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public boolean hasOverlappingRendering() {
+ return true;
+ }
+
+ /**
+ * Sets the opacity of the view to a value from 0 to 1, where 0 means the view is
+ * completely transparent and 1 means the view is completely opaque.
+ *
+ * <p class="note"><strong>Note:</strong> setting alpha to a translucent value (0 < alpha < 1)
+ * can have significant performance implications, especially for large views. It is best to use
+ * the alpha property sparingly and transiently, as in the case of fading animations.</p>
+ *
+ * <p>For a view with a frequently changing alpha, such as during a fading animation, it is
+ * strongly recommended for performance reasons to either override
+ * {@link #hasOverlappingRendering()} to return <code>false</code> if appropriate, or setting a
+ * {@link #setLayerType(int, android.graphics.Paint) layer type} on the view for the duration
+ * of the animation. On versions {@link android.os.Build.VERSION_CODES#M} and below,
+ * the default path for rendering an unlayered View with alpha could add multiple milliseconds
+ * of rendering cost, even for simple or small views. Starting with
+ * {@link android.os.Build.VERSION_CODES#M}, {@link #LAYER_TYPE_HARDWARE} is automatically
+ * applied to the view at the rendering level.</p>
+ *
+ * <p>If this view overrides {@link #onSetAlpha(int)} to return true, then this view is
+ * responsible for applying the opacity itself.</p>
+ *
+ * <p>On versions {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and below, note that if
+ * the view is backed by a {@link #setLayerType(int, android.graphics.Paint) layer} and is
+ * associated with a {@link #setLayerPaint(android.graphics.Paint) layer paint}, setting an
+ * alpha value less than 1.0 will supersede the alpha of the layer paint.</p>
+ *
+ * <p>Starting with {@link android.os.Build.VERSION_CODES#M}, setting a translucent alpha
+ * value will clip a View to its bounds, unless the View returns <code>false</code> from
+ * {@link #hasOverlappingRendering}.</p>
+ *
+ * @param alpha The opacity of the view.
+ *
+ * @see #hasOverlappingRendering()
+ * @see #setLayerType(int, android.graphics.Paint)
+ *
+ * @attr ref android.R.styleable#View_alpha
+ */
+ @RemotableViewMethod
+ public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
+ ensureTransformationInfo();
+ if (mTransformationInfo.mAlpha != alpha) {
+ setAlphaInternal(alpha);
+ if (onSetAlpha((int) (alpha * 255))) {
+ mPrivateFlags |= PFLAG_ALPHA_SET;
+ // subclass is handling alpha - don't optimize rendering cache invalidation
+ invalidateParentCaches();
+ invalidate(true);
+ } else {
+ mPrivateFlags &= ~PFLAG_ALPHA_SET;
+ invalidateViewProperty(true, false);
+ mRenderNode.setAlpha(getFinalAlpha());
+ }
+ }
+ }
+
+ /**
+ * Faster version of setAlpha() which performs the same steps except there are
+ * no calls to invalidate(). The caller of this function should perform proper invalidation
+ * on the parent and this object. The return value indicates whether the subclass handles
+ * alpha (the return value for onSetAlpha()).
+ *
+ * @param alpha The new value for the alpha property
+ * @return true if the View subclass handles alpha (the return value for onSetAlpha()) and
+ * the new value for the alpha property is different from the old value
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768435)
+ boolean setAlphaNoInvalidation(float alpha) {
+ ensureTransformationInfo();
+ if (mTransformationInfo.mAlpha != alpha) {
+ setAlphaInternal(alpha);
+ boolean subclassHandlesAlpha = onSetAlpha((int) (alpha * 255));
+ if (subclassHandlesAlpha) {
+ mPrivateFlags |= PFLAG_ALPHA_SET;
+ return true;
+ } else {
+ mPrivateFlags &= ~PFLAG_ALPHA_SET;
+ mRenderNode.setAlpha(getFinalAlpha());
+ }
+ }
+ return false;
+ }
+
+ void setAlphaInternal(float alpha) {
+ float oldAlpha = mTransformationInfo.mAlpha;
+ mTransformationInfo.mAlpha = alpha;
+ // Report visibility changes, which can affect children, to accessibility
+ if ((alpha == 0) ^ (oldAlpha == 0)) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * This property is intended only for use by the Fade transition, which animates it
+ * to produce a visual translucency that does not side-effect (or get affected by)
+ * the real alpha property. This value is composited with the other alpha value
+ * (and the AlphaAnimation value, when that is present) to produce a final visual
+ * translucency result, which is what is passed into the DisplayList.
+ */
+ public void setTransitionAlpha(float alpha) {
+ ensureTransformationInfo();
+ if (mTransformationInfo.mTransitionAlpha != alpha) {
+ mTransformationInfo.mTransitionAlpha = alpha;
+ mPrivateFlags &= ~PFLAG_ALPHA_SET;
+ invalidateViewProperty(true, false);
+ mRenderNode.setAlpha(getFinalAlpha());
+ }
+ }
+
+ /**
+ * Calculates the visual alpha of this view, which is a combination of the actual
+ * alpha value and the transitionAlpha value (if set).
+ */
+ private float getFinalAlpha() {
+ if (mTransformationInfo != null) {
+ return mTransformationInfo.mAlpha * mTransformationInfo.mTransitionAlpha;
+ }
+ return 1;
+ }
+
+ /**
+ * This property is intended only for use by the Fade transition, which animates
+ * it to produce a visual translucency that does not side-effect (or get affected
+ * by) the real alpha property. This value is composited with the other alpha
+ * value (and the AlphaAnimation value, when that is present) to produce a final
+ * visual translucency result, which is what is passed into the DisplayList.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public float getTransitionAlpha() {
+ return mTransformationInfo != null ? mTransformationInfo.mTransitionAlpha : 1;
+ }
+
+ /**
+ * Sets whether or not to allow force dark to apply to this view.
+ *
+ * Setting this to false will disable the auto-dark feature on everything this view
+ * draws, including any descendants.
+ *
+ * Setting this to true will allow this view to be automatically made dark, however
+ * a value of 'true' will not override any 'false' value in its parent chain nor will
+ * it prevent any 'false' in any of its children.
+ *
+ * The default behavior of force dark is also influenced by the Theme's
+ * {@link android.R.styleable#Theme_isLightTheme isLightTheme} attribute.
+ * If a theme is isLightTheme="false", then force dark is globally disabled for that theme.
+ *
+ * @param allow Whether or not to allow force dark.
+ */
+ public void setForceDarkAllowed(boolean allow) {
+ if (mRenderNode.setForceDarkAllowed(allow)) {
+ // Currently toggling force-dark requires a new display list push to apply
+ // TODO: Make it not clobber the display list so this is just a damageSelf() instead
+ invalidate();
+ }
+ }
+
+ /**
+ * See {@link #setForceDarkAllowed(boolean)}
+ *
+ * @return true if force dark is allowed (default), false if it is disabled
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public boolean isForceDarkAllowed() {
+ return mRenderNode.isForceDarkAllowed();
+ }
+
+ /**
+ * Top position of this view relative to its parent.
+ *
+ * @return The top of this view, in pixels.
+ */
+ @ViewDebug.CapturedViewProperty
+ public final int getTop() {
+ return mTop;
+ }
+
+ /**
+ * Sets the top position of this view relative to its parent. This method is meant to be called
+ * by the layout system and should not generally be called otherwise, because the property
+ * may be changed at any time by the layout.
+ *
+ * @param top The top of this view, in pixels.
+ */
+ public final void setTop(int top) {
+ if (top != mTop) {
+ final boolean matrixIsIdentity = hasIdentityMatrix();
+ if (matrixIsIdentity) {
+ if (mAttachInfo != null) {
+ int minTop;
+ int yLoc;
+ if (top < mTop) {
+ minTop = top;
+ yLoc = top - mTop;
+ } else {
+ minTop = mTop;
+ yLoc = 0;
+ }
+ invalidate(0, yLoc, mRight - mLeft, mBottom - minTop);
+ }
+ } else {
+ // Double-invalidation is necessary to capture view's old and new areas
+ invalidate(true);
+ }
+
+ int width = mRight - mLeft;
+ int oldHeight = mBottom - mTop;
+
+ mTop = top;
+ mRenderNode.setTop(mTop);
+
+ sizeChange(width, mBottom - mTop, width, oldHeight);
+
+ if (!matrixIsIdentity) {
+ mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
+ invalidate(true);
+ }
+ mBackgroundSizeChanged = true;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (mForegroundInfo != null) {
+ mForegroundInfo.mBoundsChanged = true;
+ }
+ invalidateParentIfNeeded();
+ }
+ }
+
+ /**
+ * Bottom position of this view relative to its parent.
+ *
+ * @return The bottom of this view, in pixels.
+ */
+ @ViewDebug.CapturedViewProperty
+ public final int getBottom() {
+ return mBottom;
+ }
+
+ /**
+ * True if this view has changed since the last time being drawn.
+ *
+ * @return The dirty state of this view.
+ */
+ public boolean isDirty() {
+ return (mPrivateFlags & PFLAG_DIRTY_MASK) != 0;
+ }
+
+ /**
+ * Sets the bottom position of this view relative to its parent. This method is meant to be
+ * called by the layout system and should not generally be called otherwise, because the
+ * property may be changed at any time by the layout.
+ *
+ * @param bottom The bottom of this view, in pixels.
+ */
+ public final void setBottom(int bottom) {
+ if (bottom != mBottom) {
+ final boolean matrixIsIdentity = hasIdentityMatrix();
+ if (matrixIsIdentity) {
+ if (mAttachInfo != null) {
+ int maxBottom;
+ if (bottom < mBottom) {
+ maxBottom = mBottom;
+ } else {
+ maxBottom = bottom;
+ }
+ invalidate(0, 0, mRight - mLeft, maxBottom - mTop);
+ }
+ } else {
+ // Double-invalidation is necessary to capture view's old and new areas
+ invalidate(true);
+ }
+
+ int width = mRight - mLeft;
+ int oldHeight = mBottom - mTop;
+
+ mBottom = bottom;
+ mRenderNode.setBottom(mBottom);
+
+ sizeChange(width, mBottom - mTop, width, oldHeight);
+
+ if (!matrixIsIdentity) {
+ mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
+ invalidate(true);
+ }
+ mBackgroundSizeChanged = true;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (mForegroundInfo != null) {
+ mForegroundInfo.mBoundsChanged = true;
+ }
+ invalidateParentIfNeeded();
+ }
+ }
+
+ /**
+ * Left position of this view relative to its parent.
+ *
+ * @return The left edge of this view, in pixels.
+ */
+ @ViewDebug.CapturedViewProperty
+ public final int getLeft() {
+ return mLeft;
+ }
+
+ /**
+ * Sets the left position of this view relative to its parent. This method is meant to be called
+ * by the layout system and should not generally be called otherwise, because the property
+ * may be changed at any time by the layout.
+ *
+ * @param left The left of this view, in pixels.
+ */
+ public final void setLeft(int left) {
+ if (left != mLeft) {
+ final boolean matrixIsIdentity = hasIdentityMatrix();
+ if (matrixIsIdentity) {
+ if (mAttachInfo != null) {
+ int minLeft;
+ int xLoc;
+ if (left < mLeft) {
+ minLeft = left;
+ xLoc = left - mLeft;
+ } else {
+ minLeft = mLeft;
+ xLoc = 0;
+ }
+ invalidate(xLoc, 0, mRight - minLeft, mBottom - mTop);
+ }
+ } else {
+ // Double-invalidation is necessary to capture view's old and new areas
+ invalidate(true);
+ }
+
+ int oldWidth = mRight - mLeft;
+ int height = mBottom - mTop;
+
+ mLeft = left;
+ mRenderNode.setLeft(left);
+
+ sizeChange(mRight - mLeft, height, oldWidth, height);
+
+ if (!matrixIsIdentity) {
+ mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
+ invalidate(true);
+ }
+ mBackgroundSizeChanged = true;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (mForegroundInfo != null) {
+ mForegroundInfo.mBoundsChanged = true;
+ }
+ invalidateParentIfNeeded();
+ }
+ }
+
+ /**
+ * Right position of this view relative to its parent.
+ *
+ * @return The right edge of this view, in pixels.
+ */
+ @ViewDebug.CapturedViewProperty
+ public final int getRight() {
+ return mRight;
+ }
+
+ /**
+ * Sets the right position of this view relative to its parent. This method is meant to be called
+ * by the layout system and should not generally be called otherwise, because the property
+ * may be changed at any time by the layout.
+ *
+ * @param right The right of this view, in pixels.
+ */
+ public final void setRight(int right) {
+ if (right != mRight) {
+ final boolean matrixIsIdentity = hasIdentityMatrix();
+ if (matrixIsIdentity) {
+ if (mAttachInfo != null) {
+ int maxRight;
+ if (right < mRight) {
+ maxRight = mRight;
+ } else {
+ maxRight = right;
+ }
+ invalidate(0, 0, maxRight - mLeft, mBottom - mTop);
+ }
+ } else {
+ // Double-invalidation is necessary to capture view's old and new areas
+ invalidate(true);
+ }
+
+ int oldWidth = mRight - mLeft;
+ int height = mBottom - mTop;
+
+ mRight = right;
+ mRenderNode.setRight(mRight);
+
+ sizeChange(mRight - mLeft, height, oldWidth, height);
+
+ if (!matrixIsIdentity) {
+ mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
+ invalidate(true);
+ }
+ mBackgroundSizeChanged = true;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (mForegroundInfo != null) {
+ mForegroundInfo.mBoundsChanged = true;
+ }
+ invalidateParentIfNeeded();
+ }
+ }
+
+ private static float sanitizeFloatPropertyValue(float value, String propertyName) {
+ return sanitizeFloatPropertyValue(value, propertyName, -Float.MAX_VALUE, Float.MAX_VALUE);
+ }
+
+ private static float sanitizeFloatPropertyValue(float value, String propertyName,
+ float min, float max) {
+ // The expected "nothing bad happened" path
+ if (value >= min && value <= max) return value;
+
+ if (value < min || value == Float.NEGATIVE_INFINITY) {
+ if (sThrowOnInvalidFloatProperties) {
+ throw new IllegalArgumentException("Cannot set '" + propertyName + "' to "
+ + value + ", the value must be >= " + min);
+ }
+ return min;
+ }
+
+ if (value > max || value == Float.POSITIVE_INFINITY) {
+ if (sThrowOnInvalidFloatProperties) {
+ throw new IllegalArgumentException("Cannot set '" + propertyName + "' to "
+ + value + ", the value must be <= " + max);
+ }
+ return max;
+ }
+
+ if (Float.isNaN(value)) {
+ if (sThrowOnInvalidFloatProperties) {
+ throw new IllegalArgumentException(
+ "Cannot set '" + propertyName + "' to Float.NaN");
+ }
+ return 0; // Unclear which direction this NaN went so... 0?
+ }
+
+ // Shouldn't be possible to reach this.
+ throw new IllegalStateException("How do you get here?? " + value);
+ }
+
+ /**
+ * The visual x position of this view, in pixels. This is equivalent to the
+ * {@link #setTranslationX(float) translationX} property plus the current
+ * {@link #getLeft() left} property.
+ *
+ * @return The visual x position of this view, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public float getX() {
+ return mLeft + getTranslationX();
+ }
+
+ /**
+ * Sets the visual x position of this view, in pixels. This is equivalent to setting the
+ * {@link #setTranslationX(float) translationX} property to be the difference between
+ * the x value passed in and the current {@link #getLeft() left} property.
+ *
+ * @param x The visual x position of this view, in pixels.
+ */
+ public void setX(float x) {
+ setTranslationX(x - mLeft);
+ }
+
+ /**
+ * The visual y position of this view, in pixels. This is equivalent to the
+ * {@link #setTranslationY(float) translationY} property plus the current
+ * {@link #getTop() top} property.
+ *
+ * @return The visual y position of this view, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public float getY() {
+ return mTop + getTranslationY();
+ }
+
+ /**
+ * Sets the visual y position of this view, in pixels. This is equivalent to setting the
+ * {@link #setTranslationY(float) translationY} property to be the difference between
+ * the y value passed in and the current {@link #getTop() top} property.
+ *
+ * @param y The visual y position of this view, in pixels.
+ */
+ public void setY(float y) {
+ setTranslationY(y - mTop);
+ }
+
+ /**
+ * The visual z position of this view, in pixels. This is equivalent to the
+ * {@link #setTranslationZ(float) translationZ} property plus the current
+ * {@link #getElevation() elevation} property.
+ *
+ * @return The visual z position of this view, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public float getZ() {
+ return getElevation() + getTranslationZ();
+ }
+
+ /**
+ * Sets the visual z position of this view, in pixels. This is equivalent to setting the
+ * {@link #setTranslationZ(float) translationZ} property to be the difference between
+ * the z value passed in and the current {@link #getElevation() elevation} property.
+ *
+ * @param z The visual z position of this view, in pixels.
+ */
+ public void setZ(float z) {
+ setTranslationZ(z - getElevation());
+ }
+
+ /**
+ * The base elevation of this view relative to its parent, in pixels.
+ *
+ * @return The base depth position of the view, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getElevation() {
+ return mRenderNode.getElevation();
+ }
+
+ /**
+ * Sets the base elevation of this view, in pixels.
+ *
+ * @attr ref android.R.styleable#View_elevation
+ */
+ @RemotableViewMethod
+ public void setElevation(float elevation) {
+ if (elevation != getElevation()) {
+ elevation = sanitizeFloatPropertyValue(elevation, "elevation");
+ invalidateViewProperty(true, false);
+ mRenderNode.setElevation(elevation);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ }
+ }
+
+ /**
+ * The horizontal location of this view relative to its {@link #getLeft() left} position.
+ * This position is post-layout, in addition to wherever the object's
+ * layout placed it.
+ *
+ * @return The horizontal position of this view relative to its left position, in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getTranslationX() {
+ return mRenderNode.getTranslationX();
+ }
+
+ /**
+ * Sets the horizontal location of this view relative to its {@link #getLeft() left} position.
+ * This effectively positions the object post-layout, in addition to wherever the object's
+ * layout placed it.
+ *
+ * @param translationX The horizontal position of this view relative to its left position,
+ * in pixels.
+ *
+ * @attr ref android.R.styleable#View_translationX
+ */
+ @RemotableViewMethod
+ public void setTranslationX(float translationX) {
+ if (translationX != getTranslationX()) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setTranslationX(translationX);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The vertical location of this view relative to its {@link #getTop() top} position.
+ * This position is post-layout, in addition to wherever the object's
+ * layout placed it.
+ *
+ * @return The vertical position of this view relative to its top position,
+ * in pixels.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getTranslationY() {
+ return mRenderNode.getTranslationY();
+ }
+
+ /**
+ * Sets the vertical location of this view relative to its {@link #getTop() top} position.
+ * This effectively positions the object post-layout, in addition to wherever the object's
+ * layout placed it.
+ *
+ * @param translationY The vertical position of this view relative to its top position,
+ * in pixels.
+ *
+ * @attr ref android.R.styleable#View_translationY
+ */
+ @RemotableViewMethod
+ public void setTranslationY(float translationY) {
+ if (translationY != getTranslationY()) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setTranslationY(translationY);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * The depth location of this view relative to its {@link #getElevation() elevation}.
+ *
+ * @return The depth of this view relative to its elevation.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public float getTranslationZ() {
+ return mRenderNode.getTranslationZ();
+ }
+
+ /**
+ * Sets the depth location of this view relative to its {@link #getElevation() elevation}.
+ *
+ * @attr ref android.R.styleable#View_translationZ
+ */
+ @RemotableViewMethod
+ public void setTranslationZ(float translationZ) {
+ if (translationZ != getTranslationZ()) {
+ translationZ = sanitizeFloatPropertyValue(translationZ, "translationZ");
+ invalidateViewProperty(true, false);
+ mRenderNode.setTranslationZ(translationZ);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ }
+ }
+
+ /**
+ * Changes the transformation matrix on the view. This is used in animation frameworks,
+ * such as {@link android.transition.Transition}. When the animation finishes, the matrix
+ * should be cleared by calling this method with <code>null</code> as the matrix parameter.
+ * Application developers should use transformation methods like {@link #setRotation(float)},
+ * {@link #setScaleX(float)}, {@link #setScaleX(float)}, {@link #setTranslationX(float)}}
+ * and {@link #setTranslationY(float)} (float)}} instead.
+ *
+ * @param matrix The matrix, null indicates that the matrix should be cleared.
+ * @see #getAnimationMatrix()
+ */
+ public void setAnimationMatrix(@Nullable Matrix matrix) {
+ invalidateViewProperty(true, false);
+ mRenderNode.setAnimationMatrix(matrix);
+ invalidateViewProperty(false, true);
+
+ invalidateParentIfNeededAndWasQuickRejected();
+ }
+
+ /**
+ * Return the current transformation matrix of the view. This is used in animation frameworks,
+ * such as {@link android.transition.Transition}. Returns <code>null</code> when there is no
+ * transformation provided by {@link #setAnimationMatrix(Matrix)}.
+ * Application developers should use transformation methods like {@link #setRotation(float)},
+ * {@link #setScaleX(float)}, {@link #setScaleX(float)}, {@link #setTranslationX(float)}}
+ * and {@link #setTranslationY(float)} (float)}} instead.
+ *
+ * @return the Matrix, null indicates there is no transformation
+ * @see #setAnimationMatrix(Matrix)
+ */
+ @Nullable
+ public Matrix getAnimationMatrix() {
+ return mRenderNode.getAnimationMatrix();
+ }
+
+ /**
+ * Returns the current StateListAnimator if exists.
+ *
+ * @return StateListAnimator or null if it does not exists
+ * @see #setStateListAnimator(android.animation.StateListAnimator)
+ */
+ @InspectableProperty
+ public StateListAnimator getStateListAnimator() {
+ return mStateListAnimator;
+ }
+
+ /**
+ * Attaches the provided StateListAnimator to this View.
+ * <p>
+ * Any previously attached StateListAnimator will be detached.
+ *
+ * @param stateListAnimator The StateListAnimator to update the view
+ * @see android.animation.StateListAnimator
+ */
+ public void setStateListAnimator(StateListAnimator stateListAnimator) {
+ if (mStateListAnimator == stateListAnimator) {
+ return;
+ }
+ if (mStateListAnimator != null) {
+ mStateListAnimator.setTarget(null);
+ }
+ mStateListAnimator = stateListAnimator;
+ if (stateListAnimator != null) {
+ stateListAnimator.setTarget(this);
+ if (isAttachedToWindow()) {
+ stateListAnimator.setState(getDrawableState());
+ }
+ }
+ }
+
+ /**
+ * Returns whether the Outline should be used to clip the contents of the View.
+ * <p>
+ * Note that this flag will only be respected if the View's Outline returns true from
+ * {@link Outline#canClip()}.
+ *
+ * @see #setOutlineProvider(ViewOutlineProvider)
+ * @see #setClipToOutline(boolean)
+ */
+ public final boolean getClipToOutline() {
+ return mRenderNode.getClipToOutline();
+ }
+
+ /**
+ * Sets whether the View's Outline should be used to clip the contents of the View.
+ * <p>
+ * Only a single non-rectangular clip can be applied on a View at any time.
+ * Circular clips from a {@link ViewAnimationUtils#createCircularReveal(View, int, int, float, float)
+ * circular reveal} animation take priority over Outline clipping, and
+ * child Outline clipping takes priority over Outline clipping done by a
+ * parent.
+ * <p>
+ * Note that this flag will only be respected if the View's Outline returns true from
+ * {@link Outline#canClip()}.
+ *
+ * @see #setOutlineProvider(ViewOutlineProvider)
+ * @see #getClipToOutline()
+ *
+ * @attr ref android.R.styleable#View_clipToOutline
+ */
+ @RemotableViewMethod
+ public void setClipToOutline(boolean clipToOutline) {
+ damageInParent();
+ if (getClipToOutline() != clipToOutline) {
+ mRenderNode.setClipToOutline(clipToOutline);
+ }
+ }
+
+ // correspond to the enum values of View_outlineProvider
+ private static final int PROVIDER_BACKGROUND = 0;
+ private static final int PROVIDER_NONE = 1;
+ private static final int PROVIDER_BOUNDS = 2;
+ private static final int PROVIDER_PADDED_BOUNDS = 3;
+ private void setOutlineProviderFromAttribute(int providerInt) {
+ switch (providerInt) {
+ case PROVIDER_BACKGROUND:
+ setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ break;
+ case PROVIDER_NONE:
+ setOutlineProvider(null);
+ break;
+ case PROVIDER_BOUNDS:
+ setOutlineProvider(ViewOutlineProvider.BOUNDS);
+ break;
+ case PROVIDER_PADDED_BOUNDS:
+ setOutlineProvider(ViewOutlineProvider.PADDED_BOUNDS);
+ break;
+ }
+ }
+
+ /**
+ * Sets the {@link ViewOutlineProvider} of the view, which generates the Outline that defines
+ * the shape of the shadow it casts, and enables outline clipping.
+ * <p>
+ * The default ViewOutlineProvider, {@link ViewOutlineProvider#BACKGROUND}, queries the Outline
+ * from the View's background drawable, via {@link Drawable#getOutline(Outline)}. Changing the
+ * outline provider with this method allows this behavior to be overridden.
+ * <p>
+ * If the ViewOutlineProvider is null, if querying it for an outline returns false,
+ * or if the produced Outline is {@link Outline#isEmpty()}, shadows will not be cast.
+ * <p>
+ * Only outlines that return true from {@link Outline#canClip()} may be used for clipping.
+ *
+ * @see #setClipToOutline(boolean)
+ * @see #getClipToOutline()
+ * @see #getOutlineProvider()
+ */
+ public void setOutlineProvider(ViewOutlineProvider provider) {
+ mOutlineProvider = provider;
+ invalidateOutline();
+ }
+
+ /**
+ * Returns the current {@link ViewOutlineProvider} of the view, which generates the Outline
+ * that defines the shape of the shadow it casts, and enables outline clipping.
+ *
+ * @see #setOutlineProvider(ViewOutlineProvider)
+ */
+ @InspectableProperty
+ public ViewOutlineProvider getOutlineProvider() {
+ return mOutlineProvider;
+ }
+
+ /**
+ * Called to rebuild this View's Outline from its {@link ViewOutlineProvider outline provider}
+ *
+ * @see #setOutlineProvider(ViewOutlineProvider)
+ */
+ public void invalidateOutline() {
+ rebuildOutline();
+
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ invalidateViewProperty(false, false);
+ }
+
+ /**
+ * Internal version of {@link #invalidateOutline()} which invalidates the
+ * outline without invalidating the view itself. This is intended to be called from
+ * within methods in the View class itself which are the result of the view being
+ * invalidated already. For example, when we are drawing the background of a View,
+ * we invalidate the outline in case it changed in the meantime, but we do not
+ * need to invalidate the view because we're already drawing the background as part
+ * of drawing the view in response to an earlier invalidation of the view.
+ */
+ private void rebuildOutline() {
+ // Unattached views ignore this signal, and outline is recomputed in onAttachedToWindow()
+ if (mAttachInfo == null) return;
+
+ if (mOutlineProvider == null) {
+ // no provider, remove outline
+ mRenderNode.setOutline(null);
+ } else {
+ final Outline outline = mAttachInfo.mTmpOutline;
+ outline.setEmpty();
+ outline.setAlpha(1.0f);
+
+ mOutlineProvider.getOutline(this, outline);
+ mRenderNode.setOutline(outline);
+ }
+ }
+
+ /**
+ * HierarchyViewer only
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public boolean hasShadow() {
+ return mRenderNode.hasShadow();
+ }
+
+ /**
+ * Sets the color of the spot shadow that is drawn when the view has a positive Z or
+ * elevation value.
+ * <p>
+ * By default the shadow color is black. Generally, this color will be opaque so the intensity
+ * of the shadow is consistent between different views with different colors.
+ * <p>
+ * The opacity of the final spot shadow is a function of the shadow caster height, the
+ * alpha channel of the outlineSpotShadowColor (typically opaque), and the
+ * {@link android.R.attr#spotShadowAlpha} theme attribute.
+ *
+ * @attr ref android.R.styleable#View_outlineSpotShadowColor
+ * @param color The color this View will cast for its elevation spot shadow.
+ */
+ public void setOutlineSpotShadowColor(@ColorInt int color) {
+ if (mRenderNode.setSpotShadowColor(color)) {
+ invalidateViewProperty(true, true);
+ }
+ }
+
+ /**
+ * @return The shadow color set by {@link #setOutlineSpotShadowColor(int)}, or black if nothing
+ * was set
+ */
+ @InspectableProperty
+ public @ColorInt int getOutlineSpotShadowColor() {
+ return mRenderNode.getSpotShadowColor();
+ }
+
+ /**
+ * Sets the color of the ambient shadow that is drawn when the view has a positive Z or
+ * elevation value.
+ * <p>
+ * By default the shadow color is black. Generally, this color will be opaque so the intensity
+ * of the shadow is consistent between different views with different colors.
+ * <p>
+ * The opacity of the final ambient shadow is a function of the shadow caster height, the
+ * alpha channel of the outlineAmbientShadowColor (typically opaque), and the
+ * {@link android.R.attr#ambientShadowAlpha} theme attribute.
+ *
+ * @attr ref android.R.styleable#View_outlineAmbientShadowColor
+ * @param color The color this View will cast for its elevation shadow.
+ */
+ public void setOutlineAmbientShadowColor(@ColorInt int color) {
+ if (mRenderNode.setAmbientShadowColor(color)) {
+ invalidateViewProperty(true, true);
+ }
+ }
+
+ /**
+ * @return The shadow color set by {@link #setOutlineAmbientShadowColor(int)}, or black if
+ * nothing was set
+ */
+ @InspectableProperty
+ public @ColorInt int getOutlineAmbientShadowColor() {
+ return mRenderNode.getAmbientShadowColor();
+ }
+
+
+ /** @hide */
+ public void setRevealClip(boolean shouldClip, float x, float y, float radius) {
+ mRenderNode.setRevealClip(shouldClip, x, y, radius);
+ invalidateViewProperty(false, false);
+ }
+
+ /**
+ * Hit rectangle in parent's coordinates
+ *
+ * @param outRect The hit rectangle of the view.
+ */
+ public void getHitRect(Rect outRect) {
+ if (hasIdentityMatrix() || mAttachInfo == null) {
+ outRect.set(mLeft, mTop, mRight, mBottom);
+ } else {
+ final RectF tmpRect = mAttachInfo.mTmpTransformRect;
+ tmpRect.set(0, 0, getWidth(), getHeight());
+ getMatrix().mapRect(tmpRect); // TODO: mRenderNode.mapRect(tmpRect)
+ outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop,
+ (int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop);
+ }
+ }
+
+ /**
+ * Determines whether the given point, in local coordinates is inside the view.
+ */
+ /*package*/ final boolean pointInView(float localX, float localY) {
+ return pointInView(localX, localY, 0);
+ }
+
+ /**
+ * Utility method to determine whether the given point, in local coordinates,
+ * is inside the view, where the area of the view is expanded by the slop factor.
+ * This method is called while processing touch-move events to determine if the event
+ * is still within the view.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean pointInView(float localX, float localY, float slop) {
+ return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
+ localY < ((mBottom - mTop) + slop);
+ }
+
+ /**
+ * When a view has focus and the user navigates away from it, the next view is searched for
+ * starting from the rectangle filled in by this method.
+ *
+ * By default, the rectangle is the {@link #getDrawingRect(android.graphics.Rect)})
+ * of the view. However, if your view maintains some idea of internal selection,
+ * such as a cursor, or a selected row or column, you should override this method and
+ * fill in a more specific rectangle.
+ *
+ * @param r The rectangle to fill in, in this view's coordinates.
+ */
+ public void getFocusedRect(Rect r) {
+ getDrawingRect(r);
+ }
+
+ /**
+ * If some part of this view is not clipped by any of its parents, then
+ * return that area in r in global (root) coordinates. To convert r to local
+ * coordinates (without taking possible View rotations into account), offset
+ * it by -globalOffset (e.g. r.offset(-globalOffset.x, -globalOffset.y)).
+ * If the view is completely clipped or translated out, return false.
+ *
+ * @param r If true is returned, r holds the global coordinates of the
+ * visible portion of this view.
+ * @param globalOffset If true is returned, globalOffset holds the dx,dy
+ * between this view and its root. globalOffet may be null.
+ * @return true if r is non-empty (i.e. part of the view is visible at the
+ * root level.
+ */
+ public boolean getGlobalVisibleRect(Rect r, Point globalOffset) {
+ int width = mRight - mLeft;
+ int height = mBottom - mTop;
+ if (width > 0 && height > 0) {
+ r.set(0, 0, width, height);
+ if (globalOffset != null) {
+ globalOffset.set(-mScrollX, -mScrollY);
+ }
+ return mParent == null || mParent.getChildVisibleRect(this, r, globalOffset);
+ }
+ return false;
+ }
+
+ public final boolean getGlobalVisibleRect(Rect r) {
+ return getGlobalVisibleRect(r, null);
+ }
+
+ public final boolean getLocalVisibleRect(Rect r) {
+ final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
+ if (getGlobalVisibleRect(r, offset)) {
+ r.offset(-offset.x, -offset.y); // make r local
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Offset this view's vertical location by the specified number of pixels.
+ *
+ * @param offset the number of pixels to offset the view by
+ */
+ public void offsetTopAndBottom(int offset) {
+ if (offset != 0) {
+ final boolean matrixIsIdentity = hasIdentityMatrix();
+ if (matrixIsIdentity) {
+ if (isHardwareAccelerated()) {
+ invalidateViewProperty(false, false);
+ } else {
+ final ViewParent p = mParent;
+ if (p != null && mAttachInfo != null) {
+ final Rect r = mAttachInfo.mTmpInvalRect;
+ int minTop;
+ int maxBottom;
+ int yLoc;
+ if (offset < 0) {
+ minTop = mTop + offset;
+ maxBottom = mBottom;
+ yLoc = offset;
+ } else {
+ minTop = mTop;
+ maxBottom = mBottom + offset;
+ yLoc = 0;
+ }
+ r.set(0, yLoc, mRight - mLeft, maxBottom - minTop);
+ p.invalidateChild(this, r);
+ }
+ }
+ } else {
+ invalidateViewProperty(false, false);
+ }
+
+ mTop += offset;
+ mBottom += offset;
+ mRenderNode.offsetTopAndBottom(offset);
+ if (isHardwareAccelerated()) {
+ invalidateViewProperty(false, false);
+ invalidateParentIfNeededAndWasQuickRejected();
+ } else {
+ if (!matrixIsIdentity) {
+ invalidateViewProperty(false, true);
+ }
+ invalidateParentIfNeeded();
+ }
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * Offset this view's horizontal location by the specified amount of pixels.
+ *
+ * @param offset the number of pixels to offset the view by
+ */
+ public void offsetLeftAndRight(int offset) {
+ if (offset != 0) {
+ final boolean matrixIsIdentity = hasIdentityMatrix();
+ if (matrixIsIdentity) {
+ if (isHardwareAccelerated()) {
+ invalidateViewProperty(false, false);
+ } else {
+ final ViewParent p = mParent;
+ if (p != null && mAttachInfo != null) {
+ final Rect r = mAttachInfo.mTmpInvalRect;
+ int minLeft;
+ int maxRight;
+ if (offset < 0) {
+ minLeft = mLeft + offset;
+ maxRight = mRight;
+ } else {
+ minLeft = mLeft;
+ maxRight = mRight + offset;
+ }
+ r.set(0, 0, maxRight - minLeft, mBottom - mTop);
+ p.invalidateChild(this, r);
+ }
+ }
+ } else {
+ invalidateViewProperty(false, false);
+ }
+
+ mLeft += offset;
+ mRight += offset;
+ mRenderNode.offsetLeftAndRight(offset);
+ if (isHardwareAccelerated()) {
+ invalidateViewProperty(false, false);
+ invalidateParentIfNeededAndWasQuickRejected();
+ } else {
+ if (!matrixIsIdentity) {
+ invalidateViewProperty(false, true);
+ }
+ invalidateParentIfNeeded();
+ }
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ }
+
+ /**
+ * Get the LayoutParams associated with this view. All views should have
+ * layout parameters. These supply parameters to the <i>parent</i> of this
+ * view specifying how it should be arranged. There are many subclasses of
+ * ViewGroup.LayoutParams, and these correspond to the different subclasses
+ * of ViewGroup that are responsible for arranging their children.
+ *
+ * This method may return null if this View is not attached to a parent
+ * ViewGroup or {@link #setLayoutParams(android.view.ViewGroup.LayoutParams)}
+ * was not invoked successfully. When a View is attached to a parent
+ * ViewGroup, this method must not return null.
+ *
+ * @return The LayoutParams associated with this view, or null if no
+ * parameters have been set yet
+ */
+ @ViewDebug.ExportedProperty(deepExport = true, prefix = "layout_")
+ public ViewGroup.LayoutParams getLayoutParams() {
+ return mLayoutParams;
+ }
+
+ /**
+ * Set the layout parameters associated with this view. These supply
+ * parameters to the <i>parent</i> of this view specifying how it should be
+ * arranged. There are many subclasses of ViewGroup.LayoutParams, and these
+ * correspond to the different subclasses of ViewGroup that are responsible
+ * for arranging their children.
+ *
+ * @param params The layout parameters for this view, cannot be null
+ */
+ public void setLayoutParams(ViewGroup.LayoutParams params) {
+ if (params == null) {
+ throw new NullPointerException("Layout parameters cannot be null");
+ }
+ mLayoutParams = params;
+ resolveLayoutParams();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).onSetLayoutParams(this, params);
+ }
+ requestLayout();
+ }
+
+ /**
+ * Resolve the layout parameters depending on the resolved layout direction
+ *
+ * @hide
+ */
+ public void resolveLayoutParams() {
+ if (mLayoutParams != null) {
+ mLayoutParams.resolveLayoutDirection(getLayoutDirection());
+ }
+ }
+
+ /**
+ * Set the scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param x the x position to scroll to
+ * @param y the y position to scroll to
+ */
+ public void scrollTo(int x, int y) {
+ if (mScrollX != x || mScrollY != y) {
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ mScrollX = x;
+ mScrollY = y;
+ invalidateParentCaches();
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ if (!awakenScrollBars()) {
+ postInvalidateOnAnimation();
+ }
+ }
+ }
+
+ /**
+ * Move the scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param x the amount of pixels to scroll by horizontally
+ * @param y the amount of pixels to scroll by vertically
+ */
+ public void scrollBy(int x, int y) {
+ scrollTo(mScrollX + x, mScrollY + y);
+ }
+
+ /**
+ * <p>Trigger the scrollbars to draw. When invoked this method starts an
+ * animation to fade the scrollbars out after a default delay. If a subclass
+ * provides animated scrolling, the start delay should equal the duration
+ * of the scrolling animation.</p>
+ *
+ * <p>The animation starts only if at least one of the scrollbars is
+ * enabled, as specified by {@link #isHorizontalScrollBarEnabled()} and
+ * {@link #isVerticalScrollBarEnabled()}. When the animation is started,
+ * this method returns true, and false otherwise. If the animation is
+ * started, this method calls {@link #invalidate()}; in that case the
+ * caller should not call {@link #invalidate()}.</p>
+ *
+ * <p>This method should be invoked every time a subclass directly updates
+ * the scroll parameters.</p>
+ *
+ * <p>This method is automatically invoked by {@link #scrollBy(int, int)}
+ * and {@link #scrollTo(int, int)}.</p>
+ *
+ * @return true if the animation is played, false otherwise
+ *
+ * @see #awakenScrollBars(int)
+ * @see #scrollBy(int, int)
+ * @see #scrollTo(int, int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ protected boolean awakenScrollBars() {
+ return mScrollCache != null &&
+ awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade, true);
+ }
+
+ /**
+ * Trigger the scrollbars to draw.
+ * This method differs from awakenScrollBars() only in its default duration.
+ * initialAwakenScrollBars() will show the scroll bars for longer than
+ * usual to give the user more of a chance to notice them.
+ *
+ * @return true if the animation is played, false otherwise.
+ */
+ private boolean initialAwakenScrollBars() {
+ return mScrollCache != null &&
+ awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade * 4, true);
+ }
+
+ /**
+ * <p>
+ * Trigger the scrollbars to draw. When invoked this method starts an
+ * animation to fade the scrollbars out after a fixed delay. If a subclass
+ * provides animated scrolling, the start delay should equal the duration of
+ * the scrolling animation.
+ * </p>
+ *
+ * <p>
+ * The animation starts only if at least one of the scrollbars is enabled,
+ * as specified by {@link #isHorizontalScrollBarEnabled()} and
+ * {@link #isVerticalScrollBarEnabled()}. When the animation is started,
+ * this method returns true, and false otherwise. If the animation is
+ * started, this method calls {@link #invalidate()}; in that case the caller
+ * should not call {@link #invalidate()}.
+ * </p>
+ *
+ * <p>
+ * This method should be invoked every time a subclass directly updates the
+ * scroll parameters.
+ * </p>
+ *
+ * @param startDelay the delay, in milliseconds, after which the animation
+ * should start; when the delay is 0, the animation starts
+ * immediately
+ * @return true if the animation is played, false otherwise
+ *
+ * @see #scrollBy(int, int)
+ * @see #scrollTo(int, int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ protected boolean awakenScrollBars(int startDelay) {
+ return awakenScrollBars(startDelay, true);
+ }
+
+ /**
+ * <p>
+ * Trigger the scrollbars to draw. When invoked this method starts an
+ * animation to fade the scrollbars out after a fixed delay. If a subclass
+ * provides animated scrolling, the start delay should equal the duration of
+ * the scrolling animation.
+ * </p>
+ *
+ * <p>
+ * The animation starts only if at least one of the scrollbars is enabled,
+ * as specified by {@link #isHorizontalScrollBarEnabled()} and
+ * {@link #isVerticalScrollBarEnabled()}. When the animation is started,
+ * this method returns true, and false otherwise. If the animation is
+ * started, this method calls {@link #invalidate()} if the invalidate parameter
+ * is set to true; in that case the caller
+ * should not call {@link #invalidate()}.
+ * </p>
+ *
+ * <p>
+ * This method should be invoked every time a subclass directly updates the
+ * scroll parameters.
+ * </p>
+ *
+ * @param startDelay the delay, in milliseconds, after which the animation
+ * should start; when the delay is 0, the animation starts
+ * immediately
+ *
+ * @param invalidate Whether this method should call invalidate
+ *
+ * @return true if the animation is played, false otherwise
+ *
+ * @see #scrollBy(int, int)
+ * @see #scrollTo(int, int)
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #isVerticalScrollBarEnabled()
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ protected boolean awakenScrollBars(int startDelay, boolean invalidate) {
+ final ScrollabilityCache scrollCache = mScrollCache;
+
+ if (scrollCache == null || !scrollCache.fadeScrollBars) {
+ return false;
+ }
+
+ if (scrollCache.scrollBar == null) {
+ scrollCache.scrollBar = new ScrollBarDrawable();
+ scrollCache.scrollBar.setState(getDrawableState());
+ scrollCache.scrollBar.setCallback(this);
+ }
+
+ if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) {
+
+ if (invalidate) {
+ // Invalidate to show the scrollbars
+ postInvalidateOnAnimation();
+ }
+
+ if (scrollCache.state == ScrollabilityCache.OFF) {
+ // FIXME: this is copied from WindowManagerService.
+ // We should get this value from the system when it
+ // is possible to do so.
+ final int KEY_REPEAT_FIRST_DELAY = 750;
+ startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);
+ }
+
+ // Tell mScrollCache when we should start fading. This may
+ // extend the fade start time if one was already scheduled
+ long fadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;
+ scrollCache.fadeStartTime = fadeStartTime;
+ scrollCache.state = ScrollabilityCache.ON;
+
+ // Schedule our fader to run, unscheduling any old ones first
+ if (mAttachInfo != null) {
+ mAttachInfo.mHandler.removeCallbacks(scrollCache);
+ mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Do not invalidate views which are not visible and which are not running an animation. They
+ * will not get drawn and they should not set dirty flags as if they will be drawn
+ */
+ private boolean skipInvalidate() {
+ return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
+ (!(mParent instanceof ViewGroup) ||
+ !((ViewGroup) mParent).isViewTransitioning(this));
+ }
+
+ /**
+ * Mark the area defined by dirty as needing to be drawn. If the view is
+ * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
+ * point in the future.
+ * <p>
+ * This must be called from a UI thread. To call from a non-UI thread, call
+ * {@link #postInvalidate()}.
+ * <p>
+ * <b>WARNING:</b> In API 19 and below, this method may be destructive to
+ * {@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;
+ invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
+ dirty.right - scrollX, dirty.bottom - scrollY, true, false);
+ }
+
+ /**
+ * Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The
+ * coordinates of the dirty rect are relative to the view. If the view is
+ * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
+ * point in the future.
+ * <p>
+ * This must be called from a UI thread. To call from a non-UI thread, call
+ * {@link #postInvalidate()}.
+ *
+ * @param l the left position of the dirty region
+ * @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;
+ invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
+ }
+
+ /**
+ * Invalidate the whole view. If the view is visible,
+ * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
+ * the future.
+ * <p>
+ * This must be called from a UI thread. To call from a non-UI thread, call
+ * {@link #postInvalidate()}.
+ */
+ public void invalidate() {
+ invalidate(true);
+ }
+
+ /**
+ * This is where the invalidate() work actually happens. A full invalidate()
+ * causes the drawing cache to be invalidated, but this function can be
+ * called with invalidateCache set to false to skip that invalidation step
+ * for cases that do not need it (for example, a component that remains at
+ * the same dimensions with the same content).
+ *
+ * @param invalidateCache Whether the drawing cache for this view should be
+ * invalidated as well. This is usually true for a full
+ * invalidate, but may be set to false if the View's contents or
+ * dimensions have not changed.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void invalidate(boolean invalidateCache) {
+ invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
+ }
+
+ void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
+ boolean fullInvalidate) {
+ if (mGhostView != null) {
+ mGhostView.invalidate(true);
+ return;
+ }
+
+ if (skipInvalidate()) {
+ return;
+ }
+
+ // Reset content capture caches
+ mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK;
+ mContentCaptureSessionCached = false;
+
+ if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
+ || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
+ || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
+ || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
+ if (fullInvalidate) {
+ mLastIsOpaque = isOpaque();
+ mPrivateFlags &= ~PFLAG_DRAWN;
+ }
+
+ mPrivateFlags |= PFLAG_DIRTY;
+
+ if (invalidateCache) {
+ mPrivateFlags |= PFLAG_INVALIDATED;
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ }
+
+ // Propagate the damage rectangle to the parent view.
+ final AttachInfo ai = mAttachInfo;
+ final ViewParent p = mParent;
+ if (p != null && ai != null && l < r && t < b) {
+ final Rect damage = ai.mTmpInvalRect;
+ damage.set(l, t, r, b);
+ p.invalidateChild(this, damage);
+ }
+
+ // Damage the entire projection receiver, if necessary.
+ if (mBackground != null && mBackground.isProjected()) {
+ final View receiver = getProjectionReceiver();
+ if (receiver != null) {
+ receiver.damageInParent();
+ }
+ }
+ }
+ }
+
+ /**
+ * @return this view's projection receiver, or {@code null} if none exists
+ */
+ private View getProjectionReceiver() {
+ ViewParent p = getParent();
+ while (p != null && p instanceof View) {
+ final View v = (View) p;
+ if (v.isProjectionReceiver()) {
+ return v;
+ }
+ p = p.getParent();
+ }
+
+ return null;
+ }
+
+ /**
+ * @return whether the view is a projection receiver
+ */
+ private boolean isProjectionReceiver() {
+ return mBackground != null;
+ }
+
+ /**
+ * Quick invalidation for View property changes (alpha, translationXY, etc.). We don't want to
+ * set any flags or handle all of the cases handled by the default invalidation methods.
+ * Instead, we just want to schedule a traversal in ViewRootImpl with the appropriate
+ * dirty rect. This method calls into fast invalidation methods in ViewGroup that
+ * walk up the hierarchy, transforming the dirty rect as necessary.
+ *
+ * The method also handles normal invalidation logic if display list properties are not
+ * being used in this view. The invalidateParent and forceRedraw flags are used by that
+ * backup approach, to handle these cases used in the various property-setting methods.
+ *
+ * @param invalidateParent Force a call to invalidateParentCaches() if display list properties
+ * are not being used in this view
+ * @param forceRedraw Mark the view as DRAWN to force the invalidation to propagate, if display
+ * list properties are not being used in this view
+ */
+ @UnsupportedAppUsage
+ void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
+ if (!isHardwareAccelerated()
+ || !mRenderNode.hasDisplayList()
+ || (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
+ if (invalidateParent) {
+ invalidateParentCaches();
+ }
+ if (forceRedraw) {
+ mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
+ }
+ invalidate(false);
+ } else {
+ damageInParent();
+ }
+ }
+
+ /**
+ * Tells the parent view to damage this view's bounds.
+ *
+ * @hide
+ */
+ protected void damageInParent() {
+ if (mParent != null && mAttachInfo != null) {
+ mParent.onDescendantInvalidated(this, this);
+ }
+ }
+
+ /**
+ * Used to indicate that the parent of this view should clear its caches. This functionality
+ * is used to force the parent to rebuild its display list (when hardware-accelerated),
+ * which is necessary when various parent-managed properties of the view change, such as
+ * alpha, translationX/Y, scrollX/Y, scaleX/Y, and rotation/X/Y. This method only
+ * clears the parent caches and does not causes an invalidate event.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void invalidateParentCaches() {
+ if (mParent instanceof View) {
+ ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
+ }
+ }
+
+ /**
+ * Used to indicate that the parent of this view should be invalidated. This functionality
+ * is used to force the parent to rebuild its display list (when hardware-accelerated),
+ * which is necessary when various parent-managed properties of the view change, such as
+ * alpha, translationX/Y, scrollX/Y, scaleX/Y, and rotation/X/Y. This method will propagate
+ * an invalidation event to the parent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void invalidateParentIfNeeded() {
+ if (isHardwareAccelerated() && mParent instanceof View) {
+ ((View) mParent).invalidate(true);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected void invalidateParentIfNeededAndWasQuickRejected() {
+ if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) != 0) {
+ // View was rejected last time it was drawn by its parent; this may have changed
+ invalidateParentIfNeeded();
+ }
+ }
+
+ /**
+ * Indicates whether this View is opaque. An opaque View guarantees that it will
+ * draw all the pixels overlapping its bounds using a fully opaque color.
+ *
+ * Subclasses of View should override this method whenever possible to indicate
+ * whether an instance is opaque. Opaque Views are treated in a special way by
+ * the View hierarchy, possibly allowing it to perform optimizations during
+ * invalidate/draw passes.
+ *
+ * @return True if this View is guaranteed to be fully opaque, false otherwise.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public boolean isOpaque() {
+ return (mPrivateFlags & PFLAG_OPAQUE_MASK) == PFLAG_OPAQUE_MASK &&
+ getFinalAlpha() >= 1.0f;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void computeOpaqueFlags() {
+ // Opaque if:
+ // - Has a background
+ // - Background is opaque
+ // - Doesn't have scrollbars or scrollbars overlay
+
+ if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
+ mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
+ } else {
+ mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
+ }
+
+ final int flags = mViewFlags;
+ if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
+ (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
+ (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
+ mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
+ } else {
+ mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected boolean hasOpaqueScrollbars() {
+ return (mPrivateFlags & PFLAG_OPAQUE_SCROLLBARS) == PFLAG_OPAQUE_SCROLLBARS;
+ }
+
+ /**
+ * @return A handler associated with the thread running the View. This
+ * handler can be used to pump events in the UI events queue.
+ */
+ public Handler getHandler() {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ return attachInfo.mHandler;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the queue of runnable for this view.
+ *
+ * @return the queue of runnables for this view
+ */
+ private HandlerActionQueue getRunQueue() {
+ if (mRunQueue == null) {
+ mRunQueue = new HandlerActionQueue();
+ }
+ return mRunQueue;
+ }
+
+ /**
+ * Gets the view root associated with the View.
+ * @return The view root, or null if none.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public ViewRootImpl getViewRootImpl() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mViewRootImpl;
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public ThreadedRenderer getThreadedRenderer() {
+ return mAttachInfo != null ? mAttachInfo.mThreadedRenderer : null;
+ }
+
+ /**
+ * <p>Causes the Runnable to be added to the message queue.
+ * The runnable will be run on the user interface thread.</p>
+ *
+ * @param action The Runnable that will be executed.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ *
+ * @see #postDelayed
+ * @see #removeCallbacks
+ */
+ public boolean post(Runnable action) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ return attachInfo.mHandler.post(action);
+ }
+
+ // Postpone the runnable until we know on which thread it needs to run.
+ // Assume that the runnable will be successfully placed after attach.
+ getRunQueue().post(action);
+ return true;
+ }
+
+ /**
+ * <p>Causes the Runnable to be added to the message queue, to be run
+ * after the specified amount of time elapses.
+ * The runnable will be run on the user interface thread.</p>
+ *
+ * @param action The Runnable that will be executed.
+ * @param delayMillis The delay (in milliseconds) until the Runnable
+ * will be executed.
+ *
+ * @return true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed --
+ * if the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ *
+ * @see #post
+ * @see #removeCallbacks
+ */
+ public boolean postDelayed(Runnable action, long delayMillis) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ return attachInfo.mHandler.postDelayed(action, delayMillis);
+ }
+
+ // Postpone the runnable until we know on which thread it needs to run.
+ // Assume that the runnable will be successfully placed after attach.
+ getRunQueue().postDelayed(action, delayMillis);
+ return true;
+ }
+
+ /**
+ * <p>Causes the Runnable to execute on the next animation time step.
+ * The runnable will be run on the user interface thread.</p>
+ *
+ * @param action The Runnable that will be executed.
+ *
+ * @see #postOnAnimationDelayed
+ * @see #removeCallbacks
+ */
+ public void postOnAnimation(Runnable action) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ attachInfo.mViewRootImpl.mChoreographer.postCallback(
+ Choreographer.CALLBACK_ANIMATION, action, null);
+ } else {
+ // Postpone the runnable until we know
+ // on which thread it needs to run.
+ getRunQueue().post(action);
+ }
+ }
+
+ /**
+ * <p>Causes the Runnable to execute on the next animation time step,
+ * after the specified amount of time elapses.
+ * The runnable will be run on the user interface thread.</p>
+ *
+ * @param action The Runnable that will be executed.
+ * @param delayMillis The delay (in milliseconds) until the Runnable
+ * will be executed.
+ *
+ * @see #postOnAnimation
+ * @see #removeCallbacks
+ */
+ public void postOnAnimationDelayed(Runnable action, long delayMillis) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ attachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
+ Choreographer.CALLBACK_ANIMATION, action, null, delayMillis);
+ } else {
+ // Postpone the runnable until we know
+ // on which thread it needs to run.
+ getRunQueue().postDelayed(action, delayMillis);
+ }
+ }
+
+ /**
+ * <p>Removes the specified Runnable from the message queue.</p>
+ *
+ * @param action The Runnable to remove from the message handling queue
+ *
+ * @return true if this view could ask the Handler to remove the Runnable,
+ * false otherwise. When the returned value is true, the Runnable
+ * may or may not have been actually removed from the message queue
+ * (for instance, if the Runnable was not in the queue already.)
+ *
+ * @see #post
+ * @see #postDelayed
+ * @see #postOnAnimation
+ * @see #postOnAnimationDelayed
+ */
+ public boolean removeCallbacks(Runnable action) {
+ if (action != null) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ attachInfo.mHandler.removeCallbacks(action);
+ attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_ANIMATION, action, null);
+ }
+ getRunQueue().removeCallbacks(action);
+ }
+ return true;
+ }
+
+ /**
+ * <p>Cause an invalidate to happen on a subsequent cycle through the event loop.
+ * Use this to invalidate the View from a non-UI thread.</p>
+ *
+ * <p>This method can be invoked from outside of the UI thread
+ * only when this View is attached to a window.</p>
+ *
+ * @see #invalidate()
+ * @see #postInvalidateDelayed(long)
+ */
+ public void postInvalidate() {
+ postInvalidateDelayed(0);
+ }
+
+ /**
+ * <p>Cause an invalidate of the specified area to happen on a subsequent cycle
+ * through the event loop. Use this to invalidate the View from a non-UI thread.</p>
+ *
+ * <p>This method can be invoked from outside of the UI thread
+ * only when this View is attached to a window.</p>
+ *
+ * @param left The left coordinate of the rectangle to invalidate.
+ * @param top The top coordinate of the rectangle to invalidate.
+ * @param right The right coordinate of the rectangle to invalidate.
+ * @param bottom The bottom coordinate of the rectangle to invalidate.
+ *
+ * @see #invalidate(int, int, int, int)
+ * @see #invalidate(Rect)
+ * @see #postInvalidateDelayed(long, int, int, int, int)
+ */
+ public void postInvalidate(int left, int top, int right, int bottom) {
+ postInvalidateDelayed(0, left, top, right, bottom);
+ }
+
+ /**
+ * <p>Cause an invalidate to happen on a subsequent cycle through the event
+ * loop. Waits for the specified amount of time.</p>
+ *
+ * <p>This method can be invoked from outside of the UI thread
+ * only when this View is attached to a window.</p>
+ *
+ * @param delayMilliseconds the duration in milliseconds to delay the
+ * invalidation by
+ *
+ * @see #invalidate()
+ * @see #postInvalidate()
+ */
+ public void postInvalidateDelayed(long delayMilliseconds) {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
+ }
+ }
+
+ /**
+ * <p>Cause an invalidate of the specified area to happen on a subsequent cycle
+ * through the event loop. Waits for the specified amount of time.</p>
+ *
+ * <p>This method can be invoked from outside of the UI thread
+ * only when this View is attached to a window.</p>
+ *
+ * @param delayMilliseconds the duration in milliseconds to delay the
+ * invalidation by
+ * @param left The left coordinate of the rectangle to invalidate.
+ * @param top The top coordinate of the rectangle to invalidate.
+ * @param right The right coordinate of the rectangle to invalidate.
+ * @param bottom The bottom coordinate of the rectangle to invalidate.
+ *
+ * @see #invalidate(int, int, int, int)
+ * @see #invalidate(Rect)
+ * @see #postInvalidate(int, int, int, int)
+ */
+ public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
+ int right, int bottom) {
+
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
+ info.target = this;
+ info.left = left;
+ info.top = top;
+ info.right = right;
+ info.bottom = bottom;
+
+ attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds);
+ }
+ }
+
+ /**
+ * <p>Cause an invalidate to happen on the next animation time step, typically the
+ * next display frame.</p>
+ *
+ * <p>This method can be invoked from outside of the UI thread
+ * only when this View is attached to a window.</p>
+ *
+ * @see #invalidate()
+ */
+ public void postInvalidateOnAnimation() {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
+ }
+ }
+
+ /**
+ * <p>Cause an invalidate of the specified area to happen on the next animation
+ * time step, typically the next display frame.</p>
+ *
+ * <p>This method can be invoked from outside of the UI thread
+ * only when this View is attached to a window.</p>
+ *
+ * @param left The left coordinate of the rectangle to invalidate.
+ * @param top The top coordinate of the rectangle to invalidate.
+ * @param right The right coordinate of the rectangle to invalidate.
+ * @param bottom The bottom coordinate of the rectangle to invalidate.
+ *
+ * @see #invalidate(int, int, int, int)
+ * @see #invalidate(Rect)
+ */
+ public void postInvalidateOnAnimation(int left, int top, int right, int bottom) {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
+ info.target = this;
+ info.left = left;
+ info.top = top;
+ info.right = right;
+ info.bottom = bottom;
+
+ attachInfo.mViewRootImpl.dispatchInvalidateRectOnAnimation(info);
+ }
+ }
+
+ /**
+ * Post a callback to send a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
+ * This event is sent at most once every
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
+ */
+ private void postSendViewScrolledAccessibilityEventCallback(int dx, int dy) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+ event.setScrollDeltaX(dx);
+ event.setScrollDeltaY(dy);
+ sendAccessibilityEventUnchecked(event);
+ }
+ }
+
+ /**
+ * Called by a parent to request that a child update its values for mScrollX
+ * and mScrollY if necessary. This will typically be done if the child is
+ * animating a scroll using a {@link android.widget.Scroller Scroller}
+ * object.
+ */
+ public void computeScroll() {
+ }
+
+ /**
+ * <p>Indicate whether the horizontal edges are faded when the view is
+ * scrolled horizontally.</p>
+ *
+ * @return true if the horizontal edges should are faded on scroll, false
+ * otherwise
+ *
+ * @see #setHorizontalFadingEdgeEnabled(boolean)
+ *
+ * @attr ref android.R.styleable#View_requiresFadingEdge
+ */
+ public boolean isHorizontalFadingEdgeEnabled() {
+ return (mViewFlags & FADING_EDGE_HORIZONTAL) == FADING_EDGE_HORIZONTAL;
+ }
+
+ /**
+ * <p>Define whether the horizontal edges should be faded when this view
+ * is scrolled horizontally.</p>
+ *
+ * @param horizontalFadingEdgeEnabled true if the horizontal edges should
+ * be faded when the view is scrolled
+ * horizontally
+ *
+ * @see #isHorizontalFadingEdgeEnabled()
+ *
+ * @attr ref android.R.styleable#View_requiresFadingEdge
+ */
+ public void setHorizontalFadingEdgeEnabled(boolean horizontalFadingEdgeEnabled) {
+ if (isHorizontalFadingEdgeEnabled() != horizontalFadingEdgeEnabled) {
+ if (horizontalFadingEdgeEnabled) {
+ initScrollCache();
+ }
+
+ mViewFlags ^= FADING_EDGE_HORIZONTAL;
+ }
+ }
+
+ /**
+ * <p>Indicate whether the vertical edges are faded when the view is
+ * scrolled horizontally.</p>
+ *
+ * @return true if the vertical edges should are faded on scroll, false
+ * otherwise
+ *
+ * @see #setVerticalFadingEdgeEnabled(boolean)
+ *
+ * @attr ref android.R.styleable#View_requiresFadingEdge
+ */
+ public boolean isVerticalFadingEdgeEnabled() {
+ return (mViewFlags & FADING_EDGE_VERTICAL) == FADING_EDGE_VERTICAL;
+ }
+
+ /**
+ * <p>Define whether the vertical edges should be faded when this view
+ * is scrolled vertically.</p>
+ *
+ * @param verticalFadingEdgeEnabled true if the vertical edges should
+ * be faded when the view is scrolled
+ * vertically
+ *
+ * @see #isVerticalFadingEdgeEnabled()
+ *
+ * @attr ref android.R.styleable#View_requiresFadingEdge
+ */
+ public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
+ if (isVerticalFadingEdgeEnabled() != verticalFadingEdgeEnabled) {
+ if (verticalFadingEdgeEnabled) {
+ initScrollCache();
+ }
+
+ mViewFlags ^= FADING_EDGE_VERTICAL;
+ }
+ }
+
+ /**
+ * Get the fading edge flags, used for inspection.
+ *
+ * @return One of {@link #FADING_EDGE_NONE}, {@link #FADING_EDGE_VERTICAL},
+ * or {@link #FADING_EDGE_HORIZONTAL}
+ * @hide
+ */
+ @InspectableProperty(name = "requiresFadingEdge", flagMapping = {
+ @FlagEntry(target = FADING_EDGE_NONE, mask = FADING_EDGE_MASK, name = "none"),
+ @FlagEntry(target = FADING_EDGE_VERTICAL, name = "vertical"),
+ @FlagEntry(target = FADING_EDGE_HORIZONTAL, name = "horizontal")
+ })
+ public int getFadingEdge() {
+ return mViewFlags & FADING_EDGE_MASK;
+ }
+
+ /**
+ * Get the fading edge length, used for inspection
+ *
+ * @return The fading edge length or 0
+ * @hide
+ */
+ @InspectableProperty
+ public int getFadingEdgeLength() {
+ if (mScrollCache != null && (mViewFlags & FADING_EDGE_MASK) != FADING_EDGE_NONE) {
+ return mScrollCache.fadingEdgeLength;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the top faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the top fade as a float between 0.0f and 1.0f
+ */
+ protected float getTopFadingEdgeStrength() {
+ return computeVerticalScrollOffset() > 0 ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the bottom faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the bottom fade as a float between 0.0f and 1.0f
+ */
+ protected float getBottomFadingEdgeStrength() {
+ return computeVerticalScrollOffset() + computeVerticalScrollExtent() <
+ computeVerticalScrollRange() ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the left faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the left fade as a float between 0.0f and 1.0f
+ */
+ protected float getLeftFadingEdgeStrength() {
+ return computeHorizontalScrollOffset() > 0 ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the right faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the right fade as a float between 0.0f and 1.0f
+ */
+ protected float getRightFadingEdgeStrength() {
+ return computeHorizontalScrollOffset() + computeHorizontalScrollExtent() <
+ computeHorizontalScrollRange() ? 1.0f : 0.0f;
+ }
+
+ /**
+ * <p>Indicate whether the horizontal scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @return true if the horizontal scrollbar should be painted, false
+ * otherwise
+ *
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ */
+ public boolean isHorizontalScrollBarEnabled() {
+ return (mViewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
+ }
+
+ /**
+ * <p>Define whether the horizontal scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @param horizontalScrollBarEnabled true if the horizontal scrollbar should
+ * be painted
+ *
+ * @see #isHorizontalScrollBarEnabled()
+ */
+ public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) {
+ if (isHorizontalScrollBarEnabled() != horizontalScrollBarEnabled) {
+ mViewFlags ^= SCROLLBARS_HORIZONTAL;
+ computeOpaqueFlags();
+ resolvePadding();
+ }
+ }
+
+ /**
+ * <p>Indicate whether the vertical scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @return true if the vertical scrollbar should be painted, false
+ * otherwise
+ *
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ public boolean isVerticalScrollBarEnabled() {
+ return (mViewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL;
+ }
+
+ /**
+ * <p>Define whether the vertical scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @param verticalScrollBarEnabled true if the vertical scrollbar should
+ * be painted
+ *
+ * @see #isVerticalScrollBarEnabled()
+ */
+ public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) {
+ if (isVerticalScrollBarEnabled() != verticalScrollBarEnabled) {
+ mViewFlags ^= SCROLLBARS_VERTICAL;
+ computeOpaqueFlags();
+ resolvePadding();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void recomputePadding() {
+ internalSetPadding(mUserPaddingLeft, mPaddingTop, mUserPaddingRight, mUserPaddingBottom);
+ }
+
+ /**
+ * Define whether scrollbars will fade when the view is not scrolling.
+ *
+ * @param fadeScrollbars whether to enable fading
+ *
+ * @attr ref android.R.styleable#View_fadeScrollbars
+ */
+ public void setScrollbarFadingEnabled(boolean fadeScrollbars) {
+ initScrollCache();
+ final ScrollabilityCache scrollabilityCache = mScrollCache;
+ scrollabilityCache.fadeScrollBars = fadeScrollbars;
+ if (fadeScrollbars) {
+ scrollabilityCache.state = ScrollabilityCache.OFF;
+ } else {
+ scrollabilityCache.state = ScrollabilityCache.ON;
+ }
+ }
+
+ /**
+ *
+ * Returns true if scrollbars will fade when this view is not scrolling
+ *
+ * @return true if scrollbar fading is enabled
+ *
+ * @attr ref android.R.styleable#View_fadeScrollbars
+ */
+ public boolean isScrollbarFadingEnabled() {
+ return mScrollCache != null && mScrollCache.fadeScrollBars;
+ }
+
+ /**
+ *
+ * Returns the delay before scrollbars fade.
+ *
+ * @return the delay before scrollbars fade
+ *
+ * @attr ref android.R.styleable#View_scrollbarDefaultDelayBeforeFade
+ */
+ @InspectableProperty(name = "scrollbarDefaultDelayBeforeFade")
+ public int getScrollBarDefaultDelayBeforeFade() {
+ return mScrollCache == null ? ViewConfiguration.getScrollDefaultDelay() :
+ mScrollCache.scrollBarDefaultDelayBeforeFade;
+ }
+
+ /**
+ * Define the delay before scrollbars fade.
+ *
+ * @param scrollBarDefaultDelayBeforeFade - the delay before scrollbars fade
+ *
+ * @attr ref android.R.styleable#View_scrollbarDefaultDelayBeforeFade
+ */
+ public void setScrollBarDefaultDelayBeforeFade(int scrollBarDefaultDelayBeforeFade) {
+ getScrollCache().scrollBarDefaultDelayBeforeFade = scrollBarDefaultDelayBeforeFade;
+ }
+
+ /**
+ *
+ * Returns the scrollbar fade duration.
+ *
+ * @return the scrollbar fade duration, in milliseconds
+ *
+ * @attr ref android.R.styleable#View_scrollbarFadeDuration
+ */
+ @InspectableProperty(name = "scrollbarFadeDuration")
+ public int getScrollBarFadeDuration() {
+ return mScrollCache == null ? ViewConfiguration.getScrollBarFadeDuration() :
+ mScrollCache.scrollBarFadeDuration;
+ }
+
+ /**
+ * Define the scrollbar fade duration.
+ *
+ * @param scrollBarFadeDuration - the scrollbar fade duration, in milliseconds
+ *
+ * @attr ref android.R.styleable#View_scrollbarFadeDuration
+ */
+ public void setScrollBarFadeDuration(int scrollBarFadeDuration) {
+ getScrollCache().scrollBarFadeDuration = scrollBarFadeDuration;
+ }
+
+ /**
+ *
+ * Returns the scrollbar size.
+ *
+ * @return the scrollbar size
+ *
+ * @attr ref android.R.styleable#View_scrollbarSize
+ */
+ @InspectableProperty(name = "scrollbarSize")
+ public int getScrollBarSize() {
+ return mScrollCache == null ? ViewConfiguration.get(mContext).getScaledScrollBarSize() :
+ mScrollCache.scrollBarSize;
+ }
+
+ /**
+ * Define the scrollbar size.
+ *
+ * @param scrollBarSize - the scrollbar size
+ *
+ * @attr ref android.R.styleable#View_scrollbarSize
+ */
+ public void setScrollBarSize(int scrollBarSize) {
+ getScrollCache().scrollBarSize = scrollBarSize;
+ }
+
+ /**
+ * <p>Specify the style of the scrollbars. The scrollbars can be overlaid or
+ * inset. When inset, they add to the padding of the view. And the scrollbars
+ * can be drawn inside the padding area or on the edge of the view. For example,
+ * if a view has a background drawable and you want to draw the scrollbars
+ * inside the padding specified by the drawable, you can use
+ * SCROLLBARS_INSIDE_OVERLAY or SCROLLBARS_INSIDE_INSET. If you want them to
+ * appear at the edge of the view, ignoring the padding, then you can use
+ * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET.</p>
+ * @param style the style of the scrollbars. Should be one of
+ * SCROLLBARS_INSIDE_OVERLAY, SCROLLBARS_INSIDE_INSET,
+ * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET.
+ * @see #SCROLLBARS_INSIDE_OVERLAY
+ * @see #SCROLLBARS_INSIDE_INSET
+ * @see #SCROLLBARS_OUTSIDE_OVERLAY
+ * @see #SCROLLBARS_OUTSIDE_INSET
+ *
+ * @attr ref android.R.styleable#View_scrollbarStyle
+ */
+ public void setScrollBarStyle(@ScrollBarStyle int style) {
+ if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) {
+ mViewFlags = (mViewFlags & ~SCROLLBARS_STYLE_MASK) | (style & SCROLLBARS_STYLE_MASK);
+ computeOpaqueFlags();
+ resolvePadding();
+ }
+ }
+
+ /**
+ * <p>Returns the current scrollbar style.</p>
+ * @return the current scrollbar style
+ * @see #SCROLLBARS_INSIDE_OVERLAY
+ * @see #SCROLLBARS_INSIDE_INSET
+ * @see #SCROLLBARS_OUTSIDE_OVERLAY
+ * @see #SCROLLBARS_OUTSIDE_INSET
+ *
+ * @attr ref android.R.styleable#View_scrollbarStyle
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = SCROLLBARS_INSIDE_OVERLAY, to = "INSIDE_OVERLAY"),
+ @ViewDebug.IntToString(from = SCROLLBARS_INSIDE_INSET, to = "INSIDE_INSET"),
+ @ViewDebug.IntToString(from = SCROLLBARS_OUTSIDE_OVERLAY, to = "OUTSIDE_OVERLAY"),
+ @ViewDebug.IntToString(from = SCROLLBARS_OUTSIDE_INSET, to = "OUTSIDE_INSET")
+ })
+ @InspectableProperty(name = "scrollbarStyle", enumMapping = {
+ @EnumEntry(value = SCROLLBARS_INSIDE_OVERLAY, name = "insideOverlay"),
+ @EnumEntry(value = SCROLLBARS_INSIDE_INSET, name = "insideInset"),
+ @EnumEntry(value = SCROLLBARS_OUTSIDE_OVERLAY, name = "outsideOverlay"),
+ @EnumEntry(value = SCROLLBARS_OUTSIDE_INSET, name = "outsideInset")
+ })
+ @ScrollBarStyle
+ public int getScrollBarStyle() {
+ return mViewFlags & SCROLLBARS_STYLE_MASK;
+ }
+
+ /**
+ * <p>Compute the horizontal range that the horizontal scrollbar
+ * represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollExtent()} and
+ * {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>The default range is the drawing width of this view.</p>
+ *
+ * @return the total horizontal range represented by the horizontal
+ * scrollbar
+ *
+ * @see #computeHorizontalScrollExtent()
+ * @see #computeHorizontalScrollOffset()
+ */
+ protected int computeHorizontalScrollRange() {
+ return getWidth();
+ }
+
+ /**
+ * <p>Compute the horizontal offset of the horizontal scrollbar's thumb
+ * within the horizontal range. This value is used to compute the position
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeHorizontalScrollExtent()}.</p>
+ *
+ * <p>The default offset is the scroll offset of this view.</p>
+ *
+ * @return the horizontal offset of the scrollbar's thumb
+ *
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollExtent()
+ */
+ protected int computeHorizontalScrollOffset() {
+ return mScrollX;
+ }
+
+ /**
+ * <p>Compute the horizontal extent of the horizontal scrollbar's thumb
+ * within the horizontal range. This value is used to compute the length
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>The default extent is the drawing width of this view.</p>
+ *
+ * @return the horizontal extent of the scrollbar's thumb
+ *
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollOffset()
+ */
+ protected int computeHorizontalScrollExtent() {
+ return getWidth();
+ }
+
+ /**
+ * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollExtent()} and
+ * {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * @return the total vertical range represented by the vertical scrollbar
+ *
+ * <p>The default range is the drawing height of this view.</p>
+ *
+ * @see #computeVerticalScrollExtent()
+ * @see #computeVerticalScrollOffset()
+ */
+ protected int computeVerticalScrollRange() {
+ return getHeight();
+ }
+
+ /**
+ * <p>Compute the vertical offset of the vertical scrollbar's thumb
+ * within the horizontal range. This value is used to compute the position
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollRange()} and
+ * {@link #computeVerticalScrollExtent()}.</p>
+ *
+ * <p>The default offset is the scroll offset of this view.</p>
+ *
+ * @return the vertical offset of the scrollbar's thumb
+ *
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollExtent()
+ */
+ protected int computeVerticalScrollOffset() {
+ return mScrollY;
+ }
+
+ /**
+ * <p>Compute the vertical extent of the vertical scrollbar's thumb
+ * within the vertical range. This value is used to compute the length
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollRange()} and
+ * {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>The default extent is the drawing height of this view.</p>
+ *
+ * @return the vertical extent of the scrollbar's thumb
+ *
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollOffset()
+ */
+ protected int computeVerticalScrollExtent() {
+ return getHeight();
+ }
+
+ /**
+ * Check if this view can be scrolled horizontally in a certain direction.
+ *
+ * @param direction Negative to check scrolling left, positive to check scrolling right.
+ * @return true if this view can be scrolled in the specified direction, false otherwise.
+ */
+ public boolean canScrollHorizontally(int direction) {
+ final int offset = computeHorizontalScrollOffset();
+ final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
+ if (range == 0) return false;
+ if (direction < 0) {
+ return offset > 0;
+ } else {
+ return offset < range - 1;
+ }
+ }
+
+ /**
+ * Check if this view can be scrolled vertically in a certain direction.
+ *
+ * @param direction Negative to check scrolling up, positive to check scrolling down.
+ * @return true if this view can be scrolled in the specified direction, false otherwise.
+ */
+ public boolean canScrollVertically(int direction) {
+ final int offset = computeVerticalScrollOffset();
+ final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
+ if (range == 0) return false;
+ if (direction < 0) {
+ return offset > 0;
+ } else {
+ return offset < range - 1;
+ }
+ }
+
+ void getScrollIndicatorBounds(@NonNull Rect out) {
+ out.left = mScrollX;
+ out.right = mScrollX + mRight - mLeft;
+ out.top = mScrollY;
+ out.bottom = mScrollY + mBottom - mTop;
+ }
+
+ private void onDrawScrollIndicators(Canvas c) {
+ if ((mPrivateFlags3 & SCROLL_INDICATORS_PFLAG3_MASK) == 0) {
+ // No scroll indicators enabled.
+ return;
+ }
+
+ final Drawable dr = mScrollIndicatorDrawable;
+ if (dr == null) {
+ // Scroll indicators aren't supported here.
+ return;
+ }
+
+ if (mAttachInfo == null) {
+ // View is not attached.
+ return;
+ }
+
+ final int h = dr.getIntrinsicHeight();
+ final int w = dr.getIntrinsicWidth();
+ final Rect rect = mAttachInfo.mTmpInvalRect;
+ getScrollIndicatorBounds(rect);
+
+ if ((mPrivateFlags3 & PFLAG3_SCROLL_INDICATOR_TOP) != 0) {
+ final boolean canScrollUp = canScrollVertically(-1);
+ if (canScrollUp) {
+ dr.setBounds(rect.left, rect.top, rect.right, rect.top + h);
+ dr.draw(c);
+ }
+ }
+
+ if ((mPrivateFlags3 & PFLAG3_SCROLL_INDICATOR_BOTTOM) != 0) {
+ final boolean canScrollDown = canScrollVertically(1);
+ if (canScrollDown) {
+ dr.setBounds(rect.left, rect.bottom - h, rect.right, rect.bottom);
+ dr.draw(c);
+ }
+ }
+
+ final int leftRtl;
+ final int rightRtl;
+ if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
+ leftRtl = PFLAG3_SCROLL_INDICATOR_END;
+ rightRtl = PFLAG3_SCROLL_INDICATOR_START;
+ } else {
+ leftRtl = PFLAG3_SCROLL_INDICATOR_START;
+ rightRtl = PFLAG3_SCROLL_INDICATOR_END;
+ }
+
+ final int leftMask = PFLAG3_SCROLL_INDICATOR_LEFT | leftRtl;
+ if ((mPrivateFlags3 & leftMask) != 0) {
+ final boolean canScrollLeft = canScrollHorizontally(-1);
+ if (canScrollLeft) {
+ dr.setBounds(rect.left, rect.top, rect.left + w, rect.bottom);
+ dr.draw(c);
+ }
+ }
+
+ final int rightMask = PFLAG3_SCROLL_INDICATOR_RIGHT | rightRtl;
+ if ((mPrivateFlags3 & rightMask) != 0) {
+ final boolean canScrollRight = canScrollHorizontally(1);
+ if (canScrollRight) {
+ dr.setBounds(rect.right - w, rect.top, rect.right, rect.bottom);
+ dr.draw(c);
+ }
+ }
+ }
+
+ private void getHorizontalScrollBarBounds(@Nullable Rect drawBounds,
+ @Nullable Rect touchBounds) {
+ final Rect bounds = drawBounds != null ? drawBounds : touchBounds;
+ if (bounds == null) {
+ return;
+ }
+ final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
+ final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
+ && !isVerticalScrollBarHidden();
+ final int size = getHorizontalScrollbarHeight();
+ final int verticalScrollBarGap = drawVerticalScrollBar ?
+ getVerticalScrollbarWidth() : 0;
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
+ bounds.top = mScrollY + height - size - (mUserPaddingBottom & inside);
+ bounds.left = mScrollX + (mPaddingLeft & inside);
+ bounds.right = mScrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;
+ bounds.bottom = bounds.top + size;
+
+ if (touchBounds == null) {
+ return;
+ }
+ if (touchBounds != bounds) {
+ touchBounds.set(bounds);
+ }
+ final int minTouchTarget = mScrollCache.scrollBarMinTouchTarget;
+ if (touchBounds.height() < minTouchTarget) {
+ final int adjust = (minTouchTarget - touchBounds.height()) / 2;
+ touchBounds.bottom = Math.min(touchBounds.bottom + adjust, mScrollY + height);
+ touchBounds.top = touchBounds.bottom - minTouchTarget;
+ }
+ if (touchBounds.width() < minTouchTarget) {
+ final int adjust = (minTouchTarget - touchBounds.width()) / 2;
+ touchBounds.left -= adjust;
+ touchBounds.right = touchBounds.left + minTouchTarget;
+ }
+ }
+
+ private void getVerticalScrollBarBounds(@Nullable Rect bounds, @Nullable Rect touchBounds) {
+ if (mRoundScrollbarRenderer == null) {
+ getStraightVerticalScrollBarBounds(bounds, touchBounds);
+ } else {
+ getRoundVerticalScrollBarBounds(bounds != null ? bounds : touchBounds);
+ }
+ }
+
+ private void getRoundVerticalScrollBarBounds(Rect bounds) {
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
+ // Do not take padding into account as we always want the scrollbars
+ // to hug the screen for round wearable devices.
+ bounds.left = mScrollX;
+ bounds.top = mScrollY;
+ bounds.right = bounds.left + width;
+ bounds.bottom = mScrollY + height;
+ }
+
+ private void getStraightVerticalScrollBarBounds(@Nullable Rect drawBounds,
+ @Nullable Rect touchBounds) {
+ final Rect bounds = drawBounds != null ? drawBounds : touchBounds;
+ if (bounds == null) {
+ return;
+ }
+ final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
+ final int size = getVerticalScrollbarWidth();
+ int verticalScrollbarPosition = mVerticalScrollbarPosition;
+ if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {
+ verticalScrollbarPosition = isLayoutRtl() ?
+ SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;
+ }
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
+ switch (verticalScrollbarPosition) {
+ default:
+ case SCROLLBAR_POSITION_RIGHT:
+ bounds.left = mScrollX + width - size - (mUserPaddingRight & inside);
+ break;
+ case SCROLLBAR_POSITION_LEFT:
+ bounds.left = mScrollX + (mUserPaddingLeft & inside);
+ break;
+ }
+ bounds.top = mScrollY + (mPaddingTop & inside);
+ bounds.right = bounds.left + size;
+ bounds.bottom = mScrollY + height - (mUserPaddingBottom & inside);
+
+ if (touchBounds == null) {
+ return;
+ }
+ if (touchBounds != bounds) {
+ touchBounds.set(bounds);
+ }
+ final int minTouchTarget = mScrollCache.scrollBarMinTouchTarget;
+ if (touchBounds.width() < minTouchTarget) {
+ final int adjust = (minTouchTarget - touchBounds.width()) / 2;
+ if (verticalScrollbarPosition == SCROLLBAR_POSITION_RIGHT) {
+ touchBounds.right = Math.min(touchBounds.right + adjust, mScrollX + width);
+ touchBounds.left = touchBounds.right - minTouchTarget;
+ } else {
+ touchBounds.left = Math.max(touchBounds.left + adjust, mScrollX);
+ touchBounds.right = touchBounds.left + minTouchTarget;
+ }
+ }
+ if (touchBounds.height() < minTouchTarget) {
+ final int adjust = (minTouchTarget - touchBounds.height()) / 2;
+ touchBounds.top -= adjust;
+ touchBounds.bottom = touchBounds.top + minTouchTarget;
+ }
+ }
+
+ /**
+ * <p>Request the drawing of the horizontal and the vertical scrollbar. The
+ * scrollbars are painted only if they have been awakened first.</p>
+ *
+ * @param canvas the canvas on which to draw the scrollbars
+ *
+ * @see #awakenScrollBars(int)
+ */
+ protected final void onDrawScrollBars(Canvas canvas) {
+ // scrollbars are drawn only when the animation is running
+ final ScrollabilityCache cache = mScrollCache;
+
+ if (cache != null) {
+
+ int state = cache.state;
+
+ if (state == ScrollabilityCache.OFF) {
+ return;
+ }
+
+ boolean invalidate = false;
+
+ if (state == ScrollabilityCache.FADING) {
+ // We're fading -- get our fade interpolation
+ if (cache.interpolatorValues == null) {
+ cache.interpolatorValues = new float[1];
+ }
+
+ float[] values = cache.interpolatorValues;
+
+ // Stops the animation if we're done
+ if (cache.scrollBarInterpolator.timeToValues(values) ==
+ Interpolator.Result.FREEZE_END) {
+ cache.state = ScrollabilityCache.OFF;
+ } else {
+ cache.scrollBar.mutate().setAlpha(Math.round(values[0]));
+ }
+
+ // This will make the scroll bars inval themselves after
+ // drawing. We only want this when we're fading so that
+ // we prevent excessive redraws
+ invalidate = true;
+ } else {
+ // We're just on -- but we may have been fading before so
+ // reset alpha
+ cache.scrollBar.mutate().setAlpha(255);
+ }
+
+ final boolean drawHorizontalScrollBar = isHorizontalScrollBarEnabled();
+ final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
+ && !isVerticalScrollBarHidden();
+
+ // Fork out the scroll bar drawing for round wearable devices.
+ if (mRoundScrollbarRenderer != null) {
+ if (drawVerticalScrollBar) {
+ final Rect bounds = cache.mScrollBarBounds;
+ getVerticalScrollBarBounds(bounds, null);
+ mRoundScrollbarRenderer.drawRoundScrollbars(
+ canvas, (float) cache.scrollBar.getAlpha() / 255f, bounds);
+ if (invalidate) {
+ invalidate();
+ }
+ }
+ // Do not draw horizontal scroll bars for round wearable devices.
+ } else if (drawVerticalScrollBar || drawHorizontalScrollBar) {
+ final ScrollBarDrawable scrollBar = cache.scrollBar;
+
+ if (drawHorizontalScrollBar) {
+ scrollBar.setParameters(computeHorizontalScrollRange(),
+ computeHorizontalScrollOffset(),
+ computeHorizontalScrollExtent(), false);
+ final Rect bounds = cache.mScrollBarBounds;
+ getHorizontalScrollBarBounds(bounds, null);
+ onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
+ bounds.right, bounds.bottom);
+ if (invalidate) {
+ invalidate(bounds);
+ }
+ }
+
+ if (drawVerticalScrollBar) {
+ scrollBar.setParameters(computeVerticalScrollRange(),
+ computeVerticalScrollOffset(),
+ computeVerticalScrollExtent(), true);
+ final Rect bounds = cache.mScrollBarBounds;
+ getVerticalScrollBarBounds(bounds, null);
+ onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
+ bounds.right, bounds.bottom);
+ if (invalidate) {
+ invalidate(bounds);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Override this if the vertical scrollbar needs to be hidden in a subclass, like when
+ * FastScroller is visible.
+ * @return whether to temporarily hide the vertical scrollbar
+ * @hide
+ */
+ protected boolean isVerticalScrollBarHidden() {
+ return false;
+ }
+
+ /**
+ * <p>Draw the horizontal scrollbar if
+ * {@link #isHorizontalScrollBarEnabled()} returns true.</p>
+ *
+ * @param canvas the canvas on which to draw the scrollbar
+ * @param scrollBar the scrollbar's drawable
+ *
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollExtent()
+ * @see #computeHorizontalScrollOffset()
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar,
+ int l, int t, int r, int b) {
+ scrollBar.setBounds(l, t, r, b);
+ scrollBar.draw(canvas);
+ }
+
+ /**
+ * <p>Draw the vertical scrollbar if {@link #isVerticalScrollBarEnabled()}
+ * returns true.</p>
+ *
+ * @param canvas the canvas on which to draw the scrollbar
+ * @param scrollBar the scrollbar's drawable
+ *
+ * @see #isVerticalScrollBarEnabled()
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollExtent()
+ * @see #computeVerticalScrollOffset()
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
+ int l, int t, int r, int b) {
+ scrollBar.setBounds(l, t, r, b);
+ scrollBar.draw(canvas);
+ }
+
+ /**
+ * Implement this to do your drawing.
+ *
+ * @param canvas the canvas on which the background will be drawn
+ */
+ protected void onDraw(Canvas canvas) {
+ }
+
+ /*
+ * Caller is responsible for calling requestLayout if necessary.
+ * (This allows addViewInLayout to not request a new layout.)
+ */
+ @UnsupportedAppUsage
+ void assignParent(ViewParent parent) {
+ if (mParent == null) {
+ mParent = parent;
+ } else if (parent == null) {
+ mParent = null;
+ } else {
+ throw new RuntimeException("view " + this + " being added, but"
+ + " it already has a parent");
+ }
+ }
+
+ /**
+ * This is called when the view is attached to a window. At this point it
+ * has a Surface and will start drawing. Note that this function is
+ * guaranteed to be called before {@link #onDraw(android.graphics.Canvas)},
+ * however it may be called any time before the first onDraw -- including
+ * before or after {@link #onMeasure(int, int)}.
+ *
+ * @see #onDetachedFromWindow()
+ */
+ @CallSuper
+ protected void onAttachedToWindow() {
+ if ((mPrivateFlags & PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
+ mParent.requestTransparentRegion(this);
+ }
+
+ mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
+
+ jumpDrawablesToCurrentState();
+
+ AccessibilityNodeIdManager.getInstance().registerViewWithId(this, getAccessibilityViewId());
+ resetSubtreeAccessibilityStateChanged();
+
+ // rebuild, since Outline not maintained while View is detached
+ rebuildOutline();
+
+ if (isFocused()) {
+ notifyFocusChangeToImeFocusController(true /* hasFocus */);
+ }
+ }
+
+ /**
+ * Resolve all RTL related properties.
+ *
+ * @return true if resolution of RTL properties has been done
+ *
+ * @hide
+ */
+ public boolean resolveRtlPropertiesIfNeeded() {
+ if (!needRtlPropertiesResolution()) return false;
+
+ // Order is important here: LayoutDirection MUST be resolved first
+ if (!isLayoutDirectionResolved()) {
+ resolveLayoutDirection();
+ resolveLayoutParams();
+ }
+ // ... then we can resolve the others properties depending on the resolved LayoutDirection.
+ if (!isTextDirectionResolved()) {
+ resolveTextDirection();
+ }
+ if (!isTextAlignmentResolved()) {
+ resolveTextAlignment();
+ }
+ // Should resolve Drawables before Padding because we need the layout direction of the
+ // Drawable to correctly resolve Padding.
+ if (!areDrawablesResolved()) {
+ resolveDrawables();
+ }
+ if (!isPaddingResolved()) {
+ resolvePadding();
+ }
+ onRtlPropertiesChanged(getLayoutDirection());
+ return true;
+ }
+
+ /**
+ * Reset resolution of all RTL related properties.
+ *
+ * @hide
+ */
+ @TestApi
+ public void resetRtlProperties() {
+ resetResolvedLayoutDirection();
+ resetResolvedTextDirection();
+ resetResolvedTextAlignment();
+ resetResolvedPadding();
+ resetResolvedDrawables();
+ }
+
+ /**
+ * @see #onScreenStateChanged(int)
+ */
+ void dispatchScreenStateChanged(int screenState) {
+ onScreenStateChanged(screenState);
+ }
+
+ /**
+ * This method is called whenever the state of the screen this view is
+ * attached to changes. A state change will usually occurs when the screen
+ * turns on or off (whether it happens automatically or the user does it
+ * manually.)
+ *
+ * @param screenState The new state of the screen. Can be either
+ * {@link #SCREEN_STATE_ON} or {@link #SCREEN_STATE_OFF}
+ */
+ public void onScreenStateChanged(int screenState) {
+ }
+
+ /**
+ * @see #onMovedToDisplay(int, Configuration)
+ */
+ void dispatchMovedToDisplay(Display display, Configuration config) {
+ mAttachInfo.mDisplay = display;
+ mAttachInfo.mDisplayState = display.getState();
+ onMovedToDisplay(display.getDisplayId(), config);
+ }
+
+ /**
+ * Called by the system when the hosting activity is moved from one display to another without
+ * recreation. This means that the activity is declared to handle all changes to configuration
+ * that happened when it was switched to another display, so it wasn't destroyed and created
+ * again.
+ *
+ * <p>This call will be followed by {@link #onConfigurationChanged(Configuration)} if the
+ * applied configuration actually changed. It is up to app developer to choose whether to handle
+ * the change in this method or in the following {@link #onConfigurationChanged(Configuration)}
+ * call.
+ *
+ * <p>Use this callback to track changes to the displays if some functionality relies on an
+ * association with some display properties.
+ *
+ * @param displayId The id of the display to which the view was moved.
+ * @param config Configuration of the resources on new display after move.
+ *
+ * @see #onConfigurationChanged(Configuration)
+ * @hide
+ */
+ public void onMovedToDisplay(int displayId, Configuration config) {
+ }
+
+ /**
+ * Return true if the application tag in the AndroidManifest has set "supportRtl" to true
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private boolean hasRtlSupport() {
+ return mContext.getApplicationInfo().hasRtlSupport();
+ }
+
+ /**
+ * Return true if we are in RTL compatibility mode (either before Jelly Bean MR1 or
+ * RTL not supported)
+ */
+ private boolean isRtlCompatibilityMode() {
+ final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+ return targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR1 || !hasRtlSupport();
+ }
+
+ /**
+ * @return true if RTL properties need resolution.
+ *
+ */
+ private boolean needRtlPropertiesResolution() {
+ return (mPrivateFlags2 & ALL_RTL_PROPERTIES_RESOLVED) != ALL_RTL_PROPERTIES_RESOLVED;
+ }
+
+ /**
+ * Called when any RTL property (layout direction or text direction or text alignment) has
+ * been changed.
+ *
+ * Subclasses need to override this method to take care of cached information that depends on the
+ * resolved layout direction, or to inform child views that inherit their layout direction.
+ *
+ * The default implementation does nothing.
+ *
+ * @param layoutDirection the direction of the layout
+ *
+ * @see #LAYOUT_DIRECTION_LTR
+ * @see #LAYOUT_DIRECTION_RTL
+ */
+ public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+ }
+
+ /**
+ * Resolve and cache the layout direction. LTR is set initially. This is implicitly supposing
+ * that the parent directionality can and will be resolved before its children.
+ *
+ * @return true if resolution has been done, false otherwise.
+ *
+ * @hide
+ */
+ public boolean resolveLayoutDirection() {
+ // Clear any previous layout direction resolution
+ mPrivateFlags2 &= ~PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK;
+
+ if (hasRtlSupport()) {
+ // Set resolved depending on layout direction
+ switch ((mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_MASK) >>
+ PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT) {
+ case LAYOUT_DIRECTION_INHERIT:
+ // We cannot resolve yet. LTR is by default and let the resolution happen again
+ // later to get the correct resolved value
+ if (!canResolveLayoutDirection()) return false;
+
+ // Parent has not yet resolved, LTR is still the default
+ try {
+ if (!mParent.isLayoutDirectionResolved()) return false;
+
+ if (mParent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
+ mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL;
+ }
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ break;
+ case LAYOUT_DIRECTION_RTL:
+ mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL;
+ break;
+ case LAYOUT_DIRECTION_LOCALE:
+ if((LAYOUT_DIRECTION_RTL ==
+ TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()))) {
+ mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL;
+ }
+ break;
+ default:
+ // Nothing to do, LTR by default
+ }
+ }
+
+ // Set to resolved
+ mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED;
+ return true;
+ }
+
+ /**
+ * Check if layout direction resolution can be done.
+ *
+ * @return true if layout direction resolution can be done otherwise return false.
+ */
+ public boolean canResolveLayoutDirection() {
+ switch (getRawLayoutDirection()) {
+ case LAYOUT_DIRECTION_INHERIT:
+ if (mParent != null) {
+ try {
+ return mParent.canResolveLayoutDirection();
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Reset the resolved layout direction. Layout direction will be resolved during a call to
+ * {@link #onMeasure(int, int)}.
+ *
+ * @hide
+ */
+ @TestApi
+ public void resetResolvedLayoutDirection() {
+ // Reset the current resolved bits
+ mPrivateFlags2 &= ~PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK;
+ }
+
+ /**
+ * @return true if the layout direction is inherited.
+ *
+ * @hide
+ */
+ public boolean isLayoutDirectionInherited() {
+ return (getRawLayoutDirection() == LAYOUT_DIRECTION_INHERIT);
+ }
+
+ /**
+ * @return true if layout direction has been resolved.
+ */
+ public boolean isLayoutDirectionResolved() {
+ return (mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_RESOLVED) == PFLAG2_LAYOUT_DIRECTION_RESOLVED;
+ }
+
+ /**
+ * Return if padding has been resolved
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ boolean isPaddingResolved() {
+ return (mPrivateFlags2 & PFLAG2_PADDING_RESOLVED) == PFLAG2_PADDING_RESOLVED;
+ }
+
+ /**
+ * Resolves padding depending on layout direction, if applicable, and
+ * recomputes internal padding values to adjust for scroll bars.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void resolvePadding() {
+ final int resolvedLayoutDirection = getLayoutDirection();
+
+ if (!isRtlCompatibilityMode()) {
+ // Post Jelly Bean MR1 case: we need to take the resolved layout direction into account.
+ // If start / end padding are defined, they will be resolved (hence overriding) to
+ // left / right or right / left depending on the resolved layout direction.
+ // If start / end padding are not defined, use the left / right ones.
+ if (mBackground != null && (!mLeftPaddingDefined || !mRightPaddingDefined)) {
+ Rect padding = sThreadLocal.get();
+ if (padding == null) {
+ padding = new Rect();
+ sThreadLocal.set(padding);
+ }
+ mBackground.getPadding(padding);
+ if (!mLeftPaddingDefined) {
+ mUserPaddingLeftInitial = padding.left;
+ }
+ if (!mRightPaddingDefined) {
+ mUserPaddingRightInitial = padding.right;
+ }
+ }
+ switch (resolvedLayoutDirection) {
+ case LAYOUT_DIRECTION_RTL:
+ if (mUserPaddingStart != UNDEFINED_PADDING) {
+ mUserPaddingRight = mUserPaddingStart;
+ } else {
+ mUserPaddingRight = mUserPaddingRightInitial;
+ }
+ if (mUserPaddingEnd != UNDEFINED_PADDING) {
+ mUserPaddingLeft = mUserPaddingEnd;
+ } else {
+ mUserPaddingLeft = mUserPaddingLeftInitial;
+ }
+ break;
+ case LAYOUT_DIRECTION_LTR:
+ default:
+ if (mUserPaddingStart != UNDEFINED_PADDING) {
+ mUserPaddingLeft = mUserPaddingStart;
+ } else {
+ mUserPaddingLeft = mUserPaddingLeftInitial;
+ }
+ if (mUserPaddingEnd != UNDEFINED_PADDING) {
+ mUserPaddingRight = mUserPaddingEnd;
+ } else {
+ mUserPaddingRight = mUserPaddingRightInitial;
+ }
+ }
+
+ mUserPaddingBottom = (mUserPaddingBottom >= 0) ? mUserPaddingBottom : mPaddingBottom;
+ }
+
+ internalSetPadding(mUserPaddingLeft, mPaddingTop, mUserPaddingRight, mUserPaddingBottom);
+ onRtlPropertiesChanged(resolvedLayoutDirection);
+
+ mPrivateFlags2 |= PFLAG2_PADDING_RESOLVED;
+ }
+
+ /**
+ * Reset the resolved layout direction.
+ *
+ * @hide
+ */
+ @TestApi
+ public void resetResolvedPadding() {
+ resetResolvedPaddingInternal();
+ }
+
+ /**
+ * Used when we only want to reset *this* view's padding and not trigger overrides
+ * in ViewGroup that reset children too.
+ */
+ void resetResolvedPaddingInternal() {
+ mPrivateFlags2 &= ~PFLAG2_PADDING_RESOLVED;
+ }
+
+ /**
+ * This is called when the view is detached from a window. At this point it
+ * no longer has a surface for drawing.
+ *
+ * @see #onAttachedToWindow()
+ */
+ @CallSuper
+ protected void onDetachedFromWindow() {
+ }
+
+ /**
+ * This is a framework-internal mirror of onDetachedFromWindow() that's called
+ * after onDetachedFromWindow().
+ *
+ * If you override this you *MUST* call super.onDetachedFromWindowInternal()!
+ * The super method should be called at the end of the overridden method to ensure
+ * subclasses are destroyed first
+ *
+ * @hide
+ */
+ @CallSuper
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void onDetachedFromWindowInternal() {
+ mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
+ mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
+ mPrivateFlags3 &= ~PFLAG3_TEMPORARY_DETACH;
+
+ removeUnsetPressCallback();
+ removeLongPressCallback();
+ removePerformClickCallback();
+ clearAccessibilityThrottles();
+ stopNestedScroll();
+
+ // Anything that started animating right before detach should already
+ // be in its final state when re-attached.
+ jumpDrawablesToCurrentState();
+
+ destroyDrawingCache();
+
+ cleanupDraw();
+ mCurrentAnimation = null;
+
+ if ((mViewFlags & TOOLTIP) == TOOLTIP) {
+ hideTooltip();
+ }
+
+ AccessibilityNodeIdManager.getInstance().unregisterViewWithId(getAccessibilityViewId());
+ }
+
+ private void cleanupDraw() {
+ resetDisplayList();
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewRootImpl.cancelInvalidate(this);
+ }
+ }
+
+ void invalidateInheritedLayoutMode(int layoutModeOfRoot) {
+ }
+
+ /**
+ * @return The number of times this view has been attached to a window
+ */
+ protected int getWindowAttachCount() {
+ return mWindowAttachCount;
+ }
+
+ /**
+ * Retrieve a unique token identifying the window this view is attached to.
+ * @return Return the window's token for use in
+ * {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}.
+ */
+ public IBinder getWindowToken() {
+ return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
+ }
+
+ /**
+ * Retrieve the {@link WindowId} for the window this view is
+ * currently attached to.
+ */
+ public WindowId getWindowId() {
+ AttachInfo ai = mAttachInfo;
+ if (ai == null) {
+ return null;
+ }
+ if (ai.mWindowId == null) {
+ try {
+ ai.mIWindowId = ai.mSession.getWindowId(ai.mWindowToken);
+ if (ai.mIWindowId != null) {
+ ai.mWindowId = new WindowId(ai.mIWindowId);
+ }
+ } catch (RemoteException e) {
+ }
+ }
+ return ai.mWindowId;
+ }
+
+ /**
+ * Retrieve a unique token identifying the top-level "real" window of
+ * the window that this view is attached to. That is, this is like
+ * {@link #getWindowToken}, except if the window this view in is a panel
+ * window (attached to another containing window), then the token of
+ * the containing window is returned instead.
+ *
+ * @return Returns the associated window token, either
+ * {@link #getWindowToken()} or the containing window's token.
+ */
+ public IBinder getApplicationWindowToken() {
+ AttachInfo ai = mAttachInfo;
+ if (ai != null) {
+ IBinder appWindowToken = ai.mPanelParentWindowToken;
+ if (appWindowToken == null) {
+ appWindowToken = ai.mWindowToken;
+ }
+ return appWindowToken;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the logical display to which the view's window has been attached.
+ *
+ * @return The logical display, or null if the view is not currently attached to a window.
+ */
+ public Display getDisplay() {
+ return mAttachInfo != null ? mAttachInfo.mDisplay : null;
+ }
+
+ /**
+ * Retrieve private session object this view hierarchy is using to
+ * communicate with the window manager.
+ * @return the session object to communicate with the window manager
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ /*package*/ IWindowSession getWindowSession() {
+ return mAttachInfo != null ? mAttachInfo.mSession : null;
+ }
+
+ /**
+ * Return the window this view is currently attached to.
+ * @hide
+ */
+ protected IWindow getWindow() {
+ return mAttachInfo != null ? mAttachInfo.mWindow : null;
+ }
+
+ /**
+ * Return the visibility value of the least visible component passed.
+ */
+ int combineVisibility(int vis1, int vis2) {
+ // This works because VISIBLE < INVISIBLE < GONE.
+ return Math.max(vis1, vis2);
+ }
+
+ /**
+ * @param info the {@link android.view.View.AttachInfo} to associated with
+ * this view
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ void dispatchAttachedToWindow(AttachInfo info, int visibility) {
+ mAttachInfo = info;
+ if (mOverlay != null) {
+ mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
+ }
+ mWindowAttachCount++;
+ // We will need to evaluate the drawable state at least once.
+ mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
+ if (mFloatingTreeObserver != null) {
+ info.mTreeObserver.merge(mFloatingTreeObserver);
+ mFloatingTreeObserver = null;
+ }
+
+ registerPendingFrameMetricsObservers();
+
+ if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {
+ mAttachInfo.mScrollContainers.add(this);
+ mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
+ }
+ // Transfer all pending runnables.
+ if (mRunQueue != null) {
+ mRunQueue.executeActions(info.mHandler);
+ mRunQueue = null;
+ }
+ performCollectViewAttributes(mAttachInfo, visibility);
+ onAttachedToWindow();
+
+ ListenerInfo li = mListenerInfo;
+ final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
+ li != null ? li.mOnAttachStateChangeListeners : null;
+ if (listeners != null && listeners.size() > 0) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ for (OnAttachStateChangeListener listener : listeners) {
+ listener.onViewAttachedToWindow(this);
+ }
+ }
+
+ int vis = info.mWindowVisibility;
+ if (vis != GONE) {
+ onWindowVisibilityChanged(vis);
+ if (isShown()) {
+ // Calling onVisibilityAggregated directly here since the subtree will also
+ // receive dispatchAttachedToWindow and this same call
+ onVisibilityAggregated(vis == VISIBLE);
+ }
+ }
+
+ // Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
+ // As all views in the subtree will already receive dispatchAttachedToWindow
+ // traversing the subtree again here is not desired.
+ onVisibilityChanged(this, visibility);
+
+ if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {
+ // If nobody has evaluated the drawable state yet, then do it now.
+ refreshDrawableState();
+ }
+ needGlobalAttributesUpdate(false);
+
+ notifyEnterOrExitForAutoFillIfNeeded(true);
+ notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ void dispatchDetachedFromWindow() {
+ AttachInfo info = mAttachInfo;
+ if (info != null) {
+ int vis = info.mWindowVisibility;
+ if (vis != GONE) {
+ onWindowVisibilityChanged(GONE);
+ if (isShown()) {
+ // Invoking onVisibilityAggregated directly here since the subtree
+ // will also receive detached from window
+ onVisibilityAggregated(false);
+ }
+ }
+ }
+
+ onDetachedFromWindow();
+ onDetachedFromWindowInternal();
+
+ if (info != null) {
+ info.mViewRootImpl.getImeFocusController().onViewDetachedFromWindow(this);
+ }
+
+ ListenerInfo li = mListenerInfo;
+ final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
+ li != null ? li.mOnAttachStateChangeListeners : null;
+ if (listeners != null && listeners.size() > 0) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ for (OnAttachStateChangeListener listener : listeners) {
+ listener.onViewDetachedFromWindow(this);
+ }
+ }
+
+ if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
+ mAttachInfo.mScrollContainers.remove(this);
+ mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;
+ }
+
+ mAttachInfo = null;
+ if (mOverlay != null) {
+ mOverlay.getOverlayView().dispatchDetachedFromWindow();
+ }
+
+ notifyEnterOrExitForAutoFillIfNeeded(false);
+ notifyAppearedOrDisappearedForContentCaptureIfNeeded(false);
+ }
+
+ /**
+ * Cancel any deferred high-level input events that were previously posted to the event queue.
+ *
+ * <p>Many views post high-level events such as click handlers to the event queue
+ * to run deferred in order to preserve a desired user experience - clearing visible
+ * pressed states before executing, etc. This method will abort any events of this nature
+ * that are currently in flight.</p>
+ *
+ * <p>Custom views that generate their own high-level deferred input events should override
+ * {@link #onCancelPendingInputEvents()} and remove those pending events from the queue.</p>
+ *
+ * <p>This will also cancel pending input events for any child views.</p>
+ *
+ * <p>Note that this may not be sufficient as a debouncing strategy for clicks in all cases.
+ * This will not impact newer events posted after this call that may occur as a result of
+ * lower-level input events still waiting in the queue. If you are trying to prevent
+ * double-submitted events for the duration of some sort of asynchronous transaction
+ * you should also take other steps to protect against unexpected double inputs e.g. calling
+ * {@link #setEnabled(boolean) setEnabled(false)} and re-enabling the view when
+ * the transaction completes, tracking already submitted transaction IDs, etc.</p>
+ */
+ public final void cancelPendingInputEvents() {
+ dispatchCancelPendingInputEvents();
+ }
+
+ /**
+ * Called by {@link #cancelPendingInputEvents()} to cancel input events in flight.
+ * Overridden by ViewGroup to dispatch. Package scoped to prevent app-side meddling.
+ */
+ void dispatchCancelPendingInputEvents() {
+ mPrivateFlags3 &= ~PFLAG3_CALLED_SUPER;
+ onCancelPendingInputEvents();
+ if ((mPrivateFlags3 & PFLAG3_CALLED_SUPER) != PFLAG3_CALLED_SUPER) {
+ throw new SuperNotCalledException("View " + getClass().getSimpleName() +
+ " did not call through to super.onCancelPendingInputEvents()");
+ }
+ }
+
+ /**
+ * Called as the result of a call to {@link #cancelPendingInputEvents()} on this view or
+ * a parent view.
+ *
+ * <p>This method is responsible for removing any pending high-level input events that were
+ * posted to the event queue to run later. Custom view classes that post their own deferred
+ * high-level events via {@link #post(Runnable)}, {@link #postDelayed(Runnable, long)} or
+ * {@link android.os.Handler} should override this method, call
+ * <code>super.onCancelPendingInputEvents()</code> and remove those callbacks as appropriate.
+ * </p>
+ */
+ public void onCancelPendingInputEvents() {
+ removePerformClickCallback();
+ cancelLongPress();
+ mPrivateFlags3 |= PFLAG3_CALLED_SUPER;
+ }
+
+ /**
+ * Store this view hierarchy's frozen state into the given container.
+ *
+ * @param container The SparseArray in which to save the view's state.
+ *
+ * @see #restoreHierarchyState(android.util.SparseArray)
+ * @see #dispatchSaveInstanceState(android.util.SparseArray)
+ * @see #onSaveInstanceState()
+ */
+ public void saveHierarchyState(SparseArray<Parcelable> container) {
+ dispatchSaveInstanceState(container);
+ }
+
+ /**
+ * Called by {@link #saveHierarchyState(android.util.SparseArray)} to store the state for
+ * this view and its children. May be overridden to modify how freezing happens to a
+ * view's children; for example, some views may want to not store state for their children.
+ *
+ * @param container The SparseArray in which to save the view's state.
+ *
+ * @see #dispatchRestoreInstanceState(android.util.SparseArray)
+ * @see #saveHierarchyState(android.util.SparseArray)
+ * @see #onSaveInstanceState()
+ */
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
+ mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
+ Parcelable state = onSaveInstanceState();
+ if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
+ throw new IllegalStateException(
+ "Derived class did not call super.onSaveInstanceState()");
+ }
+ if (state != null) {
+ // Log.i("View", "Freezing #" + Integer.toHexString(mID)
+ // + ": " + state);
+ container.put(mID, state);
+ }
+ }
+ }
+
+ /**
+ * Hook allowing a view to generate a representation of its internal state
+ * that can later be used to create a new instance with that same state.
+ * This state should only contain information that is not persistent or can
+ * not be reconstructed later. For example, you will never store your
+ * current position on screen because that will be computed again when a
+ * new instance of the view is placed in its view hierarchy.
+ * <p>
+ * Some examples of things you may store here: the current cursor position
+ * in a text view (but usually not the text itself since that is stored in a
+ * content provider or other persistent storage), the currently selected
+ * item in a list view.
+ *
+ * @return Returns a Parcelable object containing the view's current dynamic
+ * state, or null if there is nothing interesting to save.
+ * @see #onRestoreInstanceState(Parcelable)
+ * @see #saveHierarchyState(SparseArray)
+ * @see #dispatchSaveInstanceState(SparseArray)
+ * @see #setSaveEnabled(boolean)
+ */
+ @CallSuper
+ @Nullable protected Parcelable onSaveInstanceState() {
+ mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
+ if (mStartActivityRequestWho != null || isAutofilled()
+ || mAutofillViewId > LAST_APP_AUTOFILL_ID) {
+ BaseSavedState state = new BaseSavedState(AbsSavedState.EMPTY_STATE);
+
+ if (mStartActivityRequestWho != null) {
+ state.mSavedData |= BaseSavedState.START_ACTIVITY_REQUESTED_WHO_SAVED;
+ }
+
+ if (isAutofilled()) {
+ state.mSavedData |= BaseSavedState.IS_AUTOFILLED;
+ }
+
+ if (mAutofillViewId > LAST_APP_AUTOFILL_ID) {
+ state.mSavedData |= BaseSavedState.AUTOFILL_ID;
+ }
+
+ state.mStartActivityRequestWhoSaved = mStartActivityRequestWho;
+ state.mIsAutofilled = isAutofilled();
+ state.mHideHighlight = hideAutofillHighlight();
+ state.mAutofillViewId = mAutofillViewId;
+ return state;
+ }
+ return BaseSavedState.EMPTY_STATE;
+ }
+
+ /**
+ * Restore this view hierarchy's frozen state from the given container.
+ *
+ * @param container The SparseArray which holds previously frozen states.
+ *
+ * @see #saveHierarchyState(android.util.SparseArray)
+ * @see #dispatchRestoreInstanceState(android.util.SparseArray)
+ * @see #onRestoreInstanceState(android.os.Parcelable)
+ */
+ public void restoreHierarchyState(SparseArray<Parcelable> container) {
+ dispatchRestoreInstanceState(container);
+ }
+
+ /**
+ * Called by {@link #restoreHierarchyState(android.util.SparseArray)} to retrieve the
+ * state for this view and its children. May be overridden to modify how restoring
+ * happens to a view's children; for example, some views may want to not store state
+ * for their children.
+ *
+ * @param container The SparseArray which holds previously saved state.
+ *
+ * @see #dispatchSaveInstanceState(android.util.SparseArray)
+ * @see #restoreHierarchyState(android.util.SparseArray)
+ * @see #onRestoreInstanceState(android.os.Parcelable)
+ */
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ if (mID != NO_ID) {
+ Parcelable state = container.get(mID);
+ if (state != null) {
+ // Log.i("View", "Restoreing #" + Integer.toHexString(mID)
+ // + ": " + state);
+ mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
+ onRestoreInstanceState(state);
+ if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
+ throw new IllegalStateException(
+ "Derived class did not call super.onRestoreInstanceState()");
+ }
+ }
+ }
+ }
+
+ /**
+ * Hook allowing a view to re-apply a representation of its internal state that had previously
+ * been generated by {@link #onSaveInstanceState}. This function will never be called with a
+ * null state.
+ *
+ * @param state The frozen state that had previously been returned by
+ * {@link #onSaveInstanceState}.
+ *
+ * @see #onSaveInstanceState()
+ * @see #restoreHierarchyState(android.util.SparseArray)
+ * @see #dispatchRestoreInstanceState(android.util.SparseArray)
+ */
+ @CallSuper
+ protected void onRestoreInstanceState(Parcelable state) {
+ mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
+ if (state != null && !(state instanceof AbsSavedState)) {
+ throw new IllegalArgumentException("Wrong state class, expecting View State but "
+ + "received " + state.getClass().toString() + " instead. This usually happens "
+ + "when two views of different type have the same id in the same hierarchy. "
+ + "This view's id is " + ViewDebug.resolveId(mContext, getId()) + ". Make sure "
+ + "other views do not use the same id.");
+ }
+ if (state != null && state instanceof BaseSavedState) {
+ BaseSavedState baseState = (BaseSavedState) state;
+
+ if ((baseState.mSavedData & BaseSavedState.START_ACTIVITY_REQUESTED_WHO_SAVED) != 0) {
+ mStartActivityRequestWho = baseState.mStartActivityRequestWhoSaved;
+ }
+ if ((baseState.mSavedData & BaseSavedState.IS_AUTOFILLED) != 0) {
+ setAutofilled(baseState.mIsAutofilled, baseState.mHideHighlight);
+ }
+ if ((baseState.mSavedData & BaseSavedState.AUTOFILL_ID) != 0) {
+ // It can happen that views have the same view id and the restoration path will not
+ // be able to distinguish between them. The autofill id needs to be unique though.
+ // Hence prevent the same autofill view id from being restored multiple times.
+ ((BaseSavedState) state).mSavedData &= ~BaseSavedState.AUTOFILL_ID;
+
+ if ((mPrivateFlags3 & PFLAG3_AUTOFILLID_EXPLICITLY_SET) != 0) {
+ // Ignore when view already set it through setAutofillId();
+ if (Log.isLoggable(AUTOFILL_LOG_TAG, Log.DEBUG)) {
+ Log.d(AUTOFILL_LOG_TAG, "onRestoreInstanceState(): not setting autofillId "
+ + "to " + baseState.mAutofillViewId + " because view explicitly set"
+ + " it to " + mAutofillId);
+ }
+ } else {
+ mAutofillViewId = baseState.mAutofillViewId;
+ mAutofillId = null; // will be set on demand by getAutofillId()
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>Return the time at which the drawing of the view hierarchy started.</p>
+ *
+ * @return the drawing start time in milliseconds
+ */
+ public long getDrawingTime() {
+ return mAttachInfo != null ? mAttachInfo.mDrawingTime : 0;
+ }
+
+ /**
+ * <p>Enables or disables the duplication of the parent's state into this view. When
+ * duplication is enabled, this view gets its drawable state from its parent rather
+ * than from its own internal properties.</p>
+ *
+ * <p>Note: in the current implementation, setting this property to true after the
+ * view was added to a ViewGroup might have no effect at all. This property should
+ * always be used from XML or set to true before adding this view to a ViewGroup.</p>
+ *
+ * <p>Note: if this view's parent addStateFromChildren property is enabled and this
+ * property is enabled, an exception will be thrown.</p>
+ *
+ * <p>Note: if the child view uses and updates additional states which are unknown to the
+ * parent, these states should not be affected by this method.</p>
+ *
+ * @param enabled True to enable duplication of the parent's drawable state, false
+ * to disable it.
+ *
+ * @see #getDrawableState()
+ * @see #isDuplicateParentStateEnabled()
+ */
+ public void setDuplicateParentStateEnabled(boolean enabled) {
+ setFlags(enabled ? DUPLICATE_PARENT_STATE : 0, DUPLICATE_PARENT_STATE);
+ }
+
+ /**
+ * <p>Indicates whether this duplicates its drawable state from its parent.</p>
+ *
+ * @return True if this view's drawable state is duplicated from the parent,
+ * false otherwise
+ *
+ * @see #getDrawableState()
+ * @see #setDuplicateParentStateEnabled(boolean)
+ */
+ @InspectableProperty(name = "duplicateParentState")
+ public boolean isDuplicateParentStateEnabled() {
+ return (mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE;
+ }
+
+ /**
+ * <p>Specifies the type of layer backing this view. The layer can be
+ * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or
+ * {@link #LAYER_TYPE_HARDWARE}.</p>
+ *
+ * <p>A layer is associated with an optional {@link android.graphics.Paint}
+ * instance that controls how the layer is composed on screen. The following
+ * properties of the paint are taken into account when composing the layer:</p>
+ * <ul>
+ * <li>{@link android.graphics.Paint#getAlpha() Translucency (alpha)}</li>
+ * <li>{@link android.graphics.Paint#getXfermode() Blending mode}</li>
+ * <li>{@link android.graphics.Paint#getColorFilter() Color filter}</li>
+ * </ul>
+ *
+ * <p>If this view has an alpha value set to < 1.0 by calling
+ * {@link #setAlpha(float)}, the alpha value of the layer's paint is superseded
+ * by this view's alpha value.</p>
+ *
+ * <p>Refer to the documentation of {@link #LAYER_TYPE_NONE},
+ * {@link #LAYER_TYPE_SOFTWARE} and {@link #LAYER_TYPE_HARDWARE}
+ * for more information on when and how to use layers.</p>
+ *
+ * @param layerType The type of layer to use with this view, must be one of
+ * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or
+ * {@link #LAYER_TYPE_HARDWARE}
+ * @param paint The paint used to compose the layer. This argument is optional
+ * and can be null. It is ignored when the layer type is
+ * {@link #LAYER_TYPE_NONE}
+ *
+ * @see #getLayerType()
+ * @see #LAYER_TYPE_NONE
+ * @see #LAYER_TYPE_SOFTWARE
+ * @see #LAYER_TYPE_HARDWARE
+ * @see #setAlpha(float)
+ *
+ * @attr ref android.R.styleable#View_layerType
+ */
+ public void setLayerType(@LayerType int layerType, @Nullable Paint paint) {
+ if (layerType < LAYER_TYPE_NONE || layerType > LAYER_TYPE_HARDWARE) {
+ throw new IllegalArgumentException("Layer type can only be one of: LAYER_TYPE_NONE, "
+ + "LAYER_TYPE_SOFTWARE or LAYER_TYPE_HARDWARE");
+ }
+
+ boolean typeChanged = mRenderNode.setLayerType(layerType);
+
+ if (!typeChanged) {
+ setLayerPaint(paint);
+ return;
+ }
+
+ if (layerType != LAYER_TYPE_SOFTWARE) {
+ // Destroy any previous software drawing cache if present
+ // NOTE: even if previous layer type is HW, we do this to ensure we've cleaned up
+ // drawing cache created in View#draw when drawing to a SW canvas.
+ destroyDrawingCache();
+ }
+
+ mLayerType = layerType;
+ mLayerPaint = mLayerType == LAYER_TYPE_NONE ? null : paint;
+ mRenderNode.setLayerPaint(mLayerPaint);
+
+ // draw() behaves differently if we are on a layer, so we need to
+ // invalidate() here
+ invalidateParentCaches();
+ invalidate(true);
+ }
+
+ /**
+ * Configure the {@link android.graphics.RenderEffect} to apply to this View.
+ * This will apply a visual effect to the results of the View before it is drawn. For example if
+ * {@link RenderEffect#createBlurEffect(float, float, RenderEffect, Shader.TileMode)}
+ * is provided, the contents will be drawn in a separate layer, then this layer will be blurred
+ * when this View is drawn.
+ * @param renderEffect to be applied to the View. Passing null clears the previously configured
+ * {@link RenderEffect}
+ */
+ public void setRenderEffect(@Nullable RenderEffect renderEffect) {
+ if (mRenderNode.setRenderEffect(renderEffect)) {
+ invalidateViewProperty(true, true);
+ }
+ }
+
+ /**
+ * Updates the {@link Paint} object used with the current layer (used only if the current
+ * layer type is not set to {@link #LAYER_TYPE_NONE}). Changed properties of the Paint
+ * provided to {@link #setLayerType(int, android.graphics.Paint)} will be used the next time
+ * the View is redrawn, but {@link #setLayerPaint(android.graphics.Paint)} must be called to
+ * ensure that the view gets redrawn immediately.
+ *
+ * <p>A layer is associated with an optional {@link android.graphics.Paint}
+ * instance that controls how the layer is composed on screen. The following
+ * properties of the paint are taken into account when composing the layer:</p>
+ * <ul>
+ * <li>{@link android.graphics.Paint#getAlpha() Translucency (alpha)}</li>
+ * <li>{@link android.graphics.Paint#getXfermode() Blending mode}</li>
+ * <li>{@link android.graphics.Paint#getColorFilter() Color filter}</li>
+ * </ul>
+ *
+ * <p>If this view has an alpha value set to < 1.0 by calling {@link #setAlpha(float)}, the
+ * alpha value of the layer's paint is superseded by this view's alpha value.</p>
+ *
+ * @param paint The paint used to compose the layer. This argument is optional
+ * and can be null. It is ignored when the layer type is
+ * {@link #LAYER_TYPE_NONE}
+ *
+ * @see #setLayerType(int, android.graphics.Paint)
+ */
+ public void setLayerPaint(@Nullable Paint paint) {
+ int layerType = getLayerType();
+ if (layerType != LAYER_TYPE_NONE) {
+ mLayerPaint = paint;
+ if (layerType == LAYER_TYPE_HARDWARE) {
+ if (mRenderNode.setLayerPaint(paint)) {
+ invalidateViewProperty(false, false);
+ }
+ } else {
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Indicates what type of layer is currently associated with this view. By default
+ * a view does not have a layer, and the layer type is {@link #LAYER_TYPE_NONE}.
+ * Refer to the documentation of {@link #setLayerType(int, android.graphics.Paint)}
+ * for more information on the different types of layers.
+ *
+ * @return {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or
+ * {@link #LAYER_TYPE_HARDWARE}
+ *
+ * @see #setLayerType(int, android.graphics.Paint)
+ * @see #buildLayer()
+ * @see #LAYER_TYPE_NONE
+ * @see #LAYER_TYPE_SOFTWARE
+ * @see #LAYER_TYPE_HARDWARE
+ */
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = LAYER_TYPE_NONE, name = "none"),
+ @EnumEntry(value = LAYER_TYPE_SOFTWARE, name = "software"),
+ @EnumEntry(value = LAYER_TYPE_HARDWARE, name = "hardware")
+ })
+ @LayerType
+ public int getLayerType() {
+ return mLayerType;
+ }
+
+ /**
+ * Forces this view's layer to be created and this view to be rendered
+ * into its layer. If this view's layer type is set to {@link #LAYER_TYPE_NONE},
+ * invoking this method will have no effect.
+ *
+ * This method can for instance be used to render a view into its layer before
+ * starting an animation. If this view is complex, rendering into the layer
+ * before starting the animation will avoid skipping frames.
+ *
+ * @throws IllegalStateException If this view is not attached to a window
+ *
+ * @see #setLayerType(int, android.graphics.Paint)
+ */
+ public void buildLayer() {
+ if (mLayerType == LAYER_TYPE_NONE) return;
+
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo == null) {
+ throw new IllegalStateException("This view must be attached to a window first");
+ }
+
+ if (getWidth() == 0 || getHeight() == 0) {
+ return;
+ }
+
+ switch (mLayerType) {
+ case LAYER_TYPE_HARDWARE:
+ updateDisplayListIfDirty();
+ if (attachInfo.mThreadedRenderer != null && mRenderNode.hasDisplayList()) {
+ attachInfo.mThreadedRenderer.buildLayer(mRenderNode);
+ }
+ break;
+ case LAYER_TYPE_SOFTWARE:
+ buildDrawingCache(true);
+ break;
+ }
+ }
+
+ /**
+ * Destroys all hardware rendering resources. This method is invoked
+ * when the system needs to reclaim resources. Upon execution of this
+ * method, you should free any OpenGL resources created by the view.
+ *
+ * Note: you <strong>must</strong> call
+ * <code>super.destroyHardwareResources()</code> when overriding
+ * this method.
+ *
+ * @hide
+ */
+ @CallSuper
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void destroyHardwareResources() {
+ if (mOverlay != null) {
+ mOverlay.getOverlayView().destroyHardwareResources();
+ }
+ if (mGhostView != null) {
+ mGhostView.destroyHardwareResources();
+ }
+ }
+
+ /**
+ * <p>Enables or disables the drawing cache. When the drawing cache is enabled, the next call
+ * to {@link #getDrawingCache()} or {@link #buildDrawingCache()} will draw the view in a
+ * bitmap. Calling {@link #draw(android.graphics.Canvas)} will not draw from the cache when
+ * the cache is enabled. To benefit from the cache, you must request the drawing cache by
+ * calling {@link #getDrawingCache()} and draw it on screen if the returned bitmap is not
+ * null.</p>
+ *
+ * <p>Enabling the drawing cache is similar to
+ * {@link #setLayerType(int, android.graphics.Paint) setting a layer} when hardware
+ * acceleration is turned off. When hardware acceleration is turned on, enabling the
+ * drawing cache has no effect on rendering because the system uses a different mechanism
+ * for acceleration which ignores the flag. If you want to use a Bitmap for the view, even
+ * when hardware acceleration is enabled, see {@link #setLayerType(int, android.graphics.Paint)}
+ * for information on how to enable software and hardware layers.</p>
+ *
+ * <p>This API can be used to manually generate
+ * a bitmap copy of this view, by setting the flag to <code>true</code> and calling
+ * {@link #getDrawingCache()}.</p>
+ *
+ * @param enabled true to enable the drawing cache, false otherwise
+ *
+ * @see #isDrawingCacheEnabled()
+ * @see #getDrawingCache()
+ * @see #buildDrawingCache()
+ * @see #setLayerType(int, android.graphics.Paint)
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void setDrawingCacheEnabled(boolean enabled) {
+ mCachingFailed = false;
+ setFlags(enabled ? DRAWING_CACHE_ENABLED : 0, DRAWING_CACHE_ENABLED);
+ }
+
+ /**
+ * <p>Indicates whether the drawing cache is enabled for this view.</p>
+ *
+ * @return true if the drawing cache is enabled
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #getDrawingCache()
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public boolean isDrawingCacheEnabled() {
+ return (mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED;
+ }
+
+ /**
+ * Debugging utility which recursively outputs the dirty state of a view and its
+ * descendants.
+ *
+ * @hide
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public void outputDirtyFlags(String indent, boolean clear, int clearMask) {
+ Log.d(VIEW_LOG_TAG, indent + this + " DIRTY("
+ + (mPrivateFlags & View.PFLAG_DIRTY_MASK)
+ + ") DRAWN(" + (mPrivateFlags & PFLAG_DRAWN) + ")" + " CACHE_VALID("
+ + (mPrivateFlags & View.PFLAG_DRAWING_CACHE_VALID)
+ + ") INVALIDATED(" + (mPrivateFlags & PFLAG_INVALIDATED) + ")");
+ if (clear) {
+ mPrivateFlags &= clearMask;
+ }
+ if (this instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) this;
+ final int count = parent.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = parent.getChildAt(i);
+ child.outputDirtyFlags(indent + " ", clear, clearMask);
+ }
+ }
+ }
+
+ /**
+ * This method is used by ViewGroup to cause its children to restore or recreate their
+ * display lists. It is called by getDisplayList() when the parent ViewGroup does not need
+ * to recreate its own display list, which would happen if it went through the normal
+ * draw/dispatchDraw mechanisms.
+ *
+ * @hide
+ */
+ protected void dispatchGetDisplayList() {}
+
+ /**
+ * A view that is not attached or hardware accelerated cannot create a display list.
+ * This method checks these conditions and returns the appropriate result.
+ *
+ * @return true if view has the ability to create a display list, false otherwise.
+ *
+ * @hide
+ */
+ public boolean canHaveDisplayList() {
+ return !(mAttachInfo == null || mAttachInfo.mThreadedRenderer == null);
+ }
+
+ /**
+ * Gets the RenderNode for the view, and updates its DisplayList (if needed and supported)
+ * @hide
+ */
+ @NonNull
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public RenderNode updateDisplayListIfDirty() {
+ final RenderNode renderNode = mRenderNode;
+ if (!canHaveDisplayList()) {
+ // can't populate RenderNode, don't try
+ return renderNode;
+ }
+
+ if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
+ || !renderNode.hasDisplayList()
+ || (mRecreateDisplayList)) {
+ // Don't need to recreate the display list, just need to tell our
+ // children to restore/recreate theirs
+ if (renderNode.hasDisplayList()
+ && !mRecreateDisplayList) {
+ mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ dispatchGetDisplayList();
+
+ return renderNode; // no work needed
+ }
+
+ // If we got here, we're recreating it. Mark it as such to ensure that
+ // we copy in child display lists into ours in drawChild()
+ mRecreateDisplayList = true;
+
+ int width = mRight - mLeft;
+ int height = mBottom - mTop;
+ int layerType = getLayerType();
+
+ // Hacky hack: Reset any stretch effects as those are applied during the draw pass
+ // instead of being "stateful" like other RenderNode properties
+ renderNode.clearStretch();
+
+ final RecordingCanvas canvas = renderNode.beginRecording(width, height);
+
+ try {
+ if (layerType == LAYER_TYPE_SOFTWARE) {
+ buildDrawingCache(true);
+ Bitmap cache = getDrawingCache(true);
+ if (cache != null) {
+ canvas.drawBitmap(cache, 0, 0, mLayerPaint);
+ }
+ } else {
+ computeScroll();
+
+ canvas.translate(-mScrollX, -mScrollY);
+ mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ dispatchDraw(canvas);
+ drawAutofilledHighlight(canvas);
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().draw(canvas);
+ }
+ if (isShowingLayoutBounds()) {
+ debugDrawFocus(canvas);
+ }
+ } else {
+ draw(canvas);
+ }
+ }
+ } finally {
+ renderNode.endRecording();
+ setDisplayListProperties(renderNode);
+ }
+ } else {
+ mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ }
+ return renderNode;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void resetDisplayList() {
+ mRenderNode.discardDisplayList();
+ if (mBackgroundRenderNode != null) {
+ mBackgroundRenderNode.discardDisplayList();
+ }
+ }
+
+ /**
+ * <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p>
+ *
+ * @return A non-scaled bitmap representing this view or null if cache is disabled.
+ *
+ * @see #getDrawingCache(boolean)
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public Bitmap getDrawingCache() {
+ return getDrawingCache(false);
+ }
+
+ /**
+ * <p>Returns the bitmap in which this view drawing is cached. The returned bitmap
+ * is null when caching is disabled. If caching is enabled and the cache is not ready,
+ * this method will create it. Calling {@link #draw(android.graphics.Canvas)} will not
+ * draw from the cache when the cache is enabled. To benefit from the cache, you must
+ * request the drawing cache by calling this method and draw it on screen if the
+ * returned bitmap is not null.</p>
+ *
+ * <p>Note about auto scaling in compatibility mode: When auto scaling is not enabled,
+ * this method will create a bitmap of the same size as this view. Because this bitmap
+ * will be drawn scaled by the parent ViewGroup, the result on screen might show
+ * scaling artifacts. To avoid such artifacts, you should call this method by setting
+ * the auto scaling to true. Doing so, however, will generate a bitmap of a different
+ * size than the view. This implies that your application must be able to handle this
+ * size.</p>
+ *
+ * @param autoScale Indicates whether the generated bitmap should be scaled based on
+ * the current density of the screen when the application is in compatibility
+ * mode.
+ *
+ * @return A bitmap representing this view or null if cache is disabled.
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #isDrawingCacheEnabled()
+ * @see #buildDrawingCache(boolean)
+ * @see #destroyDrawingCache()
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public Bitmap getDrawingCache(boolean autoScale) {
+ if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) {
+ return null;
+ }
+ if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {
+ buildDrawingCache(autoScale);
+ }
+ return autoScale ? mDrawingCache : mUnscaledDrawingCache;
+ }
+
+ /**
+ * <p>Frees the resources used by the drawing cache. If you call
+ * {@link #buildDrawingCache()} manually without calling
+ * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you
+ * should cleanup the cache with this method afterwards.</p>
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #buildDrawingCache()
+ * @see #getDrawingCache()
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void destroyDrawingCache() {
+ if (mDrawingCache != null) {
+ mDrawingCache.recycle();
+ mDrawingCache = null;
+ }
+ if (mUnscaledDrawingCache != null) {
+ mUnscaledDrawingCache.recycle();
+ mUnscaledDrawingCache = null;
+ }
+ }
+
+ /**
+ * Setting a solid background color for the drawing cache's bitmaps will improve
+ * performance and memory usage. Note, though that this should only be used if this
+ * view will always be drawn on top of a solid color.
+ *
+ * @param color The background color to use for the drawing cache's bitmap
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #buildDrawingCache()
+ * @see #getDrawingCache()
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void setDrawingCacheBackgroundColor(@ColorInt int color) {
+ if (color != mDrawingCacheBackgroundColor) {
+ mDrawingCacheBackgroundColor = color;
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ }
+ }
+
+ /**
+ * @see #setDrawingCacheBackgroundColor(int)
+ *
+ * @return The background color to used for the drawing cache's bitmap
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ @ColorInt
+ public int getDrawingCacheBackgroundColor() {
+ return mDrawingCacheBackgroundColor;
+ }
+
+ /**
+ * <p>Calling this method is equivalent to calling <code>buildDrawingCache(false)</code>.</p>
+ *
+ * @see #buildDrawingCache(boolean)
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void buildDrawingCache() {
+ buildDrawingCache(false);
+ }
+
+ /**
+ * <p>Forces the drawing cache to be built if the drawing cache is invalid.</p>
+ *
+ * <p>If you call {@link #buildDrawingCache()} manually without calling
+ * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you
+ * should cleanup the cache by calling {@link #destroyDrawingCache()} afterwards.</p>
+ *
+ * <p>Note about auto scaling in compatibility mode: When auto scaling is not enabled,
+ * this method will create a bitmap of the same size as this view. Because this bitmap
+ * will be drawn scaled by the parent ViewGroup, the result on screen might show
+ * scaling artifacts. To avoid such artifacts, you should call this method by setting
+ * the auto scaling to true. Doing so, however, will generate a bitmap of a different
+ * size than the view. This implies that your application must be able to handle this
+ * size.</p>
+ *
+ * <p>You should avoid calling this method when hardware acceleration is enabled. If
+ * you do not need the drawing cache bitmap, calling this method will increase memory
+ * usage and cause the view to be rendered in software once, thus negatively impacting
+ * performance.</p>
+ *
+ * @see #getDrawingCache()
+ * @see #destroyDrawingCache()
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void buildDrawingCache(boolean autoScale) {
+ if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ?
+ mDrawingCache == null : mUnscaledDrawingCache == null)) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "buildDrawingCache/SW Layer for " + getClass().getSimpleName());
+ }
+ try {
+ buildDrawingCacheImpl(autoScale);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+ }
+
+ /**
+ * private, internal implementation of buildDrawingCache, used to enable tracing
+ */
+ private void buildDrawingCacheImpl(boolean autoScale) {
+ mCachingFailed = false;
+
+ int width = mRight - mLeft;
+ int height = mBottom - mTop;
+
+ final AttachInfo attachInfo = mAttachInfo;
+ final boolean scalingRequired = attachInfo != null && attachInfo.mScalingRequired;
+
+ if (autoScale && scalingRequired) {
+ width = (int) ((width * attachInfo.mApplicationScale) + 0.5f);
+ height = (int) ((height * attachInfo.mApplicationScale) + 0.5f);
+ }
+
+ final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor;
+ final boolean opaque = drawingCacheBackgroundColor != 0 || isOpaque();
+ final boolean use32BitCache = attachInfo != null && attachInfo.mUse32BitDrawingCache;
+
+ final long projectedBitmapSize = width * height * (opaque && !use32BitCache ? 2 : 4);
+ final long drawingCacheSize =
+ ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize();
+ if (width <= 0 || height <= 0 || projectedBitmapSize > drawingCacheSize) {
+ if (width > 0 && height > 0) {
+ Log.w(VIEW_LOG_TAG, getClass().getSimpleName() + " not displayed because it is"
+ + " too large to fit into a software layer (or drawing cache), needs "
+ + projectedBitmapSize + " bytes, only "
+ + drawingCacheSize + " available");
+ }
+ destroyDrawingCache();
+ mCachingFailed = true;
+ return;
+ }
+
+ boolean clear = true;
+ Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache;
+
+ if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) {
+ Bitmap.Config quality;
+ if (!opaque) {
+ // Never pick ARGB_4444 because it looks awful
+ // Keep the DRAWING_CACHE_QUALITY_LOW flag just in case
+ switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) {
+ case DRAWING_CACHE_QUALITY_AUTO:
+ case DRAWING_CACHE_QUALITY_LOW:
+ case DRAWING_CACHE_QUALITY_HIGH:
+ default:
+ quality = Bitmap.Config.ARGB_8888;
+ break;
+ }
+ } else {
+ // Optimization for translucent windows
+ // If the window is translucent, use a 32 bits bitmap to benefit from memcpy()
+ quality = use32BitCache ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
+ }
+
+ // Try to cleanup memory
+ if (bitmap != null) bitmap.recycle();
+
+ try {
+ bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
+ width, height, quality);
+ bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
+ if (autoScale) {
+ mDrawingCache = bitmap;
+ } else {
+ mUnscaledDrawingCache = bitmap;
+ }
+ if (opaque && use32BitCache) bitmap.setHasAlpha(false);
+ } catch (OutOfMemoryError e) {
+ // If there is not enough memory to create the bitmap cache, just
+ // ignore the issue as bitmap caches are not required to draw the
+ // view hierarchy
+ if (autoScale) {
+ mDrawingCache = null;
+ } else {
+ mUnscaledDrawingCache = null;
+ }
+ mCachingFailed = true;
+ return;
+ }
+
+ clear = drawingCacheBackgroundColor != 0;
+ }
+
+ Canvas canvas;
+ if (attachInfo != null) {
+ canvas = attachInfo.mCanvas;
+ if (canvas == null) {
+ canvas = new Canvas();
+ }
+ canvas.setBitmap(bitmap);
+ // Temporarily clobber the cached Canvas in case one of our children
+ // is also using a drawing cache. Without this, the children would
+ // steal the canvas by attaching their own bitmap to it and bad, bad
+ // thing would happen (invisible views, corrupted drawings, etc.)
+ attachInfo.mCanvas = null;
+ } else {
+ // This case should hopefully never or seldom happen
+ canvas = new Canvas(bitmap);
+ }
+
+ if (clear) {
+ bitmap.eraseColor(drawingCacheBackgroundColor);
+ }
+
+ computeScroll();
+ final int restoreCount = canvas.save();
+
+ if (autoScale && scalingRequired) {
+ final float scale = attachInfo.mApplicationScale;
+ canvas.scale(scale, scale);
+ }
+
+ canvas.translate(-mScrollX, -mScrollY);
+
+ mPrivateFlags |= PFLAG_DRAWN;
+ if (mAttachInfo == null || !mAttachInfo.mHardwareAccelerated ||
+ mLayerType != LAYER_TYPE_NONE) {
+ mPrivateFlags |= PFLAG_DRAWING_CACHE_VALID;
+ }
+
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ dispatchDraw(canvas);
+ drawAutofilledHighlight(canvas);
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().draw(canvas);
+ }
+ } else {
+ draw(canvas);
+ }
+
+ canvas.restoreToCount(restoreCount);
+ canvas.setBitmap(null);
+
+ if (attachInfo != null) {
+ // Restore the cached Canvas for our siblings
+ attachInfo.mCanvas = canvas;
+ }
+ }
+
+ /**
+ * Create a snapshot of the view into a bitmap. We should probably make
+ * some form of this public, but should think about the API.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Bitmap createSnapshot(ViewDebug.CanvasProvider canvasProvider, boolean skipChildren) {
+ int width = mRight - mLeft;
+ int height = mBottom - mTop;
+
+ final AttachInfo attachInfo = mAttachInfo;
+ final float scale = attachInfo != null ? attachInfo.mApplicationScale : 1.0f;
+ width = (int) ((width * scale) + 0.5f);
+ height = (int) ((height * scale) + 0.5f);
+
+ Canvas oldCanvas = null;
+ try {
+ Canvas canvas = canvasProvider.getCanvas(this,
+ width > 0 ? width : 1, height > 0 ? height : 1);
+
+ if (attachInfo != null) {
+ oldCanvas = attachInfo.mCanvas;
+ // Temporarily clobber the cached Canvas in case one of our children
+ // is also using a drawing cache. Without this, the children would
+ // steal the canvas by attaching their own bitmap to it and bad, bad
+ // things would happen (invisible views, corrupted drawings, etc.)
+ attachInfo.mCanvas = null;
+ }
+
+ computeScroll();
+ final int restoreCount = canvas.save();
+ canvas.scale(scale, scale);
+ canvas.translate(-mScrollX, -mScrollY);
+
+ // Temporarily remove the dirty mask
+ int flags = mPrivateFlags;
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ dispatchDraw(canvas);
+ drawAutofilledHighlight(canvas);
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().draw(canvas);
+ }
+ } else {
+ draw(canvas);
+ }
+
+ mPrivateFlags = flags;
+ canvas.restoreToCount(restoreCount);
+ return canvasProvider.createBitmap();
+ } finally {
+ if (oldCanvas != null) {
+ attachInfo.mCanvas = oldCanvas;
+ }
+ }
+ }
+
+ /**
+ * Indicates whether this View is currently in edit mode. A View is usually
+ * in edit mode when displayed within a developer tool. For instance, if
+ * this View is being drawn by a visual user interface builder, this method
+ * should return true.
+ *
+ * Subclasses should check the return value of this method to provide
+ * different behaviors if their normal behavior might interfere with the
+ * host environment. For instance: the class spawns a thread in its
+ * constructor, the drawing code relies on device-specific features, etc.
+ *
+ * This method is usually checked in the drawing code of custom widgets.
+ *
+ * @return True if this View is in edit mode, false otherwise.
+ */
+ public boolean isInEditMode() {
+ return false;
+ }
+
+ /**
+ * If the View draws content inside its padding and enables fading edges,
+ * it needs to support padding offsets. Padding offsets are added to the
+ * fading edges to extend the length of the fade so that it covers pixels
+ * drawn inside the padding.
+ *
+ * Subclasses of this class should override this method if they need
+ * to draw content inside the padding.
+ *
+ * @return True if padding offset must be applied, false otherwise.
+ *
+ * @see #getLeftPaddingOffset()
+ * @see #getRightPaddingOffset()
+ * @see #getTopPaddingOffset()
+ * @see #getBottomPaddingOffset()
+ *
+ * @since CURRENT
+ */
+ protected boolean isPaddingOffsetRequired() {
+ return false;
+ }
+
+ /**
+ * Amount by which to extend the left fading region. Called only when
+ * {@link #isPaddingOffsetRequired()} returns true.
+ *
+ * @return The left padding offset in pixels.
+ *
+ * @see #isPaddingOffsetRequired()
+ *
+ * @since CURRENT
+ */
+ protected int getLeftPaddingOffset() {
+ return 0;
+ }
+
+ /**
+ * Amount by which to extend the right fading region. Called only when
+ * {@link #isPaddingOffsetRequired()} returns true.
+ *
+ * @return The right padding offset in pixels.
+ *
+ * @see #isPaddingOffsetRequired()
+ *
+ * @since CURRENT
+ */
+ protected int getRightPaddingOffset() {
+ return 0;
+ }
+
+ /**
+ * Amount by which to extend the top fading region. Called only when
+ * {@link #isPaddingOffsetRequired()} returns true.
+ *
+ * @return The top padding offset in pixels.
+ *
+ * @see #isPaddingOffsetRequired()
+ *
+ * @since CURRENT
+ */
+ protected int getTopPaddingOffset() {
+ return 0;
+ }
+
+ /**
+ * Amount by which to extend the bottom fading region. Called only when
+ * {@link #isPaddingOffsetRequired()} returns true.
+ *
+ * @return The bottom padding offset in pixels.
+ *
+ * @see #isPaddingOffsetRequired()
+ *
+ * @since CURRENT
+ */
+ protected int getBottomPaddingOffset() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ * @param offsetRequired
+ */
+ protected int getFadeTop(boolean offsetRequired) {
+ int top = mPaddingTop;
+ if (offsetRequired) top += getTopPaddingOffset();
+ return top;
+ }
+
+ /**
+ * @hide
+ * @param offsetRequired
+ */
+ protected int getFadeHeight(boolean offsetRequired) {
+ int padding = mPaddingTop;
+ if (offsetRequired) padding += getTopPaddingOffset();
+ return mBottom - mTop - mPaddingBottom - padding;
+ }
+
+ /**
+ * <p>Indicates whether this view is attached to a hardware accelerated
+ * window or not.</p>
+ *
+ * <p>Even if this method returns true, it does not mean that every call
+ * to {@link #draw(android.graphics.Canvas)} will be made with an hardware
+ * accelerated {@link android.graphics.Canvas}. For instance, if this view
+ * is drawn onto an offscreen {@link android.graphics.Bitmap} and its
+ * window is hardware accelerated,
+ * {@link android.graphics.Canvas#isHardwareAccelerated()} will likely
+ * return false, and this method will return true.</p>
+ *
+ * @return True if the view is attached to a window and the window is
+ * hardware accelerated; false in any other case.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ public boolean isHardwareAccelerated() {
+ return mAttachInfo != null && mAttachInfo.mHardwareAccelerated;
+ }
+
+ /**
+ * Sets a rectangular area on this view to which the view will be clipped
+ * when it is drawn. Setting the value to null will remove the clip bounds
+ * and the view will draw normally, using its full bounds.
+ *
+ * @param clipBounds The rectangular area, in the local coordinates of
+ * this view, to which future drawing operations will be clipped.
+ */
+ public void setClipBounds(Rect clipBounds) {
+ if (clipBounds == mClipBounds
+ || (clipBounds != null && clipBounds.equals(mClipBounds))) {
+ return;
+ }
+ if (clipBounds != null) {
+ if (mClipBounds == null) {
+ mClipBounds = new Rect(clipBounds);
+ } else {
+ mClipBounds.set(clipBounds);
+ }
+ } else {
+ mClipBounds = null;
+ }
+ mRenderNode.setClipRect(mClipBounds);
+ invalidateViewProperty(false, false);
+ }
+
+ /**
+ * Returns a copy of the current {@link #setClipBounds(Rect) clipBounds}.
+ *
+ * @return A copy of the current clip bounds if clip bounds are set,
+ * otherwise null.
+ */
+ public Rect getClipBounds() {
+ return (mClipBounds != null) ? new Rect(mClipBounds) : null;
+ }
+
+
+ /**
+ * Populates an output rectangle with the clip bounds of the view,
+ * returning {@code true} if successful or {@code false} if the view's
+ * clip bounds are {@code null}.
+ *
+ * @param outRect rectangle in which to place the clip bounds of the view
+ * @return {@code true} if successful or {@code false} if the view's
+ * clip bounds are {@code null}
+ */
+ public boolean getClipBounds(Rect outRect) {
+ if (mClipBounds != null) {
+ outRect.set(mClipBounds);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
+ * case of an active Animation being run on the view.
+ */
+ private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
+ Animation a, boolean scalingRequired) {
+ Transformation invalidationTransform;
+ final int flags = parent.mGroupFlags;
+ final boolean initialized = a.isInitialized();
+ if (!initialized) {
+ a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
+ a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
+ if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
+ onAnimationStart();
+ }
+
+ final Transformation t = parent.getChildTransformation();
+ boolean more = a.getTransformation(drawingTime, t, 1f);
+ if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
+ if (parent.mInvalidationTransformation == null) {
+ parent.mInvalidationTransformation = new Transformation();
+ }
+ invalidationTransform = parent.mInvalidationTransformation;
+ a.getTransformation(drawingTime, invalidationTransform, 1f);
+ } else {
+ invalidationTransform = t;
+ }
+
+ if (more) {
+ if (!a.willChangeBounds()) {
+ if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
+ ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
+ parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
+ } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
+ // The child need to draw an animation, potentially offscreen, so
+ // make sure we do not cancel invalidate requests
+ parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
+ parent.invalidate(mLeft, mTop, mRight, mBottom);
+ }
+ } else {
+ if (parent.mInvalidateRegion == null) {
+ parent.mInvalidateRegion = new RectF();
+ }
+ final RectF region = parent.mInvalidateRegion;
+ a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
+ invalidationTransform);
+
+ // The child need to draw an animation, potentially offscreen, so
+ // make sure we do not cancel invalidate requests
+ parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
+
+ final int left = mLeft + (int) region.left;
+ final int top = mTop + (int) region.top;
+ parent.invalidate(left, top, left + (int) (region.width() + .5f),
+ top + (int) (region.height() + .5f));
+ }
+ }
+ return more;
+ }
+
+ /**
+ * This method is called by getDisplayList() when a display list is recorded for a View.
+ * It pushes any properties to the RenderNode that aren't managed by the RenderNode.
+ */
+ void setDisplayListProperties(RenderNode renderNode) {
+ if (renderNode != null) {
+ renderNode.setHasOverlappingRendering(getHasOverlappingRendering());
+ renderNode.setClipToBounds(mParent instanceof ViewGroup
+ && ((ViewGroup) mParent).getClipChildren());
+
+ float alpha = 1;
+ if (mParent instanceof ViewGroup && (((ViewGroup) mParent).mGroupFlags &
+ ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
+ ViewGroup parentVG = (ViewGroup) mParent;
+ final Transformation t = parentVG.getChildTransformation();
+ if (parentVG.getChildStaticTransformation(this, t)) {
+ final int transformType = t.getTransformationType();
+ if (transformType != Transformation.TYPE_IDENTITY) {
+ if ((transformType & Transformation.TYPE_ALPHA) != 0) {
+ alpha = t.getAlpha();
+ }
+ if ((transformType & Transformation.TYPE_MATRIX) != 0) {
+ renderNode.setStaticMatrix(t.getMatrix());
+ }
+ }
+ }
+ }
+ if (mTransformationInfo != null) {
+ alpha *= getFinalAlpha();
+ if (alpha < 1) {
+ final int multipliedAlpha = (int) (255 * alpha);
+ if (onSetAlpha(multipliedAlpha)) {
+ alpha = 1;
+ }
+ }
+ renderNode.setAlpha(alpha);
+ } else if (alpha < 1) {
+ renderNode.setAlpha(alpha);
+ }
+ }
+ }
+
+ /**
+ * This method is called by ViewGroup.drawChild() to have each child view draw itself.
+ *
+ * This is where the View specializes rendering behavior based on layer type,
+ * and hardware acceleration.
+ */
+ boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
+
+ final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
+ /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
+ *
+ * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
+ * HW accelerated, it can't handle drawing RenderNodes.
+ */
+ boolean drawingWithRenderNode = mAttachInfo != null
+ && mAttachInfo.mHardwareAccelerated
+ && hardwareAcceleratedCanvas;
+
+ boolean more = false;
+ final boolean childHasIdentityMatrix = hasIdentityMatrix();
+ final int parentFlags = parent.mGroupFlags;
+
+ if ((parentFlags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) != 0) {
+ parent.getChildTransformation().clear();
+ parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
+ }
+
+ Transformation transformToApply = null;
+ boolean concatMatrix = false;
+ final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
+ final Animation a = getAnimation();
+ if (a != null) {
+ more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
+ concatMatrix = a.willChangeTransformationMatrix();
+ if (concatMatrix) {
+ mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
+ }
+ transformToApply = parent.getChildTransformation();
+ } else {
+ if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
+ // No longer animating: clear out old animation matrix
+ mRenderNode.setAnimationMatrix(null);
+ mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
+ }
+ if (!drawingWithRenderNode
+ && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
+ final Transformation t = parent.getChildTransformation();
+ final boolean hasTransform = parent.getChildStaticTransformation(this, t);
+ if (hasTransform) {
+ final int transformType = t.getTransformationType();
+ transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
+ concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
+ }
+ }
+ }
+
+ concatMatrix |= !childHasIdentityMatrix;
+
+ // Sets the flag as early as possible to allow draw() implementations
+ // to call invalidate() successfully when doing animations
+ mPrivateFlags |= PFLAG_DRAWN;
+
+ if (!concatMatrix &&
+ (parentFlags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
+ ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
+ canvas.quickReject(mLeft, mTop, mRight, mBottom) &&
+ (mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
+ mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
+ return more;
+ }
+ mPrivateFlags2 &= ~PFLAG2_VIEW_QUICK_REJECTED;
+
+ if (hardwareAcceleratedCanvas) {
+ // Clear INVALIDATED flag to allow invalidation to occur during rendering, but
+ // retain the flag's value temporarily in the mRecreateDisplayList flag
+ mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
+ mPrivateFlags &= ~PFLAG_INVALIDATED;
+ }
+
+ RenderNode renderNode = null;
+ Bitmap cache = null;
+ int layerType = getLayerType(); // TODO: signify cache state with just 'cache' local
+ if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
+ if (layerType != LAYER_TYPE_NONE) {
+ // If not drawing with RenderNode, treat HW layers as SW
+ layerType = LAYER_TYPE_SOFTWARE;
+ buildDrawingCache(true);
+ }
+ cache = getDrawingCache(true);
+ }
+
+ if (drawingWithRenderNode) {
+ // Delay getting the display list until animation-driven alpha values are
+ // set up and possibly passed on to the view
+ renderNode = updateDisplayListIfDirty();
+ if (!renderNode.hasDisplayList()) {
+ // Uncommon, but possible. If a view is removed from the hierarchy during the call
+ // to getDisplayList(), the display list will be marked invalid and we should not
+ // try to use it again.
+ renderNode = null;
+ drawingWithRenderNode = false;
+ }
+ }
+
+ int sx = 0;
+ int sy = 0;
+ if (!drawingWithRenderNode) {
+ computeScroll();
+ sx = mScrollX;
+ sy = mScrollY;
+ }
+
+ final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
+ final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
+
+ int restoreTo = -1;
+ if (!drawingWithRenderNode || transformToApply != null) {
+ restoreTo = canvas.save();
+ }
+ if (offsetForScroll) {
+ canvas.translate(mLeft - sx, mTop - sy);
+ } else {
+ if (!drawingWithRenderNode) {
+ canvas.translate(mLeft, mTop);
+ }
+ if (scalingRequired) {
+ if (drawingWithRenderNode) {
+ // TODO: Might not need this if we put everything inside the DL
+ restoreTo = canvas.save();
+ }
+ // mAttachInfo cannot be null, otherwise scalingRequired == false
+ final float scale = 1.0f / mAttachInfo.mApplicationScale;
+ canvas.scale(scale, scale);
+ }
+ }
+
+ float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
+ if (transformToApply != null
+ || alpha < 1
+ || !hasIdentityMatrix()
+ || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
+ if (transformToApply != null || !childHasIdentityMatrix) {
+ int transX = 0;
+ int transY = 0;
+
+ if (offsetForScroll) {
+ transX = -sx;
+ transY = -sy;
+ }
+
+ if (transformToApply != null) {
+ if (concatMatrix) {
+ if (drawingWithRenderNode) {
+ renderNode.setAnimationMatrix(transformToApply.getMatrix());
+ } else {
+ // Undo the scroll translation, apply the transformation matrix,
+ // then redo the scroll translate to get the correct result.
+ canvas.translate(-transX, -transY);
+ canvas.concat(transformToApply.getMatrix());
+ canvas.translate(transX, transY);
+ }
+ parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
+ }
+
+ float transformAlpha = transformToApply.getAlpha();
+ if (transformAlpha < 1) {
+ alpha *= transformAlpha;
+ parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
+ }
+ }
+
+ if (!childHasIdentityMatrix && !drawingWithRenderNode) {
+ canvas.translate(-transX, -transY);
+ canvas.concat(getMatrix());
+ canvas.translate(transX, transY);
+ }
+ }
+
+ // Deal with alpha if it is or used to be <1
+ if (alpha < 1 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
+ if (alpha < 1) {
+ mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_ALPHA;
+ } else {
+ mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_ALPHA;
+ }
+ parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
+ if (!drawingWithDrawingCache) {
+ final int multipliedAlpha = (int) (255 * alpha);
+ if (!onSetAlpha(multipliedAlpha)) {
+ if (drawingWithRenderNode) {
+ renderNode.setAlpha(alpha * getAlpha() * getTransitionAlpha());
+ } else if (layerType == LAYER_TYPE_NONE) {
+ canvas.saveLayerAlpha(sx, sy, sx + getWidth(), sy + getHeight(),
+ multipliedAlpha);
+ }
+ } else {
+ // Alpha is handled by the child directly, clobber the layer's alpha
+ mPrivateFlags |= PFLAG_ALPHA_SET;
+ }
+ }
+ }
+ } else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
+ onSetAlpha(255);
+ mPrivateFlags &= ~PFLAG_ALPHA_SET;
+ }
+
+ if (!drawingWithRenderNode) {
+ // apply clips directly, since RenderNode won't do it for this draw
+ if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
+ if (offsetForScroll) {
+ canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
+ } else {
+ if (!scalingRequired || cache == null) {
+ canvas.clipRect(0, 0, getWidth(), getHeight());
+ } else {
+ canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
+ }
+ }
+ }
+
+ if (mClipBounds != null) {
+ // clip bounds ignore scroll
+ canvas.clipRect(mClipBounds);
+ }
+ }
+
+ if (!drawingWithDrawingCache) {
+ if (drawingWithRenderNode) {
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ ((RecordingCanvas) canvas).drawRenderNode(renderNode);
+ } else {
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ dispatchDraw(canvas);
+ } else {
+ draw(canvas);
+ }
+ }
+ } else if (cache != null) {
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
+ // no layer paint, use temporary paint to draw bitmap
+ Paint cachePaint = parent.mCachePaint;
+ if (cachePaint == null) {
+ cachePaint = new Paint();
+ cachePaint.setDither(false);
+ parent.mCachePaint = cachePaint;
+ }
+ cachePaint.setAlpha((int) (alpha * 255));
+ canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
+ } else {
+ // use layer paint to draw the bitmap, merging the two alphas, but also restore
+ int layerPaintAlpha = mLayerPaint.getAlpha();
+ if (alpha < 1) {
+ mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
+ }
+ canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
+ if (alpha < 1) {
+ mLayerPaint.setAlpha(layerPaintAlpha);
+ }
+ }
+ }
+
+ if (restoreTo >= 0) {
+ canvas.restoreToCount(restoreTo);
+ }
+
+ if (a != null && !more) {
+ if (!hardwareAcceleratedCanvas && !a.getFillAfter()) {
+ onSetAlpha(255);
+ }
+ parent.finishAnimatingView(this, a);
+ }
+
+ if (more && hardwareAcceleratedCanvas) {
+ if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
+ // alpha animations should cause the child to recreate its display list
+ invalidate(true);
+ }
+ }
+
+ mRecreateDisplayList = false;
+
+ return more;
+ }
+
+ static Paint getDebugPaint() {
+ if (sDebugPaint == null) {
+ sDebugPaint = new Paint();
+ sDebugPaint.setAntiAlias(false);
+ }
+ return sDebugPaint;
+ }
+
+ final int dipsToPixels(int dips) {
+ float scale = getContext().getResources().getDisplayMetrics().density;
+ return (int) (dips * scale + 0.5f);
+ }
+
+ final private void debugDrawFocus(Canvas canvas) {
+ if (isFocused()) {
+ final int cornerSquareSize = dipsToPixels(DEBUG_CORNERS_SIZE_DIP);
+ final int l = mScrollX;
+ final int r = l + mRight - mLeft;
+ final int t = mScrollY;
+ final int b = t + mBottom - mTop;
+
+ final Paint paint = getDebugPaint();
+ paint.setColor(DEBUG_CORNERS_COLOR);
+
+ // Draw squares in corners.
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawRect(l, t, l + cornerSquareSize, t + cornerSquareSize, paint);
+ canvas.drawRect(r - cornerSquareSize, t, r, t + cornerSquareSize, paint);
+ canvas.drawRect(l, b - cornerSquareSize, l + cornerSquareSize, b, paint);
+ canvas.drawRect(r - cornerSquareSize, b - cornerSquareSize, r, b, paint);
+
+ // Draw big X across the view.
+ paint.setStyle(Paint.Style.STROKE);
+ canvas.drawLine(l, t, r, b, paint);
+ canvas.drawLine(l, b, r, t, paint);
+ }
+ }
+
+ /**
+ * Manually render this view (and all of its children) to the given Canvas.
+ * The view must have already done a full layout before this function is
+ * called. When implementing a view, implement
+ * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
+ * If you do need to override this method, call the superclass version.
+ *
+ * @param canvas The Canvas to which the View is rendered.
+ */
+ @CallSuper
+ public void draw(Canvas canvas) {
+ final int privateFlags = mPrivateFlags;
+ mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
+
+ /*
+ * Draw traversal performs several drawing steps which must be executed
+ * in the appropriate order:
+ *
+ * 1. Draw the background
+ * 2. If necessary, save the canvas' layers to prepare for fading
+ * 3. Draw view's content
+ * 4. Draw children
+ * 5. If necessary, draw the fading edges and restore layers
+ * 6. Draw decorations (scrollbars for instance)
+ * 7. If necessary, draw the default focus highlight
+ */
+
+ // Step 1, draw the background, if needed
+ int saveCount;
+
+ drawBackground(canvas);
+
+ // skip step 2 & 5 if possible (common case)
+ final int viewFlags = mViewFlags;
+ boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
+ boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
+ if (!verticalEdges && !horizontalEdges) {
+ // Step 3, draw the content
+ onDraw(canvas);
+
+ // Step 4, draw the children
+ dispatchDraw(canvas);
+
+ drawAutofilledHighlight(canvas);
+
+ // Overlay is part of the content and draws beneath Foreground
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().dispatchDraw(canvas);
+ }
+
+ // Step 6, draw decorations (foreground, scrollbars)
+ onDrawForeground(canvas);
+
+ // Step 7, draw the default focus highlight
+ drawDefaultFocusHighlight(canvas);
+
+ if (isShowingLayoutBounds()) {
+ debugDrawFocus(canvas);
+ }
+
+ // we're done...
+ return;
+ }
+
+ /*
+ * Here we do the full fledged routine...
+ * (this is an uncommon case where speed matters less,
+ * this is why we repeat some of the tests that have been
+ * done above)
+ */
+
+ boolean drawTop = false;
+ boolean drawBottom = false;
+ boolean drawLeft = false;
+ boolean drawRight = false;
+
+ float topFadeStrength = 0.0f;
+ float bottomFadeStrength = 0.0f;
+ float leftFadeStrength = 0.0f;
+ float rightFadeStrength = 0.0f;
+
+ // Step 2, save the canvas' layers
+ int paddingLeft = mPaddingLeft;
+
+ final boolean offsetRequired = isPaddingOffsetRequired();
+ if (offsetRequired) {
+ paddingLeft += getLeftPaddingOffset();
+ }
+
+ int left = mScrollX + paddingLeft;
+ int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
+ int top = mScrollY + getFadeTop(offsetRequired);
+ int bottom = top + getFadeHeight(offsetRequired);
+
+ if (offsetRequired) {
+ right += getRightPaddingOffset();
+ bottom += getBottomPaddingOffset();
+ }
+
+ final ScrollabilityCache scrollabilityCache = mScrollCache;
+ final float fadeHeight = scrollabilityCache.fadingEdgeLength;
+ int length = (int) fadeHeight;
+
+ // clip the fade length if top and bottom fades overlap
+ // overlapping fades produce odd-looking artifacts
+ if (verticalEdges && (top + length > bottom - length)) {
+ length = (bottom - top) / 2;
+ }
+
+ // also clip horizontal fades if necessary
+ if (horizontalEdges && (left + length > right - length)) {
+ length = (right - left) / 2;
+ }
+
+ if (verticalEdges) {
+ topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
+ drawTop = topFadeStrength * fadeHeight > 1.0f;
+ bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
+ drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
+ }
+
+ if (horizontalEdges) {
+ leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
+ drawLeft = leftFadeStrength * fadeHeight > 1.0f;
+ rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
+ drawRight = rightFadeStrength * fadeHeight > 1.0f;
+ }
+
+ saveCount = canvas.getSaveCount();
+ int topSaveCount = -1;
+ int bottomSaveCount = -1;
+ int leftSaveCount = -1;
+ int rightSaveCount = -1;
+
+ int solidColor = getSolidColor();
+ if (solidColor == 0) {
+ if (drawTop) {
+ topSaveCount = canvas.saveUnclippedLayer(left, top, right, top + length);
+ }
+
+ if (drawBottom) {
+ bottomSaveCount = canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
+ }
+
+ if (drawLeft) {
+ leftSaveCount = canvas.saveUnclippedLayer(left, top, left + length, bottom);
+ }
+
+ if (drawRight) {
+ rightSaveCount = canvas.saveUnclippedLayer(right - length, top, right, bottom);
+ }
+ } else {
+ scrollabilityCache.setFadeColor(solidColor);
+ }
+
+ // Step 3, draw the content
+ onDraw(canvas);
+
+ // Step 4, draw the children
+ dispatchDraw(canvas);
+
+ // Step 5, draw the fade effect and restore layers
+ final Paint p = scrollabilityCache.paint;
+ final Matrix matrix = scrollabilityCache.matrix;
+ final Shader fade = scrollabilityCache.shader;
+
+ // must be restored in the reverse order that they were saved
+ if (drawRight) {
+ matrix.setScale(1, fadeHeight * rightFadeStrength);
+ matrix.postRotate(90);
+ matrix.postTranslate(right, top);
+ fade.setLocalMatrix(matrix);
+ p.setShader(fade);
+ if (solidColor == 0) {
+ canvas.restoreUnclippedLayer(rightSaveCount, p);
+
+ } else {
+ canvas.drawRect(right - length, top, right, bottom, p);
+ }
+ }
+
+ if (drawLeft) {
+ matrix.setScale(1, fadeHeight * leftFadeStrength);
+ matrix.postRotate(-90);
+ matrix.postTranslate(left, top);
+ fade.setLocalMatrix(matrix);
+ p.setShader(fade);
+ if (solidColor == 0) {
+ canvas.restoreUnclippedLayer(leftSaveCount, p);
+ } else {
+ canvas.drawRect(left, top, left + length, bottom, p);
+ }
+ }
+
+ if (drawBottom) {
+ matrix.setScale(1, fadeHeight * bottomFadeStrength);
+ matrix.postRotate(180);
+ matrix.postTranslate(left, bottom);
+ fade.setLocalMatrix(matrix);
+ p.setShader(fade);
+ if (solidColor == 0) {
+ canvas.restoreUnclippedLayer(bottomSaveCount, p);
+ } else {
+ canvas.drawRect(left, bottom - length, right, bottom, p);
+ }
+ }
+
+ if (drawTop) {
+ matrix.setScale(1, fadeHeight * topFadeStrength);
+ matrix.postTranslate(left, top);
+ fade.setLocalMatrix(matrix);
+ p.setShader(fade);
+ if (solidColor == 0) {
+ canvas.restoreUnclippedLayer(topSaveCount, p);
+ } else {
+ canvas.drawRect(left, top, right, top + length, p);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+
+ drawAutofilledHighlight(canvas);
+
+ // Overlay is part of the content and draws beneath Foreground
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().dispatchDraw(canvas);
+ }
+
+ // Step 6, draw decorations (foreground, scrollbars)
+ onDrawForeground(canvas);
+
+ // Step 7, draw the default focus highlight
+ drawDefaultFocusHighlight(canvas);
+
+ if (isShowingLayoutBounds()) {
+ debugDrawFocus(canvas);
+ }
+ }
+
+ /**
+ * Draws the background onto the specified canvas.
+ *
+ * @param canvas Canvas on which to draw the background
+ */
+ @UnsupportedAppUsage
+ private void drawBackground(Canvas canvas) {
+ final Drawable background = mBackground;
+ if (background == null) {
+ return;
+ }
+
+ setBackgroundBounds();
+
+ // Attempt to use a display list if requested.
+ if (canvas.isHardwareAccelerated() && mAttachInfo != null
+ && mAttachInfo.mThreadedRenderer != null) {
+ mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
+
+ final RenderNode renderNode = mBackgroundRenderNode;
+ if (renderNode != null && renderNode.hasDisplayList()) {
+ setBackgroundRenderNodeProperties(renderNode);
+ ((RecordingCanvas) canvas).drawRenderNode(renderNode);
+ return;
+ }
+ }
+
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ if ((scrollX | scrollY) == 0) {
+ background.draw(canvas);
+ } else {
+ canvas.translate(scrollX, scrollY);
+ background.draw(canvas);
+ canvas.translate(-scrollX, -scrollY);
+ }
+ }
+
+ /**
+ * Sets the correct background bounds and rebuilds the outline, if needed.
+ * <p/>
+ * This is called by LayoutLib.
+ */
+ void setBackgroundBounds() {
+ if (mBackgroundSizeChanged && mBackground != null) {
+ mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
+ mBackgroundSizeChanged = false;
+ rebuildOutline();
+ }
+ }
+
+ private void setBackgroundRenderNodeProperties(RenderNode renderNode) {
+ renderNode.setTranslationX(mScrollX);
+ renderNode.setTranslationY(mScrollY);
+ }
+
+ /**
+ * Creates a new display list or updates the existing display list for the
+ * specified Drawable.
+ *
+ * @param drawable Drawable for which to create a display list
+ * @param renderNode Existing RenderNode, or {@code null}
+ * @return A valid display list for the specified drawable
+ */
+ private RenderNode getDrawableRenderNode(Drawable drawable, RenderNode renderNode) {
+ if (renderNode == null) {
+ renderNode = RenderNode.create(drawable.getClass().getName(),
+ new ViewAnimationHostBridge(this));
+ renderNode.setUsageHint(RenderNode.USAGE_BACKGROUND);
+ }
+
+ final Rect bounds = drawable.getBounds();
+ final int width = bounds.width();
+ final int height = bounds.height();
+
+ // Hacky hack: Reset any stretch effects as those are applied during the draw pass
+ // instead of being "stateful" like other RenderNode properties
+ renderNode.clearStretch();
+
+ final RecordingCanvas canvas = renderNode.beginRecording(width, height);
+
+ // Reverse left/top translation done by drawable canvas, which will
+ // instead be applied by rendernode's LTRB bounds below. This way, the
+ // drawable's bounds match with its rendernode bounds and its content
+ // will lie within those bounds in the rendernode tree.
+ canvas.translate(-bounds.left, -bounds.top);
+
+ try {
+ drawable.draw(canvas);
+ } finally {
+ renderNode.endRecording();
+ }
+
+ // Set up drawable properties that are view-independent.
+ renderNode.setLeftTopRightBottom(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ renderNode.setProjectBackwards(drawable.isProjected());
+ renderNode.setProjectionReceiver(true);
+ renderNode.setClipToBounds(false);
+ return renderNode;
+ }
+
+ /**
+ * Returns the overlay for this view, creating it if it does not yet exist.
+ * Adding drawables to the overlay will cause them to be displayed whenever
+ * the view itself is redrawn. Objects in the overlay should be actively
+ * managed: remove them when they should not be displayed anymore. The
+ * overlay will always have the same size as its host view.
+ *
+ * <p>Note: Overlays do not currently work correctly with {@link
+ * SurfaceView} or {@link TextureView}; contents in overlays for these
+ * types of views may not display correctly.</p>
+ *
+ * @return The ViewOverlay object for this view.
+ * @see ViewOverlay
+ */
+ public ViewOverlay getOverlay() {
+ if (mOverlay == null) {
+ mOverlay = new ViewOverlay(mContext, this);
+ }
+ return mOverlay;
+ }
+
+ /**
+ * Override this if your view is known to always be drawn on top of a solid color background,
+ * and needs to draw fading edges. Returning a non-zero color enables the view system to
+ * optimize the drawing of the fading edges. If you do return a non-zero color, the alpha
+ * should be set to 0xFF.
+ *
+ * @see #setVerticalFadingEdgeEnabled(boolean)
+ * @see #setHorizontalFadingEdgeEnabled(boolean)
+ *
+ * @return The known solid color background for this view, or 0 if the color may vary
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ @ColorInt
+ public int getSolidColor() {
+ return 0;
+ }
+
+ /**
+ * Build a human readable string representation of the specified view flags.
+ *
+ * @param flags the view flags to convert to a string
+ * @return a String representing the supplied flags
+ */
+ private static String printFlags(int flags) {
+ String output = "";
+ int numFlags = 0;
+ if ((flags & FOCUSABLE) == FOCUSABLE) {
+ output += "TAKES_FOCUS";
+ numFlags++;
+ }
+
+ switch (flags & VISIBILITY_MASK) {
+ case INVISIBLE:
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "INVISIBLE";
+ // USELESS HERE numFlags++;
+ break;
+ case GONE:
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "GONE";
+ // USELESS HERE numFlags++;
+ break;
+ default:
+ break;
+ }
+ return output;
+ }
+
+ /**
+ * Build a human readable string representation of the specified private
+ * view flags.
+ *
+ * @param privateFlags the private view flags to convert to a string
+ * @return a String representing the supplied flags
+ */
+ private static String printPrivateFlags(int privateFlags) {
+ String output = "";
+ int numFlags = 0;
+
+ if ((privateFlags & PFLAG_WANTS_FOCUS) == PFLAG_WANTS_FOCUS) {
+ output += "WANTS_FOCUS";
+ numFlags++;
+ }
+
+ if ((privateFlags & PFLAG_FOCUSED) == PFLAG_FOCUSED) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "FOCUSED";
+ numFlags++;
+ }
+
+ if ((privateFlags & PFLAG_SELECTED) == PFLAG_SELECTED) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "SELECTED";
+ numFlags++;
+ }
+
+ if ((privateFlags & PFLAG_IS_ROOT_NAMESPACE) == PFLAG_IS_ROOT_NAMESPACE) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "IS_ROOT_NAMESPACE";
+ numFlags++;
+ }
+
+ if ((privateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "HAS_BOUNDS";
+ numFlags++;
+ }
+
+ if ((privateFlags & PFLAG_DRAWN) == PFLAG_DRAWN) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "DRAWN";
+ // USELESS HERE numFlags++;
+ }
+ return output;
+ }
+
+ /**
+ * <p>Indicates whether or not this view's layout will be requested during
+ * the next hierarchy layout pass.</p>
+ *
+ * @return true if the layout will be forced during next layout pass
+ */
+ public boolean isLayoutRequested() {
+ return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
+ }
+
+ /**
+ * Return true if o is a ViewGroup that is laying out using optical bounds.
+ * @hide
+ */
+ public static boolean isLayoutModeOptical(Object o) {
+ return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
+ }
+
+ private boolean setOpticalFrame(int left, int top, int right, int bottom) {
+ Insets parentInsets = mParent instanceof View ?
+ ((View) mParent).getOpticalInsets() : Insets.NONE;
+ Insets childInsets = getOpticalInsets();
+ return setFrame(
+ left + parentInsets.left - childInsets.left,
+ top + parentInsets.top - childInsets.top,
+ right + parentInsets.left + childInsets.right,
+ bottom + parentInsets.top + childInsets.bottom);
+ }
+
+ /**
+ * Assign a size and position to a view and all of its
+ * descendants
+ *
+ * <p>This is the second phase of the layout mechanism.
+ * (The first is measuring). In this phase, each parent calls
+ * layout on all of its children to position them.
+ * This is typically done using the child measurements
+ * that were stored in the measure pass().</p>
+ *
+ * <p>Derived classes should not override this method.
+ * Derived classes with children should override
+ * onLayout. In that method, they should
+ * call layout on each of their children.</p>
+ *
+ * @param l Left position, relative to parent
+ * @param t Top position, relative to parent
+ * @param r Right position, relative to parent
+ * @param b Bottom position, relative to parent
+ */
+ @SuppressWarnings({"unchecked"})
+ public void layout(int l, int t, int r, int b) {
+ if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
+ onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
+ mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
+ }
+
+ int oldL = mLeft;
+ int oldT = mTop;
+ int oldB = mBottom;
+ int oldR = mRight;
+
+ boolean changed = isLayoutModeOptical(mParent) ?
+ setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
+
+ if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
+ onLayout(changed, l, t, r, b);
+
+ if (shouldDrawRoundScrollbar()) {
+ if(mRoundScrollbarRenderer == null) {
+ mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
+ }
+ } else {
+ mRoundScrollbarRenderer = null;
+ }
+
+ mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
+
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnLayoutChangeListeners != null) {
+ ArrayList<OnLayoutChangeListener> listenersCopy =
+ (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
+ int numListeners = listenersCopy.size();
+ for (int i = 0; i < numListeners; ++i) {
+ listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
+ }
+ }
+ }
+
+ final boolean wasLayoutValid = isLayoutValid();
+
+ mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
+ mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
+
+ if (!wasLayoutValid && isFocused()) {
+ mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ if (canTakeFocus()) {
+ // We have a robust focus, so parents should no longer be wanting focus.
+ clearParentsWantFocus();
+ } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
+ // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
+ // layout. In this case, there's no guarantee that parent layouts will be evaluated
+ // and thus the safest action is to clear focus here.
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ clearParentsWantFocus();
+ } else if (!hasParentWantsFocus()) {
+ // original requestFocus was likely on this view directly, so just clear focus
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ }
+ // otherwise, we let parents handle re-assigning focus during their layout passes.
+ } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
+ mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ View focused = findFocus();
+ if (focused != null) {
+ // Try to restore focus as close as possible to our starting focus.
+ if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
+ // Give up and clear focus once we've reached the top-most parent which wants
+ // focus.
+ focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ }
+ }
+ }
+
+ if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
+ mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
+ notifyEnterOrExitForAutoFillIfNeeded(true);
+ }
+
+ notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
+ }
+
+ private boolean hasParentWantsFocus() {
+ ViewParent parent = mParent;
+ while (parent instanceof ViewGroup) {
+ ViewGroup pv = (ViewGroup) parent;
+ if ((pv.mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
+ return true;
+ }
+ parent = pv.mParent;
+ }
+ return false;
+ }
+
+ /**
+ * Called from layout when this view should
+ * assign a size and position to each of its children.
+ *
+ * Derived classes with children should override
+ * this method and call layout on each of
+ * their children.
+ * @param changed This is a new size or position for this view
+ * @param left Left position, relative to parent
+ * @param top Top position, relative to parent
+ * @param right Right position, relative to parent
+ * @param bottom Bottom position, relative to parent
+ */
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * Assign a size and position to this view.
+ *
+ * This is called from layout.
+ *
+ * @param left Left position, relative to parent
+ * @param top Top position, relative to parent
+ * @param right Right position, relative to parent
+ * @param bottom Bottom position, relative to parent
+ * @return true if the new size and position are different than the
+ * previous ones
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ protected boolean setFrame(int left, int top, int right, int bottom) {
+ boolean changed = false;
+
+ if (DBG) {
+ Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ + right + "," + bottom + ")");
+ }
+
+ if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
+ changed = true;
+
+ // Remember our drawn bit
+ int drawn = mPrivateFlags & PFLAG_DRAWN;
+
+ int oldWidth = mRight - mLeft;
+ int oldHeight = mBottom - mTop;
+ int newWidth = right - left;
+ int newHeight = bottom - top;
+ boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
+
+ // Invalidate our old position
+ invalidate(sizeChanged);
+
+ mLeft = left;
+ mTop = top;
+ mRight = right;
+ mBottom = bottom;
+ mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
+
+ mPrivateFlags |= PFLAG_HAS_BOUNDS;
+
+
+ if (sizeChanged) {
+ sizeChange(newWidth, newHeight, oldWidth, oldHeight);
+ }
+
+ if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
+ // If we are visible, force the DRAWN bit to on so that
+ // this invalidate will go through (at least to our parent).
+ // This is because someone may have invalidated this view
+ // before this call to setFrame came in, thereby clearing
+ // the DRAWN bit.
+ mPrivateFlags |= PFLAG_DRAWN;
+ invalidate(sizeChanged);
+ // parent display list may need to be recreated based on a change in the bounds
+ // of any child
+ invalidateParentCaches();
+ }
+
+ // Reset drawn bit to original value (invalidate turns it off)
+ mPrivateFlags |= drawn;
+
+ mBackgroundSizeChanged = true;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (mForegroundInfo != null) {
+ mForegroundInfo.mBoundsChanged = true;
+ }
+
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+ return changed;
+ }
+
+ /**
+ * Assign a size and position to this view.
+ *
+ * This method is meant to be used in animations only as it applies this position and size
+ * for the view only temporary and it can be changed back at any time by the layout.
+ *
+ * @param left Left position, relative to parent
+ * @param top Top position, relative to parent
+ * @param right Right position, relative to parent
+ * @param bottom Bottom position, relative to parent
+ *
+ * @see #setLeft(int), #setRight(int), #setTop(int), #setBottom(int)
+ */
+ public final void setLeftTopRightBottom(int left, int top, int right, int bottom) {
+ setFrame(left, top, right, bottom);
+ }
+
+ private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
+ onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
+ if (mOverlay != null) {
+ mOverlay.getOverlayView().setRight(newWidth);
+ mOverlay.getOverlayView().setBottom(newHeight);
+ }
+ // If this isn't laid out yet, focus assignment will be handled during the "deferment/
+ // backtracking" of requestFocus during layout, so don't touch focus here.
+ if (!sCanFocusZeroSized && isLayoutValid()
+ // Don't touch focus if animating
+ && !(mParent instanceof ViewGroup && ((ViewGroup) mParent).isLayoutSuppressed())) {
+ if (newWidth <= 0 || newHeight <= 0) {
+ if (hasFocus()) {
+ clearFocus();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearFocusedInCluster();
+ }
+ }
+ clearAccessibilityFocus();
+ } else if (oldWidth <= 0 || oldHeight <= 0) {
+ if (mParent != null && canTakeFocus()) {
+ mParent.focusableViewAvailable(this);
+ }
+ }
+ }
+ rebuildOutline();
+ }
+
+ /**
+ * Finalize inflating a view from XML. This is called as the last phase
+ * of inflation, after all child views have been added.
+ *
+ * <p>Even if the subclass overrides onFinishInflate, they should always be
+ * sure to call the super method, so that we get called.
+ */
+ @CallSuper
+ protected void onFinishInflate() {
+ }
+
+ /**
+ * Returns the resources associated with this view.
+ *
+ * @return Resources object.
+ */
+ public Resources getResources() {
+ return mResources;
+ }
+
+ /**
+ * Invalidates the specified Drawable.
+ *
+ * @param drawable the drawable to invalidate
+ */
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ if (verifyDrawable(drawable)) {
+ final Rect dirty = drawable.getDirtyBounds();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+
+ invalidate(dirty.left + scrollX, dirty.top + scrollY,
+ dirty.right + scrollX, dirty.bottom + scrollY);
+ rebuildOutline();
+ }
+ }
+
+ /**
+ * Schedules an action on a drawable to occur at a specified time.
+ *
+ * @param who the recipient of the action
+ * @param what the action to run on the drawable
+ * @param when the time at which the action must occur. Uses the
+ * {@link SystemClock#uptimeMillis} timebase.
+ */
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ if (verifyDrawable(who) && what != null) {
+ final long delay = when - SystemClock.uptimeMillis();
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
+ Choreographer.CALLBACK_ANIMATION, what, who,
+ Choreographer.subtractFrameDelay(delay));
+ } else {
+ // Postpone the runnable until we know
+ // on which thread it needs to run.
+ getRunQueue().postDelayed(what, delay);
+ }
+ }
+ }
+
+ /**
+ * Cancels a scheduled action on a drawable.
+ *
+ * @param who the recipient of the action
+ * @param what the action to cancel
+ */
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ if (verifyDrawable(who) && what != null) {
+ if (mAttachInfo != null) {
+ mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_ANIMATION, what, who);
+ }
+ getRunQueue().removeCallbacks(what);
+ }
+ }
+
+ /**
+ * Unschedule any events associated with the given Drawable. This can be
+ * used when selecting a new Drawable into a view, so that the previous
+ * one is completely unscheduled.
+ *
+ * @param who The Drawable to unschedule.
+ *
+ * @see #drawableStateChanged
+ */
+ public void unscheduleDrawable(Drawable who) {
+ if (mAttachInfo != null && who != null) {
+ mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_ANIMATION, null, who);
+ }
+ }
+
+ /**
+ * Resolve the Drawables depending on the layout direction. This is implicitly supposing
+ * that the View directionality can and will be resolved before its Drawables.
+ *
+ * Will call {@link View#onResolveDrawables} when resolution is done.
+ *
+ * @hide
+ */
+ protected void resolveDrawables() {
+ // Drawables resolution may need to happen before resolving the layout direction (which is
+ // done only during the measure() call).
+ // If the layout direction is not resolved yet, we cannot resolve the Drawables except in
+ // one case: when the raw layout direction has not been defined as LAYOUT_DIRECTION_INHERIT.
+ // So, if the raw layout direction is LAYOUT_DIRECTION_LTR or LAYOUT_DIRECTION_RTL or
+ // LAYOUT_DIRECTION_LOCALE, we can "cheat" and we don't need to wait for the layout
+ // direction to be resolved as its resolved value will be the same as its raw value.
+ if (!isLayoutDirectionResolved() &&
+ getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT) {
+ return;
+ }
+
+ final int layoutDirection = isLayoutDirectionResolved() ?
+ getLayoutDirection() : getRawLayoutDirection();
+
+ if (mBackground != null) {
+ mBackground.setLayoutDirection(layoutDirection);
+ }
+ if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) {
+ mForegroundInfo.mDrawable.setLayoutDirection(layoutDirection);
+ }
+ if (mDefaultFocusHighlight != null) {
+ mDefaultFocusHighlight.setLayoutDirection(layoutDirection);
+ }
+ mPrivateFlags2 |= PFLAG2_DRAWABLE_RESOLVED;
+ onResolveDrawables(layoutDirection);
+ }
+
+ boolean areDrawablesResolved() {
+ return (mPrivateFlags2 & PFLAG2_DRAWABLE_RESOLVED) == PFLAG2_DRAWABLE_RESOLVED;
+ }
+
+ /**
+ * Called when layout direction has been resolved.
+ *
+ * The default implementation does nothing.
+ *
+ * @param layoutDirection The resolved layout direction.
+ *
+ * @see #LAYOUT_DIRECTION_LTR
+ * @see #LAYOUT_DIRECTION_RTL
+ *
+ * @hide
+ */
+ public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ protected void resetResolvedDrawables() {
+ resetResolvedDrawablesInternal();
+ }
+
+ void resetResolvedDrawablesInternal() {
+ mPrivateFlags2 &= ~PFLAG2_DRAWABLE_RESOLVED;
+ }
+
+ /**
+ * If your view subclass is displaying its own Drawable objects, it should
+ * override this function and return true for any Drawable it is
+ * displaying. This allows animations for those drawables to be
+ * scheduled.
+ *
+ * <p>Be sure to call through to the super class when overriding this
+ * function.
+ *
+ * @param who The Drawable to verify. Return true if it is one you are
+ * displaying, else return the result of calling through to the
+ * super class.
+ *
+ * @return boolean If true then the Drawable is being displayed in the
+ * view; else false and it is not allowed to animate.
+ *
+ * @see #unscheduleDrawable(android.graphics.drawable.Drawable)
+ * @see #drawableStateChanged()
+ */
+ @CallSuper
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ // Avoid verifying the scroll bar drawable so that we don't end up in
+ // an invalidation loop. This effectively prevents the scroll bar
+ // drawable from triggering invalidations and scheduling runnables.
+ return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who)
+ || (mDefaultFocusHighlight == who);
+ }
+
+ /**
+ * This function is called whenever the state of the view changes in such
+ * a way that it impacts the state of drawables being shown.
+ * <p>
+ * If the View has a StateListAnimator, it will also be called to run necessary state
+ * change animations.
+ * <p>
+ * Be sure to call through to the superclass when overriding this function.
+ *
+ * @see Drawable#setState(int[])
+ */
+ @CallSuper
+ protected void drawableStateChanged() {
+ final int[] state = getDrawableState();
+ boolean changed = false;
+
+ final Drawable bg = mBackground;
+ if (bg != null && bg.isStateful()) {
+ changed |= bg.setState(state);
+ }
+
+ final Drawable hl = mDefaultFocusHighlight;
+ if (hl != null && hl.isStateful()) {
+ changed |= hl.setState(state);
+ }
+
+ final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
+ if (fg != null && fg.isStateful()) {
+ changed |= fg.setState(state);
+ }
+
+ if (mScrollCache != null) {
+ final Drawable scrollBar = mScrollCache.scrollBar;
+ if (scrollBar != null && scrollBar.isStateful()) {
+ changed |= scrollBar.setState(state)
+ && mScrollCache.state != ScrollabilityCache.OFF;
+ }
+ }
+
+ if (mStateListAnimator != null) {
+ mStateListAnimator.setState(state);
+ }
+
+ if (!isAggregatedVisible()) {
+ // If we're not visible, skip any animated changes
+ jumpDrawablesToCurrentState();
+ }
+
+ if (changed) {
+ invalidate();
+ }
+ }
+
+ /**
+ * This function is called whenever the view hotspot changes and needs to
+ * be propagated to drawables or child views managed by the view.
+ * <p>
+ * Dispatching to child views is handled by
+ * {@link #dispatchDrawableHotspotChanged(float, float)}.
+ * <p>
+ * Be sure to call through to the superclass when overriding this function.
+ *
+ * @param x hotspot x coordinate
+ * @param y hotspot y coordinate
+ */
+ @CallSuper
+ public void drawableHotspotChanged(float x, float y) {
+ if (mBackground != null) {
+ mBackground.setHotspot(x, y);
+ }
+ if (mDefaultFocusHighlight != null) {
+ mDefaultFocusHighlight.setHotspot(x, y);
+ }
+ if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) {
+ mForegroundInfo.mDrawable.setHotspot(x, y);
+ }
+
+ dispatchDrawableHotspotChanged(x, y);
+ }
+
+ /**
+ * Dispatches drawableHotspotChanged to all of this View's children.
+ *
+ * @param x hotspot x coordinate
+ * @param y hotspot y coordinate
+ * @see #drawableHotspotChanged(float, float)
+ */
+ public void dispatchDrawableHotspotChanged(float x, float y) {
+ }
+
+ /**
+ * Call this to force a view to update its drawable state. This will cause
+ * drawableStateChanged to be called on this view. Views that are interested
+ * in the new state should call getDrawableState.
+ *
+ * @see #drawableStateChanged
+ * @see #getDrawableState
+ */
+ public void refreshDrawableState() {
+ mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
+ drawableStateChanged();
+
+ ViewParent parent = mParent;
+ if (parent != null) {
+ parent.childDrawableStateChanged(this);
+ }
+ }
+
+ /**
+ * Create a default focus highlight if it doesn't exist.
+ * @return a default focus highlight.
+ */
+ private Drawable getDefaultFocusHighlightDrawable() {
+ if (mDefaultFocusHighlightCache == null) {
+ if (mContext != null) {
+ final int[] attrs = new int[] { android.R.attr.selectableItemBackground };
+ final TypedArray ta = mContext.obtainStyledAttributes(attrs);
+ mDefaultFocusHighlightCache = ta.getDrawable(0);
+ ta.recycle();
+ }
+ }
+ return mDefaultFocusHighlightCache;
+ }
+
+ /**
+ * Set the current default focus highlight.
+ * @param highlight the highlight drawable, or {@code null} if it's no longer needed.
+ */
+ private void setDefaultFocusHighlight(Drawable highlight) {
+ mDefaultFocusHighlight = highlight;
+ mDefaultFocusHighlightSizeChanged = true;
+ if (highlight != null) {
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
+ mPrivateFlags &= ~PFLAG_SKIP_DRAW;
+ }
+ highlight.setLayoutDirection(getLayoutDirection());
+ if (highlight.isStateful()) {
+ highlight.setState(getDrawableState());
+ }
+ if (isAttachedToWindow()) {
+ highlight.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
+ }
+ // Set callback last, since the view may still be initializing.
+ highlight.setCallback(this);
+ } else if ((mViewFlags & WILL_NOT_DRAW) != 0 && mBackground == null
+ && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
+ mPrivateFlags |= PFLAG_SKIP_DRAW;
+ }
+ invalidate();
+ }
+
+ /**
+ * Check whether we need to draw a default focus highlight when this view gets focused,
+ * which requires:
+ * <ul>
+ * <li>In both background and foreground, {@link android.R.attr#state_focused}
+ * is not defined.</li>
+ * <li>This view is not in touch mode.</li>
+ * <li>This view doesn't opt out for a default focus highlight, via
+ * {@link #setDefaultFocusHighlightEnabled(boolean)}.</li>
+ * <li>This view is attached to window.</li>
+ * </ul>
+ * @return {@code true} if a default focus highlight is needed.
+ * @hide
+ */
+ @TestApi
+ public boolean isDefaultFocusHighlightNeeded(Drawable background, Drawable foreground) {
+ final boolean lackFocusState = (background == null || !background.isStateful()
+ || !background.hasFocusStateSpecified())
+ && (foreground == null || !foreground.isStateful()
+ || !foreground.hasFocusStateSpecified());
+ return !isInTouchMode() && getDefaultFocusHighlightEnabled() && lackFocusState
+ && isAttachedToWindow() && sUseDefaultFocusHighlight;
+ }
+
+ /**
+ * When this view is focused, switches on/off the default focused highlight.
+ * <p>
+ * This always happens when this view is focused, and only at this moment the default focus
+ * highlight can be visible.
+ */
+ private void switchDefaultFocusHighlight() {
+ if (isFocused()) {
+ final boolean needed = isDefaultFocusHighlightNeeded(mBackground,
+ mForegroundInfo == null ? null : mForegroundInfo.mDrawable);
+ final boolean active = mDefaultFocusHighlight != null;
+ if (needed && !active) {
+ setDefaultFocusHighlight(getDefaultFocusHighlightDrawable());
+ } else if (!needed && active) {
+ // The highlight is no longer needed, so tear it down.
+ setDefaultFocusHighlight(null);
+ }
+ }
+ }
+
+ /**
+ * Draw the default focus highlight onto the canvas if there is one and this view is focused.
+ * @param canvas the canvas where we're drawing the highlight.
+ */
+ private void drawDefaultFocusHighlight(Canvas canvas) {
+ if (mDefaultFocusHighlight != null && isFocused()) {
+ if (mDefaultFocusHighlightSizeChanged) {
+ mDefaultFocusHighlightSizeChanged = false;
+ final int l = mScrollX;
+ final int r = l + mRight - mLeft;
+ final int t = mScrollY;
+ final int b = t + mBottom - mTop;
+ mDefaultFocusHighlight.setBounds(l, t, r, b);
+ }
+ mDefaultFocusHighlight.draw(canvas);
+ }
+ }
+
+ /**
+ * Return an array of resource IDs of the drawable states representing the
+ * current state of the view.
+ *
+ * @return The current drawable state
+ *
+ * @see Drawable#setState(int[])
+ * @see #drawableStateChanged()
+ * @see #onCreateDrawableState(int)
+ */
+ public final int[] getDrawableState() {
+ if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
+ return mDrawableState;
+ } else {
+ mDrawableState = onCreateDrawableState(0);
+ mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
+ return mDrawableState;
+ }
+ }
+
+ /**
+ * Generate the new {@link android.graphics.drawable.Drawable} state for
+ * this view. This is called by the view
+ * system when the cached Drawable state is determined to be invalid. To
+ * retrieve the current state, you should use {@link #getDrawableState}.
+ *
+ * @param extraSpace if non-zero, this is the number of extra entries you
+ * would like in the returned array in which you can place your own
+ * states.
+ *
+ * @return Returns an array holding the current {@link Drawable} state of
+ * the view.
+ *
+ * @see #mergeDrawableStates(int[], int[])
+ */
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE &&
+ mParent instanceof View) {
+ return ((View) mParent).onCreateDrawableState(extraSpace);
+ }
+
+ int[] drawableState;
+
+ int privateFlags = mPrivateFlags;
+
+ int viewStateIndex = 0;
+ if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED;
+ if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED;
+ if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED;
+ if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED;
+ if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED;
+ if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED;
+ if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested) {
+ // This is set if HW acceleration is requested, even if the current
+ // process doesn't allow it. This is just to allow app preview
+ // windows to better match their app.
+ viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED;
+ }
+ if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED;
+
+ final int privateFlags2 = mPrivateFlags2;
+ if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) {
+ viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT;
+ }
+ if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) {
+ viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED;
+ }
+
+ drawableState = StateSet.get(viewStateIndex);
+
+ //noinspection ConstantIfStatement
+ if (false) {
+ Log.i("View", "drawableStateIndex=" + viewStateIndex);
+ Log.i("View", toString()
+ + " pressed=" + ((privateFlags & PFLAG_PRESSED) != 0)
+ + " en=" + ((mViewFlags & ENABLED_MASK) == ENABLED)
+ + " fo=" + hasFocus()
+ + " sl=" + ((privateFlags & PFLAG_SELECTED) != 0)
+ + " wf=" + hasWindowFocus()
+ + ": " + Arrays.toString(drawableState));
+ }
+
+ if (extraSpace == 0) {
+ return drawableState;
+ }
+
+ final int[] fullState;
+ if (drawableState != null) {
+ fullState = new int[drawableState.length + extraSpace];
+ System.arraycopy(drawableState, 0, fullState, 0, drawableState.length);
+ } else {
+ fullState = new int[extraSpace];
+ }
+
+ return fullState;
+ }
+
+ /**
+ * Merge your own state values in <var>additionalState</var> into the base
+ * state values <var>baseState</var> that were returned by
+ * {@link #onCreateDrawableState(int)}.
+ *
+ * @param baseState The base state values returned by
+ * {@link #onCreateDrawableState(int)}, which will be modified to also hold your
+ * own additional state values.
+ *
+ * @param additionalState The additional state values you would like
+ * added to <var>baseState</var>; this array is not modified.
+ *
+ * @return As a convenience, the <var>baseState</var> array you originally
+ * passed into the function is returned.
+ *
+ * @see #onCreateDrawableState(int)
+ */
+ protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState) {
+ final int N = baseState.length;
+ int i = N - 1;
+ while (i >= 0 && baseState[i] == 0) {
+ i--;
+ }
+ System.arraycopy(additionalState, 0, baseState, i + 1, additionalState.length);
+ return baseState;
+ }
+
+ /**
+ * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}
+ * on all Drawable objects associated with this view.
+ * <p>
+ * Also calls {@link StateListAnimator#jumpToCurrentState()} if there is a StateListAnimator
+ * attached to this view.
+ */
+ @CallSuper
+ public void jumpDrawablesToCurrentState() {
+ if (mBackground != null) {
+ mBackground.jumpToCurrentState();
+ }
+ if (mStateListAnimator != null) {
+ mStateListAnimator.jumpToCurrentState();
+ }
+ if (mDefaultFocusHighlight != null) {
+ mDefaultFocusHighlight.jumpToCurrentState();
+ }
+ if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) {
+ mForegroundInfo.mDrawable.jumpToCurrentState();
+ }
+ }
+
+ /**
+ * Sets the background color for this view.
+ * @param color the color of the background
+ */
+ @RemotableViewMethod
+ public void setBackgroundColor(@ColorInt int color) {
+ if (mBackground instanceof ColorDrawable) {
+ ((ColorDrawable) mBackground.mutate()).setColor(color);
+ computeOpaqueFlags();
+ mBackgroundResource = 0;
+ } else {
+ setBackground(new ColorDrawable(color));
+ }
+ }
+
+ /**
+ * Set the background to a given resource. The resource should refer to
+ * a Drawable object or 0 to remove the background.
+ * @param resid The identifier of the resource.
+ *
+ * @attr ref android.R.styleable#View_background
+ */
+ @RemotableViewMethod
+ public void setBackgroundResource(@DrawableRes int resid) {
+ if (resid != 0 && resid == mBackgroundResource) {
+ return;
+ }
+
+ Drawable d = null;
+ if (resid != 0) {
+ d = mContext.getDrawable(resid);
+ }
+ setBackground(d);
+
+ mBackgroundResource = resid;
+ }
+
+ /**
+ * Set the background to a given Drawable, or remove the background. If the
+ * background has padding, this View's padding is set to the background's
+ * padding. However, when a background is removed, this View's padding isn't
+ * touched. If setting the padding is desired, please use
+ * {@link #setPadding(int, int, int, int)}.
+ *
+ * @param background The Drawable to use as the background, or null to remove the
+ * background
+ */
+ public void setBackground(Drawable background) {
+ //noinspection deprecation
+ setBackgroundDrawable(background);
+ }
+
+ /**
+ * @deprecated use {@link #setBackground(Drawable)} instead
+ */
+ @Deprecated
+ public void setBackgroundDrawable(Drawable background) {
+ computeOpaqueFlags();
+
+ if (background == mBackground) {
+ return;
+ }
+
+ boolean requestLayout = false;
+
+ mBackgroundResource = 0;
+
+ /*
+ * Regardless of whether we're setting a new background or not, we want
+ * to clear the previous drawable. setVisible first while we still have the callback set.
+ */
+ if (mBackground != null) {
+ if (isAttachedToWindow()) {
+ mBackground.setVisible(false, false);
+ }
+ mBackground.setCallback(null);
+ unscheduleDrawable(mBackground);
+ }
+
+ if (background != null) {
+ Rect padding = sThreadLocal.get();
+ if (padding == null) {
+ padding = new Rect();
+ sThreadLocal.set(padding);
+ }
+ resetResolvedDrawablesInternal();
+ background.setLayoutDirection(getLayoutDirection());
+ if (background.getPadding(padding)) {
+ resetResolvedPaddingInternal();
+ switch (background.getLayoutDirection()) {
+ case LAYOUT_DIRECTION_RTL:
+ mUserPaddingLeftInitial = padding.right;
+ mUserPaddingRightInitial = padding.left;
+ internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
+ break;
+ case LAYOUT_DIRECTION_LTR:
+ default:
+ mUserPaddingLeftInitial = padding.left;
+ mUserPaddingRightInitial = padding.right;
+ internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
+ }
+ mLeftPaddingDefined = false;
+ mRightPaddingDefined = false;
+ }
+
+ // Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
+ // if it has a different minimum size, we should layout again
+ if (mBackground == null
+ || mBackground.getMinimumHeight() != background.getMinimumHeight()
+ || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
+ requestLayout = true;
+ }
+
+ // Set mBackground before we set this as the callback and start making other
+ // background drawable state change calls. In particular, the setVisible call below
+ // can result in drawables attempting to start animations or otherwise invalidate,
+ // which requires the view set as the callback (us) to recognize the drawable as
+ // belonging to it as per verifyDrawable.
+ mBackground = background;
+ if (background.isStateful()) {
+ background.setState(getDrawableState());
+ }
+ if (isAttachedToWindow()) {
+ background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
+ }
+
+ applyBackgroundTint();
+
+ // Set callback last, since the view may still be initializing.
+ background.setCallback(this);
+
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
+ mPrivateFlags &= ~PFLAG_SKIP_DRAW;
+ requestLayout = true;
+ }
+ } else {
+ /* Remove the background */
+ mBackground = null;
+ if ((mViewFlags & WILL_NOT_DRAW) != 0
+ && (mDefaultFocusHighlight == null)
+ && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
+ mPrivateFlags |= PFLAG_SKIP_DRAW;
+ }
+
+ /*
+ * When the background is set, we try to apply its padding to this
+ * View. When the background is removed, we don't touch this View's
+ * padding. This is noted in the Javadocs. Hence, we don't need to
+ * requestLayout(), the invalidate() below is sufficient.
+ */
+
+ // The old background's minimum size could have affected this
+ // View's layout, so let's requestLayout
+ requestLayout = true;
+ }
+
+ computeOpaqueFlags();
+
+ if (requestLayout) {
+ requestLayout();
+ }
+
+ mBackgroundSizeChanged = true;
+ invalidate(true);
+ invalidateOutline();
+ }
+
+ /**
+ * Gets the background drawable
+ *
+ * @return The drawable used as the background for this view, if any.
+ *
+ * @see #setBackground(Drawable)
+ *
+ * @attr ref android.R.styleable#View_background
+ */
+ @InspectableProperty
+ public Drawable getBackground() {
+ return mBackground;
+ }
+
+ /**
+ * Applies a tint to the background drawable. Does not modify the current tint
+ * mode, which is {@link BlendMode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setBackground(Drawable)} will automatically
+ * mutate the drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#View_backgroundTint
+ * @see #getBackgroundTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ @RemotableViewMethod
+ public void setBackgroundTintList(@Nullable ColorStateList tint) {
+ if (mBackgroundTint == null) {
+ mBackgroundTint = new TintInfo();
+ }
+ mBackgroundTint.mTintList = tint;
+ mBackgroundTint.mHasTintList = true;
+
+ applyBackgroundTint();
+ }
+
+ /**
+ * Return the tint applied to the background drawable, if specified.
+ *
+ * @return the tint applied to the background drawable
+ * @attr ref android.R.styleable#View_backgroundTint
+ * @see #setBackgroundTintList(ColorStateList)
+ */
+ @InspectableProperty(name = "backgroundTint")
+ @Nullable
+ public ColorStateList getBackgroundTintList() {
+ return mBackgroundTint != null ? mBackgroundTint.mTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setBackgroundTintList(ColorStateList)}} to the background
+ * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#View_backgroundTintMode
+ * @see #getBackgroundTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
+ BlendMode mode = null;
+ if (tintMode != null) {
+ mode = BlendMode.fromValue(tintMode.nativeInt);
+ }
+
+ setBackgroundTintBlendMode(mode);
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setBackgroundTintList(ColorStateList)}} to the background
+ * drawable. The default mode is {@link BlendMode#SRC_IN}.
+ *
+ * @param blendMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#View_backgroundTintMode
+ * @see #getBackgroundTintMode()
+ * @see Drawable#setTintBlendMode(BlendMode)
+ */
+ @RemotableViewMethod
+ public void setBackgroundTintBlendMode(@Nullable BlendMode blendMode) {
+ if (mBackgroundTint == null) {
+ mBackgroundTint = new TintInfo();
+ }
+
+ mBackgroundTint.mBlendMode = blendMode;
+ mBackgroundTint.mHasTintMode = true;
+
+ applyBackgroundTint();
+ }
+
+ /**
+ * Return the blending mode used to apply the tint to the background
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the background
+ * drawable
+ * @attr ref android.R.styleable#View_backgroundTintMode
+ * @see #setBackgroundTintBlendMode(BlendMode)
+ *
+ */
+ @Nullable
+ @InspectableProperty
+ public PorterDuff.Mode getBackgroundTintMode() {
+ PorterDuff.Mode porterDuffMode;
+ if (mBackgroundTint != null && mBackgroundTint.mBlendMode != null) {
+ porterDuffMode = BlendMode.blendModeToPorterDuffMode(mBackgroundTint.mBlendMode);
+ } else {
+ porterDuffMode = null;
+ }
+ return porterDuffMode;
+ }
+
+ /**
+ * Return the blending mode used to apply the tint to the background
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the background
+ * drawable, null if no blend has previously been configured
+ * @attr ref android.R.styleable#View_backgroundTintMode
+ * @see #setBackgroundTintBlendMode(BlendMode)
+ */
+ public @Nullable BlendMode getBackgroundTintBlendMode() {
+ return mBackgroundTint != null ? mBackgroundTint.mBlendMode : null;
+ }
+
+ private void applyBackgroundTint() {
+ if (mBackground != null && mBackgroundTint != null) {
+ final TintInfo tintInfo = mBackgroundTint;
+ if (tintInfo.mHasTintList || tintInfo.mHasTintMode) {
+ mBackground = mBackground.mutate();
+
+ if (tintInfo.mHasTintList) {
+ mBackground.setTintList(tintInfo.mTintList);
+ }
+
+ if (tintInfo.mHasTintMode) {
+ mBackground.setTintBlendMode(tintInfo.mBlendMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mBackground.isStateful()) {
+ mBackground.setState(getDrawableState());
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the drawable used as the foreground of this View. The
+ * foreground drawable, if non-null, is always drawn on top of the view's content.
+ *
+ * @return a Drawable or null if no foreground was set
+ *
+ * @see #onDrawForeground(Canvas)
+ */
+ @InspectableProperty
+ public Drawable getForeground() {
+ return mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
+ }
+
+ /**
+ * Supply a Drawable that is to be rendered on top of all of the content in the view.
+ *
+ * @param foreground the Drawable to be drawn on top of the children
+ *
+ * @attr ref android.R.styleable#View_foreground
+ */
+ public void setForeground(Drawable foreground) {
+ if (mForegroundInfo == null) {
+ if (foreground == null) {
+ // Nothing to do.
+ return;
+ }
+ mForegroundInfo = new ForegroundInfo();
+ }
+
+ if (foreground == mForegroundInfo.mDrawable) {
+ // Nothing to do
+ return;
+ }
+
+ if (mForegroundInfo.mDrawable != null) {
+ if (isAttachedToWindow()) {
+ mForegroundInfo.mDrawable.setVisible(false, false);
+ }
+ mForegroundInfo.mDrawable.setCallback(null);
+ unscheduleDrawable(mForegroundInfo.mDrawable);
+ }
+
+ mForegroundInfo.mDrawable = foreground;
+ mForegroundInfo.mBoundsChanged = true;
+ if (foreground != null) {
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
+ mPrivateFlags &= ~PFLAG_SKIP_DRAW;
+ }
+ foreground.setLayoutDirection(getLayoutDirection());
+ if (foreground.isStateful()) {
+ foreground.setState(getDrawableState());
+ }
+ applyForegroundTint();
+ if (isAttachedToWindow()) {
+ foreground.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
+ }
+ // Set callback last, since the view may still be initializing.
+ foreground.setCallback(this);
+ } else if ((mViewFlags & WILL_NOT_DRAW) != 0 && mBackground == null
+ && (mDefaultFocusHighlight == null)) {
+ mPrivateFlags |= PFLAG_SKIP_DRAW;
+ }
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Magic bit used to support features of framework-internal window decor implementation details.
+ * This used to live exclusively in FrameLayout.
+ *
+ * @return true if the foreground should draw inside the padding region or false
+ * if it should draw inset by the view's padding
+ * @hide internal use only; only used by FrameLayout and internal screen layouts.
+ */
+ public boolean isForegroundInsidePadding() {
+ return mForegroundInfo != null ? mForegroundInfo.mInsidePadding : true;
+ }
+
+ /**
+ * Describes how the foreground is positioned.
+ *
+ * @return foreground gravity.
+ *
+ * @see #setForegroundGravity(int)
+ *
+ * @attr ref android.R.styleable#View_foregroundGravity
+ */
+ @InspectableProperty(valueType = InspectableProperty.ValueType.GRAVITY)
+ public int getForegroundGravity() {
+ return mForegroundInfo != null ? mForegroundInfo.mGravity
+ : Gravity.START | Gravity.TOP;
+ }
+
+ /**
+ * Describes how the foreground is positioned. Defaults to START and TOP.
+ *
+ * @param gravity see {@link android.view.Gravity}
+ *
+ * @see #getForegroundGravity()
+ *
+ * @attr ref android.R.styleable#View_foregroundGravity
+ */
+ public void setForegroundGravity(int gravity) {
+ if (mForegroundInfo == null) {
+ mForegroundInfo = new ForegroundInfo();
+ }
+
+ if (mForegroundInfo.mGravity != gravity) {
+ if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.START;
+ }
+
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ mForegroundInfo.mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Applies a tint to the foreground drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setForeground(Drawable)} will automatically
+ * mutate the drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#View_foregroundTint
+ * @see #getForegroundTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ @RemotableViewMethod
+ public void setForegroundTintList(@Nullable ColorStateList tint) {
+ if (mForegroundInfo == null) {
+ mForegroundInfo = new ForegroundInfo();
+ }
+ if (mForegroundInfo.mTintInfo == null) {
+ mForegroundInfo.mTintInfo = new TintInfo();
+ }
+ mForegroundInfo.mTintInfo.mTintList = tint;
+ mForegroundInfo.mTintInfo.mHasTintList = true;
+
+ applyForegroundTint();
+ }
+
+ /**
+ * Return the tint applied to the foreground drawable, if specified.
+ *
+ * @return the tint applied to the foreground drawable
+ * @attr ref android.R.styleable#View_foregroundTint
+ * @see #setForegroundTintList(ColorStateList)
+ */
+ @InspectableProperty(name = "foregroundTint")
+ @Nullable
+ public ColorStateList getForegroundTintList() {
+ return mForegroundInfo != null && mForegroundInfo.mTintInfo != null
+ ? mForegroundInfo.mTintInfo.mTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setForegroundTintList(ColorStateList)}} to the background
+ * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#View_foregroundTintMode
+ * @see #getForegroundTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ *
+ */
+ public void setForegroundTintMode(@Nullable PorterDuff.Mode tintMode) {
+ BlendMode mode = null;
+ if (tintMode != null) {
+ mode = BlendMode.fromValue(tintMode.nativeInt);
+ }
+ setForegroundTintBlendMode(mode);
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setForegroundTintList(ColorStateList)}} to the background
+ * drawable. The default mode is {@link BlendMode#SRC_IN}.
+ *
+ * @param blendMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#View_foregroundTintMode
+ * @see #getForegroundTintMode()
+ * @see Drawable#setTintBlendMode(BlendMode)
+ */
+ @RemotableViewMethod
+ public void setForegroundTintBlendMode(@Nullable BlendMode blendMode) {
+ if (mForegroundInfo == null) {
+ mForegroundInfo = new ForegroundInfo();
+ }
+ if (mForegroundInfo.mTintInfo == null) {
+ mForegroundInfo.mTintInfo = new TintInfo();
+ }
+ mForegroundInfo.mTintInfo.mBlendMode = blendMode;
+ mForegroundInfo.mTintInfo.mHasTintMode = true;
+
+ applyForegroundTint();
+ }
+
+ /**
+ * Return the blending mode used to apply the tint to the foreground
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the foreground
+ * drawable
+ * @attr ref android.R.styleable#View_foregroundTintMode
+ * @see #setForegroundTintMode(PorterDuff.Mode)
+ */
+ @InspectableProperty
+ @Nullable
+ public PorterDuff.Mode getForegroundTintMode() {
+ BlendMode blendMode = mForegroundInfo != null && mForegroundInfo.mTintInfo != null
+ ? mForegroundInfo.mTintInfo.mBlendMode : null;
+ if (blendMode != null) {
+ return BlendMode.blendModeToPorterDuffMode(blendMode);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the blending mode used to apply the tint to the foreground
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the foreground
+ * drawable
+ * @attr ref android.R.styleable#View_foregroundTintMode
+ * @see #setForegroundTintBlendMode(BlendMode)
+ *
+ */
+ public @Nullable BlendMode getForegroundTintBlendMode() {
+ return mForegroundInfo != null && mForegroundInfo.mTintInfo != null
+ ? mForegroundInfo.mTintInfo.mBlendMode : null;
+ }
+
+ private void applyForegroundTint() {
+ if (mForegroundInfo != null && mForegroundInfo.mDrawable != null
+ && mForegroundInfo.mTintInfo != null) {
+ final TintInfo tintInfo = mForegroundInfo.mTintInfo;
+ if (tintInfo.mHasTintList || tintInfo.mHasTintMode) {
+ mForegroundInfo.mDrawable = mForegroundInfo.mDrawable.mutate();
+
+ if (tintInfo.mHasTintList) {
+ mForegroundInfo.mDrawable.setTintList(tintInfo.mTintList);
+ }
+
+ if (tintInfo.mHasTintMode) {
+ mForegroundInfo.mDrawable.setTintBlendMode(tintInfo.mBlendMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mForegroundInfo.mDrawable.isStateful()) {
+ mForegroundInfo.mDrawable.setState(getDrawableState());
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the drawable to be overlayed when a view is autofilled
+ *
+ * @return The drawable
+ *
+ * @throws IllegalStateException if the drawable could not be found.
+ */
+ @Nullable private Drawable getAutofilledDrawable() {
+ if (mAttachInfo == null) {
+ return null;
+ }
+ // Lazily load the isAutofilled drawable.
+ if (mAttachInfo.mAutofilledDrawable == null) {
+ Context rootContext = getRootView().getContext();
+ TypedArray a = rootContext.getTheme().obtainStyledAttributes(AUTOFILL_HIGHLIGHT_ATTR);
+ int attributeResourceId = a.getResourceId(0, 0);
+ mAttachInfo.mAutofilledDrawable = rootContext.getDrawable(attributeResourceId);
+ a.recycle();
+ }
+
+ return mAttachInfo.mAutofilledDrawable;
+ }
+
+ /**
+ * Draw {@link View#isAutofilled()} highlight over view if the view is autofilled, unless
+ * {@link #PFLAG4_AUTOFILL_HIDE_HIGHLIGHT} is enabled.
+ *
+ * @param canvas The canvas to draw on
+ */
+ private void drawAutofilledHighlight(@NonNull Canvas canvas) {
+ if (isAutofilled() && !hideAutofillHighlight()) {
+ Drawable autofilledHighlight = getAutofilledDrawable();
+
+ if (autofilledHighlight != null) {
+ autofilledHighlight.setBounds(0, 0, getWidth(), getHeight());
+ autofilledHighlight.draw(canvas);
+ }
+ }
+ }
+
+ /**
+ * Draw any foreground content for this view.
+ *
+ * <p>Foreground content may consist of scroll bars, a {@link #setForeground foreground}
+ * drawable or other view-specific decorations. The foreground is drawn on top of the
+ * primary view content.</p>
+ *
+ * @param canvas canvas to draw into
+ */
+ public void onDrawForeground(Canvas canvas) {
+ onDrawScrollIndicators(canvas);
+ onDrawScrollBars(canvas);
+
+ final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
+ if (foreground != null) {
+ if (mForegroundInfo.mBoundsChanged) {
+ mForegroundInfo.mBoundsChanged = false;
+ final Rect selfBounds = mForegroundInfo.mSelfBounds;
+ final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
+
+ if (mForegroundInfo.mInsidePadding) {
+ selfBounds.set(0, 0, getWidth(), getHeight());
+ } else {
+ selfBounds.set(getPaddingLeft(), getPaddingTop(),
+ getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
+ }
+
+ final int ld = getLayoutDirection();
+ Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
+ foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
+ foreground.setBounds(overlayBounds);
+ }
+
+ foreground.draw(canvas);
+ }
+ }
+
+ /**
+ * Sets the padding. The view may add on the space required to display
+ * the scrollbars, depending on the style and visibility of the scrollbars.
+ * So the values returned from {@link #getPaddingLeft}, {@link #getPaddingTop},
+ * {@link #getPaddingRight} and {@link #getPaddingBottom} may be different
+ * from the values set in this call.
+ *
+ * @attr ref android.R.styleable#View_padding
+ * @attr ref android.R.styleable#View_paddingBottom
+ * @attr ref android.R.styleable#View_paddingLeft
+ * @attr ref android.R.styleable#View_paddingRight
+ * @attr ref android.R.styleable#View_paddingTop
+ * @param left the left padding in pixels
+ * @param top the top padding in pixels
+ * @param right the right padding in pixels
+ * @param bottom the bottom padding in pixels
+ */
+ public void setPadding(int left, int top, int right, int bottom) {
+ resetResolvedPaddingInternal();
+
+ mUserPaddingStart = UNDEFINED_PADDING;
+ mUserPaddingEnd = UNDEFINED_PADDING;
+
+ mUserPaddingLeftInitial = left;
+ mUserPaddingRightInitial = right;
+
+ mLeftPaddingDefined = true;
+ mRightPaddingDefined = true;
+
+ internalSetPadding(left, top, right, bottom);
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768420)
+ protected void internalSetPadding(int left, int top, int right, int bottom) {
+ mUserPaddingLeft = left;
+ mUserPaddingRight = right;
+ mUserPaddingBottom = bottom;
+
+ final int viewFlags = mViewFlags;
+ boolean changed = false;
+
+ // Common case is there are no scroll bars.
+ if ((viewFlags & (SCROLLBARS_VERTICAL|SCROLLBARS_HORIZONTAL)) != 0) {
+ if ((viewFlags & SCROLLBARS_VERTICAL) != 0) {
+ final int offset = (viewFlags & SCROLLBARS_INSET_MASK) == 0
+ ? 0 : getVerticalScrollbarWidth();
+ switch (mVerticalScrollbarPosition) {
+ case SCROLLBAR_POSITION_DEFAULT:
+ if (isLayoutRtl()) {
+ left += offset;
+ } else {
+ right += offset;
+ }
+ break;
+ case SCROLLBAR_POSITION_RIGHT:
+ right += offset;
+ break;
+ case SCROLLBAR_POSITION_LEFT:
+ left += offset;
+ break;
+ }
+ }
+ if ((viewFlags & SCROLLBARS_HORIZONTAL) != 0) {
+ bottom += (viewFlags & SCROLLBARS_INSET_MASK) == 0
+ ? 0 : getHorizontalScrollbarHeight();
+ }
+ }
+
+ if (mPaddingLeft != left) {
+ changed = true;
+ mPaddingLeft = left;
+ }
+ if (mPaddingTop != top) {
+ changed = true;
+ mPaddingTop = top;
+ }
+ if (mPaddingRight != right) {
+ changed = true;
+ mPaddingRight = right;
+ }
+ if (mPaddingBottom != bottom) {
+ changed = true;
+ mPaddingBottom = bottom;
+ }
+
+ if (changed) {
+ requestLayout();
+ invalidateOutline();
+ }
+ }
+
+ /**
+ * Sets the relative padding. The view may add on the space required to display
+ * the scrollbars, depending on the style and visibility of the scrollbars.
+ * So the values returned from {@link #getPaddingStart}, {@link #getPaddingTop},
+ * {@link #getPaddingEnd} and {@link #getPaddingBottom} may be different
+ * from the values set in this call.
+ *
+ * @attr ref android.R.styleable#View_padding
+ * @attr ref android.R.styleable#View_paddingBottom
+ * @attr ref android.R.styleable#View_paddingStart
+ * @attr ref android.R.styleable#View_paddingEnd
+ * @attr ref android.R.styleable#View_paddingTop
+ * @param start the start padding in pixels
+ * @param top the top padding in pixels
+ * @param end the end padding in pixels
+ * @param bottom the bottom padding in pixels
+ */
+ public void setPaddingRelative(int start, int top, int end, int bottom) {
+ resetResolvedPaddingInternal();
+
+ mUserPaddingStart = start;
+ mUserPaddingEnd = end;
+ mLeftPaddingDefined = true;
+ mRightPaddingDefined = true;
+
+ switch(getLayoutDirection()) {
+ case LAYOUT_DIRECTION_RTL:
+ mUserPaddingLeftInitial = end;
+ mUserPaddingRightInitial = start;
+ internalSetPadding(end, top, start, bottom);
+ break;
+ case LAYOUT_DIRECTION_LTR:
+ default:
+ mUserPaddingLeftInitial = start;
+ mUserPaddingRightInitial = end;
+ internalSetPadding(start, top, end, bottom);
+ }
+ }
+
+ /**
+ * A {@link View} can be inflated from an XML layout. For such Views this method returns the
+ * resource ID of the source layout.
+ *
+ * @return The layout resource id if this view was inflated from XML, otherwise
+ * {@link Resources#ID_NULL}.
+ */
+ @LayoutRes
+ public int getSourceLayoutResId() {
+ return mSourceLayoutId;
+ }
+
+ /**
+ * Returns the top padding of this view.
+ *
+ * @return the top padding in pixels
+ */
+ @InspectableProperty
+ public int getPaddingTop() {
+ return mPaddingTop;
+ }
+
+ /**
+ * Returns the bottom padding of this view. If there are inset and enabled
+ * scrollbars, this value may include the space required to display the
+ * scrollbars as well.
+ *
+ * @return the bottom padding in pixels
+ */
+ @InspectableProperty
+ public int getPaddingBottom() {
+ return mPaddingBottom;
+ }
+
+ /**
+ * Returns the left padding of this view. If there are inset and enabled
+ * scrollbars, this value may include the space required to display the
+ * scrollbars as well.
+ *
+ * @return the left padding in pixels
+ */
+ @InspectableProperty
+ public int getPaddingLeft() {
+ if (!isPaddingResolved()) {
+ resolvePadding();
+ }
+ return mPaddingLeft;
+ }
+
+ /**
+ * Returns the start padding of this view depending on its resolved layout direction.
+ * If there are inset and enabled scrollbars, this value may include the space
+ * required to display the scrollbars as well.
+ *
+ * @return the start padding in pixels
+ */
+ public int getPaddingStart() {
+ if (!isPaddingResolved()) {
+ resolvePadding();
+ }
+ return (getLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
+ mPaddingRight : mPaddingLeft;
+ }
+
+ /**
+ * Returns the right padding of this view. If there are inset and enabled
+ * scrollbars, this value may include the space required to display the
+ * scrollbars as well.
+ *
+ * @return the right padding in pixels
+ */
+ @InspectableProperty
+ public int getPaddingRight() {
+ if (!isPaddingResolved()) {
+ resolvePadding();
+ }
+ return mPaddingRight;
+ }
+
+ /**
+ * Returns the end padding of this view depending on its resolved layout direction.
+ * If there are inset and enabled scrollbars, this value may include the space
+ * required to display the scrollbars as well.
+ *
+ * @return the end padding in pixels
+ */
+ public int getPaddingEnd() {
+ if (!isPaddingResolved()) {
+ resolvePadding();
+ }
+ return (getLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
+ mPaddingLeft : mPaddingRight;
+ }
+
+ /**
+ * Return if the padding has been set through relative values
+ * {@link #setPaddingRelative(int, int, int, int)} or through
+ * @attr ref android.R.styleable#View_paddingStart or
+ * @attr ref android.R.styleable#View_paddingEnd
+ *
+ * @return true if the padding is relative or false if it is not.
+ */
+ public boolean isPaddingRelative() {
+ return (mUserPaddingStart != UNDEFINED_PADDING || mUserPaddingEnd != UNDEFINED_PADDING);
+ }
+
+ Insets computeOpticalInsets() {
+ return (mBackground == null) ? Insets.NONE : mBackground.getOpticalInsets();
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void resetPaddingToInitialValues() {
+ if (isRtlCompatibilityMode()) {
+ mPaddingLeft = mUserPaddingLeftInitial;
+ mPaddingRight = mUserPaddingRightInitial;
+ return;
+ }
+ if (isLayoutRtl()) {
+ mPaddingLeft = (mUserPaddingEnd >= 0) ? mUserPaddingEnd : mUserPaddingLeftInitial;
+ mPaddingRight = (mUserPaddingStart >= 0) ? mUserPaddingStart : mUserPaddingRightInitial;
+ } else {
+ mPaddingLeft = (mUserPaddingStart >= 0) ? mUserPaddingStart : mUserPaddingLeftInitial;
+ mPaddingRight = (mUserPaddingEnd >= 0) ? mUserPaddingEnd : mUserPaddingRightInitial;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public Insets getOpticalInsets() {
+ if (mLayoutInsets == null) {
+ mLayoutInsets = computeOpticalInsets();
+ }
+ return mLayoutInsets;
+ }
+
+ /**
+ * Set this view's optical insets.
+ *
+ * <p>This method should be treated similarly to setMeasuredDimension and not as a general
+ * property. Views that compute their own optical insets should call it as part of measurement.
+ * This method does not request layout. If you are setting optical insets outside of
+ * measure/layout itself you will want to call requestLayout() yourself.
+ * </p>
+ * @hide
+ */
+ public void setOpticalInsets(Insets insets) {
+ mLayoutInsets = insets;
+ }
+
+ /**
+ * Changes the selection state of this view. A view can be selected or not.
+ * Note that selection is not the same as focus. Views are typically
+ * selected in the context of an AdapterView like ListView or GridView;
+ * the selected view is the view that is highlighted.
+ *
+ * @param selected true if the view must be selected, false otherwise
+ */
+ public void setSelected(boolean selected) {
+ //noinspection DoubleNegation
+ if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) {
+ mPrivateFlags = (mPrivateFlags & ~PFLAG_SELECTED) | (selected ? PFLAG_SELECTED : 0);
+ if (!selected) resetPressedState();
+ invalidate(true);
+ refreshDrawableState();
+ dispatchSetSelected(selected);
+ if (selected) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ } else {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+ }
+
+ /**
+ * Dispatch setSelected to all of this View's children.
+ *
+ * @see #setSelected(boolean)
+ *
+ * @param selected The new selected state
+ */
+ protected void dispatchSetSelected(boolean selected) {
+ }
+
+ /**
+ * Indicates the selection state of this view.
+ *
+ * @return true if the view is selected, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty(hasAttributeId = false)
+ public boolean isSelected() {
+ return (mPrivateFlags & PFLAG_SELECTED) != 0;
+ }
+
+ /**
+ * Changes the activated state of this view. A view can be activated or not.
+ * Note that activation is not the same as selection. Selection is
+ * a transient property, representing the view (hierarchy) the user is
+ * currently interacting with. Activation is a longer-term state that the
+ * user can move views in and out of. For example, in a list view with
+ * single or multiple selection enabled, the views in the current selection
+ * set are activated. (Um, yeah, we are deeply sorry about the terminology
+ * here.) The activated state is propagated down to children of the view it
+ * is set on.
+ *
+ * @param activated true if the view must be activated, false otherwise
+ */
+ public void setActivated(boolean activated) {
+ //noinspection DoubleNegation
+ if (((mPrivateFlags & PFLAG_ACTIVATED) != 0) != activated) {
+ mPrivateFlags = (mPrivateFlags & ~PFLAG_ACTIVATED) | (activated ? PFLAG_ACTIVATED : 0);
+ invalidate(true);
+ refreshDrawableState();
+ dispatchSetActivated(activated);
+ }
+ }
+
+ /**
+ * Dispatch setActivated to all of this View's children.
+ *
+ * @see #setActivated(boolean)
+ *
+ * @param activated The new activated state
+ */
+ protected void dispatchSetActivated(boolean activated) {
+ }
+
+ /**
+ * Indicates the activation state of this view.
+ *
+ * @return true if the view is activated, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty(hasAttributeId = false)
+ public boolean isActivated() {
+ return (mPrivateFlags & PFLAG_ACTIVATED) != 0;
+ }
+
+ /**
+ * Returns the ViewTreeObserver for this view's hierarchy. The view tree
+ * observer can be used to get notifications when global events, like
+ * layout, happen.
+ *
+ * The returned ViewTreeObserver observer is not guaranteed to remain
+ * valid for the lifetime of this View. If the caller of this method keeps
+ * a long-lived reference to ViewTreeObserver, it should always check for
+ * the return value of {@link ViewTreeObserver#isAlive()}.
+ *
+ * @return The ViewTreeObserver for this view's hierarchy.
+ */
+ public ViewTreeObserver getViewTreeObserver() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mTreeObserver;
+ }
+ if (mFloatingTreeObserver == null) {
+ mFloatingTreeObserver = new ViewTreeObserver(mContext);
+ }
+ return mFloatingTreeObserver;
+ }
+
+ /**
+ * <p>Finds the topmost view in the current view hierarchy.</p>
+ *
+ * @return the topmost view containing this view
+ */
+ public View getRootView() {
+ if (mAttachInfo != null) {
+ final View v = mAttachInfo.mRootView;
+ if (v != null) {
+ return v;
+ }
+ }
+
+ View parent = this;
+
+ while (parent.mParent != null && parent.mParent instanceof View) {
+ parent = (View) parent.mParent;
+ }
+
+ return parent;
+ }
+
+ /**
+ * Transforms a motion event from view-local coordinates to on-screen
+ * coordinates.
+ *
+ * @param ev the view-local motion event
+ * @return false if the transformation could not be applied
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean toGlobalMotionEvent(MotionEvent ev) {
+ final AttachInfo info = mAttachInfo;
+ if (info == null) {
+ return false;
+ }
+
+ final Matrix m = info.mTmpMatrix;
+ m.set(Matrix.IDENTITY_MATRIX);
+ transformMatrixToGlobal(m);
+ ev.transform(m);
+ return true;
+ }
+
+ /**
+ * Transforms a motion event from on-screen coordinates to view-local
+ * coordinates.
+ *
+ * @param ev the on-screen motion event
+ * @return false if the transformation could not be applied
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean toLocalMotionEvent(MotionEvent ev) {
+ final AttachInfo info = mAttachInfo;
+ if (info == null) {
+ return false;
+ }
+
+ final Matrix m = info.mTmpMatrix;
+ m.set(Matrix.IDENTITY_MATRIX);
+ transformMatrixToLocal(m);
+ ev.transform(m);
+ return true;
+ }
+
+ /**
+ * Modifies the input matrix such that it maps view-local coordinates to
+ * on-screen coordinates.
+ *
+ * @param matrix input matrix to modify
+ */
+ public void transformMatrixToGlobal(@NonNull Matrix matrix) {
+ final ViewParent parent = mParent;
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ vp.transformMatrixToGlobal(matrix);
+ matrix.preTranslate(-vp.mScrollX, -vp.mScrollY);
+ } else if (parent instanceof ViewRootImpl) {
+ final ViewRootImpl vr = (ViewRootImpl) parent;
+ vr.transformMatrixToGlobal(matrix);
+ matrix.preTranslate(0, -vr.mCurScrollY);
+ }
+
+ matrix.preTranslate(mLeft, mTop);
+
+ if (!hasIdentityMatrix()) {
+ matrix.preConcat(getMatrix());
+ }
+ }
+
+ /**
+ * Modifies the input matrix such that it maps on-screen coordinates to
+ * view-local coordinates.
+ *
+ * @param matrix input matrix to modify
+ */
+ public void transformMatrixToLocal(@NonNull Matrix matrix) {
+ final ViewParent parent = mParent;
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ vp.transformMatrixToLocal(matrix);
+ matrix.postTranslate(vp.mScrollX, vp.mScrollY);
+ } else if (parent instanceof ViewRootImpl) {
+ final ViewRootImpl vr = (ViewRootImpl) parent;
+ vr.transformMatrixToLocal(matrix);
+ matrix.postTranslate(0, vr.mCurScrollY);
+ }
+
+ matrix.postTranslate(-mLeft, -mTop);
+
+ if (!hasIdentityMatrix()) {
+ matrix.postConcat(getInverseMatrix());
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "layout", indexMapping = {
+ @ViewDebug.IntToString(from = 0, to = "x"),
+ @ViewDebug.IntToString(from = 1, to = "y")
+ })
+ @UnsupportedAppUsage
+ public int[] getLocationOnScreen() {
+ int[] location = new int[2];
+ getLocationOnScreen(location);
+ return location;
+ }
+
+ /**
+ * <p>Computes the coordinates of this view on the screen. The argument
+ * must be an array of two integers. After the method returns, the array
+ * contains the x and y location in that order.</p>
+ *
+ * @param outLocation an array of two integers in which to hold the coordinates
+ */
+ public void getLocationOnScreen(@Size(2) int[] outLocation) {
+ getLocationInWindow(outLocation);
+
+ final AttachInfo info = mAttachInfo;
+ if (info != null) {
+ outLocation[0] += info.mWindowLeft;
+ outLocation[1] += info.mWindowTop;
+ }
+ }
+
+ /**
+ * <p>Computes the coordinates of this view in its window. The argument
+ * must be an array of two integers. After the method returns, the array
+ * contains the x and y location in that order.</p>
+ *
+ * @param outLocation an array of two integers in which to hold the coordinates
+ */
+ public void getLocationInWindow(@Size(2) int[] outLocation) {
+ if (outLocation == null || outLocation.length < 2) {
+ throw new IllegalArgumentException("outLocation must be an array of two integers");
+ }
+
+ outLocation[0] = 0;
+ outLocation[1] = 0;
+
+ transformFromViewToWindowSpace(outLocation);
+ }
+
+ /** @hide */
+ public void transformFromViewToWindowSpace(@Size(2) int[] inOutLocation) {
+ if (inOutLocation == null || inOutLocation.length < 2) {
+ throw new IllegalArgumentException("inOutLocation must be an array of two integers");
+ }
+
+ if (mAttachInfo == null) {
+ // When the view is not attached to a window, this method does not make sense
+ inOutLocation[0] = inOutLocation[1] = 0;
+ return;
+ }
+
+ float position[] = mAttachInfo.mTmpTransformLocation;
+ position[0] = inOutLocation[0];
+ position[1] = inOutLocation[1];
+
+ if (!hasIdentityMatrix()) {
+ getMatrix().mapPoints(position);
+ }
+
+ position[0] += mLeft;
+ position[1] += mTop;
+
+ ViewParent viewParent = mParent;
+ while (viewParent instanceof View) {
+ final View view = (View) viewParent;
+
+ position[0] -= view.mScrollX;
+ position[1] -= view.mScrollY;
+
+ if (!view.hasIdentityMatrix()) {
+ view.getMatrix().mapPoints(position);
+ }
+
+ position[0] += view.mLeft;
+ position[1] += view.mTop;
+
+ viewParent = view.mParent;
+ }
+
+ if (viewParent instanceof ViewRootImpl) {
+ // *cough*
+ final ViewRootImpl vr = (ViewRootImpl) viewParent;
+ position[1] -= vr.mCurScrollY;
+ }
+
+ inOutLocation[0] = Math.round(position[0]);
+ inOutLocation[1] = Math.round(position[1]);
+ }
+
+ /**
+ * @param id the id of the view to be found
+ * @return the view of the specified id, null if cannot be found
+ * @hide
+ */
+ protected <T extends View> T findViewTraversal(@IdRes int id) {
+ if (id == mID) {
+ return (T) this;
+ }
+ return null;
+ }
+
+ /**
+ * @param tag the tag of the view to be found
+ * @return the view of specified tag, null if cannot be found
+ * @hide
+ */
+ protected <T extends View> T findViewWithTagTraversal(Object tag) {
+ if (tag != null && tag.equals(mTag)) {
+ return (T) this;
+ }
+ return null;
+ }
+
+ /**
+ * @param predicate The predicate to evaluate.
+ * @param childToSkip If not null, ignores this child during the recursive traversal.
+ * @return The first view that matches the predicate or null.
+ * @hide
+ */
+ protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
+ View childToSkip) {
+ if (predicate.test(this)) {
+ return (T) this;
+ }
+ return null;
+ }
+
+ /**
+ * Finds the first descendant view with the given ID, the view itself if
+ * the ID matches {@link #getId()}, or {@code null} if the ID is invalid
+ * (< 0) or there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID if found, or {@code null} otherwise
+ * @see View#requireViewById(int)
+ */
+ @Nullable
+ public final <T extends View> T findViewById(@IdRes int id) {
+ if (id == NO_ID) {
+ return null;
+ }
+ return findViewTraversal(id);
+ }
+
+ /**
+ * Finds the first descendant view with the given ID, the view itself if the ID matches
+ * {@link #getId()}, or throws an IllegalArgumentException if the ID is invalid or there is no
+ * matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this View");
+ }
+ return view;
+ }
+
+ /**
+ * Performs the traversal to find a view by its unique and stable accessibility id.
+ *
+ * <strong>Note:</strong>This method does not stop at the root namespace
+ * boundary since the user can touch the screen at an arbitrary location
+ * potentially crossing the root namespace boundary which will send an
+ * accessibility event to accessibility services and they should be able
+ * to obtain the event source. Also accessibility ids are guaranteed to be
+ * unique in the window.
+ *
+ * @param accessibilityId The accessibility id.
+ * @return The found view.
+ * @hide
+ */
+ public <T extends View> T findViewByAccessibilityIdTraversal(int accessibilityId) {
+ if (getAccessibilityViewId() == accessibilityId) {
+ return (T) this;
+ }
+ return null;
+ }
+
+ /**
+ * Performs the traversal to find a view by its autofill id.
+ *
+ * <strong>Note:</strong>This method does not stop at the root namespace
+ * boundary.
+ *
+ * @param autofillId The autofill id.
+ * @return The found view.
+ * @hide
+ */
+ public <T extends View> T findViewByAutofillIdTraversal(int autofillId) {
+ if (getAutofillViewId() == autofillId) {
+ return (T) this;
+ }
+ return null;
+ }
+
+ /**
+ * Look for a child view with the given tag. If this view has the given
+ * tag, return this view.
+ *
+ * @param tag The tag to search for, using "tag.equals(getTag())".
+ * @return The View that has the given tag in the hierarchy or null
+ */
+ public final <T extends View> T findViewWithTag(Object tag) {
+ if (tag == null) {
+ return null;
+ }
+ return findViewWithTagTraversal(tag);
+ }
+
+ /**
+ * Look for a child view that matches the specified predicate.
+ * If this view matches the predicate, return this view.
+ *
+ * @param predicate The predicate to evaluate.
+ * @return The first view that matches the predicate or null.
+ * @hide
+ */
+ public final <T extends View> T findViewByPredicate(Predicate<View> predicate) {
+ return findViewByPredicateTraversal(predicate, null);
+ }
+
+ /**
+ * Look for a child view that matches the specified predicate,
+ * starting with the specified view and its descendents and then
+ * recusively searching the ancestors and siblings of that view
+ * until this view is reached.
+ *
+ * This method is useful in cases where the predicate does not match
+ * a single unique view (perhaps multiple views use the same id)
+ * and we are trying to find the view that is "closest" in scope to the
+ * starting view.
+ *
+ * @param start The view to start from.
+ * @param predicate The predicate to evaluate.
+ * @return The first view that matches the predicate or null.
+ * @hide
+ */
+ public final <T extends View> T findViewByPredicateInsideOut(
+ View start, Predicate<View> predicate) {
+ View childToSkip = null;
+ for (;;) {
+ T view = start.findViewByPredicateTraversal(predicate, childToSkip);
+ if (view != null || start == this) {
+ return view;
+ }
+
+ ViewParent parent = start.getParent();
+ if (parent == null || !(parent instanceof View)) {
+ return null;
+ }
+
+ childToSkip = start;
+ start = (View) parent;
+ }
+ }
+
+ /**
+ * Sets the identifier for this view. The identifier does not have to be
+ * unique in this view's hierarchy. The identifier should be a positive
+ * number.
+ *
+ * @see #NO_ID
+ * @see #getId()
+ * @see #findViewById(int)
+ *
+ * @param id a number used to identify the view
+ *
+ * @attr ref android.R.styleable#View_id
+ */
+ public void setId(@IdRes int id) {
+ mID = id;
+ if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
+ mID = generateViewId();
+ }
+ }
+
+ /**
+ * {@hide}
+ *
+ * @param isRoot true if the view belongs to the root namespace, false
+ * otherwise
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public void setIsRootNamespace(boolean isRoot) {
+ if (isRoot) {
+ mPrivateFlags |= PFLAG_IS_ROOT_NAMESPACE;
+ } else {
+ mPrivateFlags &= ~PFLAG_IS_ROOT_NAMESPACE;
+ }
+ }
+
+ /**
+ * {@hide}
+ *
+ * @return true if the view belongs to the root namespace, false otherwise
+ */
+ @UnsupportedAppUsage
+ public boolean isRootNamespace() {
+ return (mPrivateFlags&PFLAG_IS_ROOT_NAMESPACE) != 0;
+ }
+
+ /**
+ * Returns this view's identifier.
+ *
+ * @return a positive integer used to identify the view or {@link #NO_ID}
+ * if the view has no ID
+ *
+ * @see #setId(int)
+ * @see #findViewById(int)
+ * @attr ref android.R.styleable#View_id
+ */
+ @IdRes
+ @ViewDebug.CapturedViewProperty
+ @InspectableProperty
+ public int getId() {
+ return mID;
+ }
+
+ /**
+ * Get the identifier used for this view by the drawing system.
+ *
+ * @see RenderNode#getUniqueId()
+ * @return A long that uniquely identifies this view's drawing component
+ */
+ public long getUniqueDrawingId() {
+ return mRenderNode.getUniqueId();
+ }
+
+ /**
+ * Returns this view's tag.
+ *
+ * @return the Object stored in this view as a tag, or {@code null} if not
+ * set
+ *
+ * @see #setTag(Object)
+ * @see #getTag(int)
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public Object getTag() {
+ return mTag;
+ }
+
+ /**
+ * Sets the tag associated with this view. A tag can be used to mark
+ * a view in its hierarchy and does not have to be unique within the
+ * hierarchy. Tags can also be used to store data within a view without
+ * resorting to another data structure.
+ *
+ * @param tag an Object to tag the view with
+ *
+ * @see #getTag()
+ * @see #setTag(int, Object)
+ */
+ public void setTag(final Object tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Returns the tag associated with this view and the specified key.
+ *
+ * @param key The key identifying the tag
+ *
+ * @return the Object stored in this view as a tag, or {@code null} if not
+ * set
+ *
+ * @see #setTag(int, Object)
+ * @see #getTag()
+ */
+ public Object getTag(int key) {
+ if (mKeyedTags != null) return mKeyedTags.get(key);
+ return null;
+ }
+
+ /**
+ * Sets a tag associated with this view and a key. A tag can be used
+ * to mark a view in its hierarchy and does not have to be unique within
+ * the hierarchy. Tags can also be used to store data within a view
+ * without resorting to another data structure.
+ *
+ * The specified key should be an id declared in the resources of the
+ * application to ensure it is unique (see the <a
+ * href="{@docRoot}guide/topics/resources/more-resources.html#Id">ID resource type</a>).
+ * Keys identified as belonging to
+ * the Android framework or not associated with any package will cause
+ * an {@link IllegalArgumentException} to be thrown.
+ *
+ * @param key The key identifying the tag
+ * @param tag An Object to tag the view with
+ *
+ * @throws IllegalArgumentException If they specified key is not valid
+ *
+ * @see #setTag(Object)
+ * @see #getTag(int)
+ */
+ public void setTag(int key, final Object tag) {
+ // If the package id is 0x00 or 0x01, it's either an undefined package
+ // or a framework id
+ if ((key >>> 24) < 2) {
+ throw new IllegalArgumentException("The key must be an application-specific "
+ + "resource id.");
+ }
+
+ setKeyedTag(key, tag);
+ }
+
+ /**
+ * Variation of {@link #setTag(int, Object)} that enforces the key to be a
+ * framework id.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setTagInternal(int key, Object tag) {
+ if ((key >>> 24) != 0x1) {
+ throw new IllegalArgumentException("The key must be a framework-specific "
+ + "resource id.");
+ }
+
+ setKeyedTag(key, tag);
+ }
+
+ private void setKeyedTag(int key, Object tag) {
+ if (mKeyedTags == null) {
+ mKeyedTags = new SparseArray<Object>(2);
+ }
+
+ mKeyedTags.put(key, tag);
+ }
+
+ /**
+ * Prints information about this view in the log output, with the tag
+ * {@link #VIEW_LOG_TAG}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void debug() {
+ debug(0);
+ }
+
+ /**
+ * Prints information about this view in the log output, with the tag
+ * {@link #VIEW_LOG_TAG}. Each line in the output is preceded with an
+ * indentation defined by the <code>depth</code>.
+ *
+ * @param depth the indentation level
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void debug(int depth) {
+ String output = debugIndent(depth - 1);
+
+ output += "+ " + this;
+ int id = getId();
+ if (id != -1) {
+ output += " (id=" + id + ")";
+ }
+ Object tag = getTag();
+ if (tag != null) {
+ output += " (tag=" + tag + ")";
+ }
+ Log.d(VIEW_LOG_TAG, output);
+
+ if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
+ output = debugIndent(depth) + " FOCUSED";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ output = debugIndent(depth);
+ output += "frame={" + mLeft + ", " + mTop + ", " + mRight
+ + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
+ + "} ";
+ Log.d(VIEW_LOG_TAG, output);
+
+ if (mPaddingLeft != 0 || mPaddingTop != 0 || mPaddingRight != 0
+ || mPaddingBottom != 0) {
+ output = debugIndent(depth);
+ output += "padding={" + mPaddingLeft + ", " + mPaddingTop
+ + ", " + mPaddingRight + ", " + mPaddingBottom + "}";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ output = debugIndent(depth);
+ output += "mMeasureWidth=" + mMeasuredWidth +
+ " mMeasureHeight=" + mMeasuredHeight;
+ Log.d(VIEW_LOG_TAG, output);
+
+ output = debugIndent(depth);
+ if (mLayoutParams == null) {
+ output += "BAD! no layout params";
+ } else {
+ output = mLayoutParams.debug(output);
+ }
+ Log.d(VIEW_LOG_TAG, output);
+
+ output = debugIndent(depth);
+ output += "flags={";
+ output += View.printFlags(mViewFlags);
+ output += "}";
+ Log.d(VIEW_LOG_TAG, output);
+
+ output = debugIndent(depth);
+ output += "privateFlags={";
+ output += View.printPrivateFlags(mPrivateFlags);
+ output += "}";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ /**
+ * Creates a string of whitespaces used for indentation.
+ *
+ * @param depth the indentation level
+ * @return a String containing (depth * 2 + 3) * 2 white spaces
+ *
+ * @hide
+ */
+ protected static String debugIndent(int depth) {
+ StringBuilder spaces = new StringBuilder((depth * 2 + 3) * 2);
+ for (int i = 0; i < (depth * 2) + 3; i++) {
+ spaces.append(' ').append(' ');
+ }
+ return spaces.toString();
+ }
+
+ /**
+ * <p>Return the offset of the widget's text baseline from the widget's top
+ * boundary. If this widget does not support baseline alignment, this
+ * method returns -1. </p>
+ *
+ * @return the offset of the baseline within the widget's bounds or -1
+ * if baseline alignment is not supported
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @InspectableProperty
+ public int getBaseline() {
+ return -1;
+ }
+
+ /**
+ * Returns whether the view hierarchy is currently undergoing a layout pass. This
+ * information is useful to avoid situations such as calling {@link #requestLayout()} during
+ * a layout pass.
+ *
+ * @return whether the view hierarchy is currently undergoing a layout pass
+ */
+ public boolean isInLayout() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ return (viewRoot != null && viewRoot.isInLayout());
+ }
+
+ /**
+ * Call this when something has changed which has invalidated the
+ * layout of this view. This will schedule a layout pass of the view
+ * tree. This should not be called while the view hierarchy is currently in a layout
+ * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
+ * end of the current layout pass (and then layout will run again) or after the current
+ * frame is drawn and the next layout occurs.
+ *
+ * <p>Subclasses which override this method should call the superclass method to
+ * handle possible request-during-layout errors correctly.</p>
+ */
+ @CallSuper
+ public void requestLayout() {
+ if (mMeasureCache != null) mMeasureCache.clear();
+
+ if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
+ // Only trigger request-during-layout logic if this is the view requesting it,
+ // not the views in its parent hierarchy
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null && viewRoot.isInLayout()) {
+ if (!viewRoot.requestLayoutDuringLayout(this)) {
+ return;
+ }
+ }
+ mAttachInfo.mViewRequestingLayout = this;
+ }
+
+ mPrivateFlags |= PFLAG_FORCE_LAYOUT;
+ mPrivateFlags |= PFLAG_INVALIDATED;
+
+ if (mParent != null && !mParent.isLayoutRequested()) {
+ mParent.requestLayout();
+ }
+ if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
+ mAttachInfo.mViewRequestingLayout = null;
+ }
+ }
+
+ /**
+ * Forces this view to be laid out during the next layout pass.
+ * This method does not call requestLayout() or forceLayout()
+ * on the parent.
+ */
+ public void forceLayout() {
+ if (mMeasureCache != null) mMeasureCache.clear();
+
+ mPrivateFlags |= PFLAG_FORCE_LAYOUT;
+ mPrivateFlags |= PFLAG_INVALIDATED;
+ }
+
+ /**
+ * <p>
+ * This is called to find out how big a view should be. The parent
+ * supplies constraint information in the width and height parameters.
+ * </p>
+ *
+ * <p>
+ * The actual measurement work of a view is performed in
+ * {@link #onMeasure(int, int)}, called by this method. Therefore, only
+ * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
+ * </p>
+ *
+ *
+ * @param widthMeasureSpec Horizontal space requirements as imposed by the
+ * parent
+ * @param heightMeasureSpec Vertical space requirements as imposed by the
+ * parent
+ *
+ * @see #onMeasure(int, int)
+ */
+ public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
+ boolean optical = isLayoutModeOptical(this);
+ if (optical != isLayoutModeOptical(mParent)) {
+ Insets insets = getOpticalInsets();
+ int oWidth = insets.left + insets.right;
+ int oHeight = insets.top + insets.bottom;
+ widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
+ heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
+ }
+
+ // Suppress sign extension for the low bytes
+ long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
+ if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
+
+ final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
+
+ // Optimize layout by avoiding an extra EXACTLY pass when the view is
+ // already measured as the correct size. In API 23 and below, this
+ // extra pass is required to make LinearLayout re-distribute weight.
+ final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
+ || heightMeasureSpec != mOldHeightMeasureSpec;
+ final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
+ && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
+ final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
+ && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
+ final boolean needsLayout = specChanged
+ && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
+
+ if (forceLayout || needsLayout) {
+ // first clears the measured dimension flag
+ mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
+
+ resolveRtlPropertiesIfNeeded();
+
+ int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
+ if (cacheIndex < 0 || sIgnoreMeasureCache) {
+ // measure ourselves, this should set the measured dimension flag back
+ onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
+ } else {
+ long value = mMeasureCache.valueAt(cacheIndex);
+ // Casting a long to int drops the high 32 bits, no mask needed
+ setMeasuredDimensionRaw((int) (value >> 32), (int) value);
+ mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
+ }
+
+ // flag not set, setMeasuredDimension() was not invoked, we raise
+ // an exception to warn the developer
+ if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
+ throw new IllegalStateException("View with id " + getId() + ": "
+ + getClass().getName() + "#onMeasure() did not set the"
+ + " measured dimension by calling"
+ + " setMeasuredDimension()");
+ }
+
+ mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
+ }
+
+ mOldWidthMeasureSpec = widthMeasureSpec;
+ mOldHeightMeasureSpec = heightMeasureSpec;
+
+ mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
+ (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
+ }
+
+ /**
+ * <p>
+ * Measure the view and its content to determine the measured width and the
+ * measured height. This method is invoked by {@link #measure(int, int)} and
+ * should be overridden by subclasses to provide accurate and efficient
+ * measurement of their contents.
+ * </p>
+ *
+ * <p>
+ * <strong>CONTRACT:</strong> When overriding this method, you
+ * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
+ * measured width and height of this view. Failure to do so will trigger an
+ * <code>IllegalStateException</code>, thrown by
+ * {@link #measure(int, int)}. Calling the superclass'
+ * {@link #onMeasure(int, int)} is a valid use.
+ * </p>
+ *
+ * <p>
+ * The base class implementation of measure defaults to the background size,
+ * unless a larger size is allowed by the MeasureSpec. Subclasses should
+ * override {@link #onMeasure(int, int)} to provide better measurements of
+ * their content.
+ * </p>
+ *
+ * <p>
+ * If this method is overridden, it is the subclass's responsibility to make
+ * sure the measured height and width are at least the view's minimum height
+ * and width ({@link #getSuggestedMinimumHeight()} and
+ * {@link #getSuggestedMinimumWidth()}).
+ * </p>
+ *
+ * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
+ * The requirements are encoded with
+ * {@link android.view.View.MeasureSpec}.
+ * @param heightMeasureSpec vertical space requirements as imposed by the parent.
+ * The requirements are encoded with
+ * {@link android.view.View.MeasureSpec}.
+ *
+ * @see #getMeasuredWidth()
+ * @see #getMeasuredHeight()
+ * @see #setMeasuredDimension(int, int)
+ * @see #getSuggestedMinimumHeight()
+ * @see #getSuggestedMinimumWidth()
+ * @see android.view.View.MeasureSpec#getMode(int)
+ * @see android.view.View.MeasureSpec#getSize(int)
+ */
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+ getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
+ }
+
+ /**
+ * <p>This method must be called by {@link #onMeasure(int, int)} to store the
+ * measured width and measured height. Failing to do so will trigger an
+ * exception at measurement time.</p>
+ *
+ * @param measuredWidth The measured width of this view. May be a complex
+ * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
+ * {@link #MEASURED_STATE_TOO_SMALL}.
+ * @param measuredHeight The measured height of this view. May be a complex
+ * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
+ * {@link #MEASURED_STATE_TOO_SMALL}.
+ */
+ protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
+ boolean optical = isLayoutModeOptical(this);
+ if (optical != isLayoutModeOptical(mParent)) {
+ Insets insets = getOpticalInsets();
+ int opticalWidth = insets.left + insets.right;
+ int opticalHeight = insets.top + insets.bottom;
+
+ measuredWidth += optical ? opticalWidth : -opticalWidth;
+ measuredHeight += optical ? opticalHeight : -opticalHeight;
+ }
+ setMeasuredDimensionRaw(measuredWidth, measuredHeight);
+ }
+
+ /**
+ * Sets the measured dimension without extra processing for things like optical bounds.
+ * Useful for reapplying consistent values that have already been cooked with adjustments
+ * for optical bounds, etc. such as those from the measurement cache.
+ *
+ * @param measuredWidth The measured width of this view. May be a complex
+ * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
+ * {@link #MEASURED_STATE_TOO_SMALL}.
+ * @param measuredHeight The measured height of this view. May be a complex
+ * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
+ * {@link #MEASURED_STATE_TOO_SMALL}.
+ */
+ private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
+ mMeasuredWidth = measuredWidth;
+ mMeasuredHeight = measuredHeight;
+
+ mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
+ }
+
+ /**
+ * Merge two states as returned by {@link #getMeasuredState()}.
+ * @param curState The current state as returned from a view or the result
+ * of combining multiple views.
+ * @param newState The new view state to combine.
+ * @return Returns a new integer reflecting the combination of the two
+ * states.
+ */
+ public static int combineMeasuredStates(int curState, int newState) {
+ return curState | newState;
+ }
+
+ /**
+ * Version of {@link #resolveSizeAndState(int, int, int)}
+ * returning only the {@link #MEASURED_SIZE_MASK} bits of the result.
+ */
+ public static int resolveSize(int size, int measureSpec) {
+ return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
+ }
+
+ /**
+ * Utility to reconcile a desired size and state, with constraints imposed
+ * by a MeasureSpec. Will take the desired size, unless a different size
+ * is imposed by the constraints. The returned value is a compound integer,
+ * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
+ * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
+ * resulting size is smaller than the size the view wants to be.
+ *
+ * @param size How big the view wants to be.
+ * @param measureSpec Constraints imposed by the parent.
+ * @param childMeasuredState Size information bit mask for the view's
+ * children.
+ * @return Size information bit mask as defined by
+ * {@link #MEASURED_SIZE_MASK} and
+ * {@link #MEASURED_STATE_TOO_SMALL}.
+ */
+ public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
+ final int specMode = MeasureSpec.getMode(measureSpec);
+ final int specSize = MeasureSpec.getSize(measureSpec);
+ final int result;
+ switch (specMode) {
+ case MeasureSpec.AT_MOST:
+ if (specSize < size) {
+ result = specSize | MEASURED_STATE_TOO_SMALL;
+ } else {
+ result = size;
+ }
+ break;
+ case MeasureSpec.EXACTLY:
+ result = specSize;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ result = size;
+ }
+ return result | (childMeasuredState & MEASURED_STATE_MASK);
+ }
+
+ /**
+ * Utility to return a default size. Uses the supplied size if the
+ * MeasureSpec imposed no constraints. Will get larger if allowed
+ * by the MeasureSpec.
+ *
+ * @param size Default size for this view
+ * @param measureSpec Constraints imposed by the parent
+ * @return The size this view should be.
+ */
+ public static int getDefaultSize(int size, int measureSpec) {
+ int result = size;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ result = size;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.EXACTLY:
+ result = specSize;
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the suggested minimum height that the view should use. This
+ * returns the maximum of the view's minimum height
+ * and the background's minimum height
+ * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
+ * <p>
+ * When being used in {@link #onMeasure(int, int)}, the caller should still
+ * ensure the returned height is within the requirements of the parent.
+ *
+ * @return The suggested minimum height of the view.
+ */
+ protected int getSuggestedMinimumHeight() {
+ return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
+
+ }
+
+ /**
+ * Returns the suggested minimum width that the view should use. This
+ * returns the maximum of the view's minimum width
+ * and the background's minimum width
+ * ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
+ * <p>
+ * When being used in {@link #onMeasure(int, int)}, the caller should still
+ * ensure the returned width is within the requirements of the parent.
+ *
+ * @return The suggested minimum width of the view.
+ */
+ protected int getSuggestedMinimumWidth() {
+ return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
+ }
+
+ /**
+ * Returns the minimum height of the view.
+ *
+ * @return the minimum height the view will try to be, in pixels
+ *
+ * @see #setMinimumHeight(int)
+ *
+ * @attr ref android.R.styleable#View_minHeight
+ */
+ @InspectableProperty(name = "minHeight")
+ public int getMinimumHeight() {
+ return mMinHeight;
+ }
+
+ /**
+ * Sets the minimum height of the view. It is not guaranteed the view will
+ * be able to achieve this minimum height (for example, if its parent layout
+ * constrains it with less available height).
+ *
+ * @param minHeight The minimum height the view will try to be, in pixels
+ *
+ * @see #getMinimumHeight()
+ *
+ * @attr ref android.R.styleable#View_minHeight
+ */
+ @RemotableViewMethod
+ public void setMinimumHeight(int minHeight) {
+ mMinHeight = minHeight;
+ requestLayout();
+ }
+
+ /**
+ * Returns the minimum width of the view.
+ *
+ * @return the minimum width the view will try to be, in pixels
+ *
+ * @see #setMinimumWidth(int)
+ *
+ * @attr ref android.R.styleable#View_minWidth
+ */
+ @InspectableProperty(name = "minWidth")
+ public int getMinimumWidth() {
+ return mMinWidth;
+ }
+
+ /**
+ * Sets the minimum width of the view. It is not guaranteed the view will
+ * be able to achieve this minimum width (for example, if its parent layout
+ * constrains it with less available width).
+ *
+ * @param minWidth The minimum width the view will try to be, in pixels
+ *
+ * @see #getMinimumWidth()
+ *
+ * @attr ref android.R.styleable#View_minWidth
+ */
+ @RemotableViewMethod
+ public void setMinimumWidth(int minWidth) {
+ mMinWidth = minWidth;
+ requestLayout();
+
+ }
+
+ /**
+ * Get the animation currently associated with this view.
+ *
+ * @return The animation that is currently playing or
+ * scheduled to play for this view.
+ */
+ public Animation getAnimation() {
+ return mCurrentAnimation;
+ }
+
+ /**
+ * Start the specified animation now.
+ *
+ * @param animation the animation to start now
+ */
+ public void startAnimation(Animation animation) {
+ animation.setStartTime(Animation.START_ON_FIRST_FRAME);
+ setAnimation(animation);
+ invalidateParentCaches();
+ invalidate(true);
+ }
+
+ /**
+ * Cancels any animations for this view.
+ */
+ public void clearAnimation() {
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.detach();
+ }
+ mCurrentAnimation = null;
+ invalidateParentIfNeeded();
+ }
+
+ /**
+ * Sets the next animation to play for this view.
+ * If you want the animation to play immediately, use
+ * {@link #startAnimation(android.view.animation.Animation)} instead.
+ * This method provides allows fine-grained
+ * control over the start time and invalidation, but you
+ * must make sure that 1) the animation has a start time set, and
+ * 2) the view's parent (which controls animations on its children)
+ * will be invalidated when the animation is supposed to
+ * start.
+ *
+ * @param animation The next animation, or null.
+ */
+ public void setAnimation(Animation animation) {
+ mCurrentAnimation = animation;
+
+ if (animation != null) {
+ // If the screen is off assume the animation start time is now instead of
+ // the next frame we draw. Keeping the START_ON_FIRST_FRAME start time
+ // would cause the animation to start when the screen turns back on
+ if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
+ && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
+ animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
+ }
+ animation.reset();
+ }
+ }
+
+ /**
+ * Invoked by a parent ViewGroup to notify the start of the animation
+ * currently associated with this view. If you override this method,
+ * always call super.onAnimationStart();
+ *
+ * @see #setAnimation(android.view.animation.Animation)
+ * @see #getAnimation()
+ */
+ @CallSuper
+ protected void onAnimationStart() {
+ mPrivateFlags |= PFLAG_ANIMATION_STARTED;
+ }
+
+ /**
+ * Invoked by a parent ViewGroup to notify the end of the animation
+ * currently associated with this view. If you override this method,
+ * always call super.onAnimationEnd();
+ *
+ * @see #setAnimation(android.view.animation.Animation)
+ * @see #getAnimation()
+ */
+ @CallSuper
+ protected void onAnimationEnd() {
+ mPrivateFlags &= ~PFLAG_ANIMATION_STARTED;
+ }
+
+ /**
+ * Invoked if there is a Transform that involves alpha. Subclass that can
+ * draw themselves with the specified alpha should return true, and then
+ * respect that alpha when their onDraw() is called. If this returns false
+ * then the view may be redirected to draw into an offscreen buffer to
+ * fulfill the request, which will look fine, but may be slower than if the
+ * subclass handles it internally. The default implementation returns false.
+ *
+ * @param alpha The alpha (0..255) to apply to the view's drawing
+ * @return true if the view can draw with the specified alpha.
+ */
+ protected boolean onSetAlpha(int alpha) {
+ return false;
+ }
+
+ /**
+ * This is used by the ViewRoot to perform an optimization when
+ * the view hierarchy contains one or several SurfaceView.
+ * SurfaceView is always considered transparent, but its children are not,
+ * therefore all View objects remove themselves from the global transparent
+ * region (passed as a parameter to this function).
+ *
+ * @param region The transparent region for this ViewAncestor (window).
+ *
+ * @return Returns true if the effective visibility of the view at this
+ * point is opaque, regardless of the transparent region; returns false
+ * if it is possible for underlying windows to be seen behind the view.
+ *
+ */
+ public boolean gatherTransparentRegion(@Nullable Region region) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (region != null && attachInfo != null) {
+ final int pflags = mPrivateFlags;
+ if ((pflags & PFLAG_SKIP_DRAW) == 0) {
+ // The SKIP_DRAW flag IS NOT set, so this view draws. We need to
+ // remove it from the transparent region.
+ final int[] location = attachInfo.mTransparentLocation;
+ getLocationInWindow(location);
+ // When a view has Z value, then it will be better to leave some area below the view
+ // for drawing shadow. The shadow outset is proportional to the Z value. Note that
+ // the bottom part needs more offset than the left, top and right parts due to the
+ // spot light effects.
+ int shadowOffset = getZ() > 0 ? (int) getZ() : 0;
+ region.op(location[0] - shadowOffset, location[1] - shadowOffset,
+ location[0] + mRight - mLeft + shadowOffset,
+ location[1] + mBottom - mTop + (shadowOffset * 3), Region.Op.DIFFERENCE);
+ } else {
+ if (mBackground != null && mBackground.getOpacity() != PixelFormat.TRANSPARENT) {
+ // The SKIP_DRAW flag IS set and the background drawable exists, we remove
+ // the background drawable's non-transparent parts from this transparent region.
+ applyDrawableToTransparentRegion(mBackground, region);
+ }
+ if (mForegroundInfo != null && mForegroundInfo.mDrawable != null
+ && mForegroundInfo.mDrawable.getOpacity() != PixelFormat.TRANSPARENT) {
+ // Similarly, we remove the foreground drawable's non-transparent parts.
+ applyDrawableToTransparentRegion(mForegroundInfo.mDrawable, region);
+ }
+ if (mDefaultFocusHighlight != null
+ && mDefaultFocusHighlight.getOpacity() != PixelFormat.TRANSPARENT) {
+ // Similarly, we remove the default focus highlight's non-transparent parts.
+ applyDrawableToTransparentRegion(mDefaultFocusHighlight, region);
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Play a sound effect for this view.
+ *
+ * <p>The framework will play sound effects for some built in actions, such as
+ * clicking, but you may wish to play these effects in your widget,
+ * for instance, for internal navigation.
+ *
+ * <p>The sound effect will only be played if sound effects are enabled by the user, and
+ * {@link #isSoundEffectsEnabled()} is true.
+ *
+ * @param soundConstant One of the constants defined in {@link SoundEffectConstants}.
+ */
+ public void playSoundEffect(@SoundEffectConstants.SoundEffect int soundConstant) {
+ if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
+ return;
+ }
+ mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
+ }
+
+ /**
+ * BZZZTT!!1!
+ *
+ * <p>Provide haptic feedback to the user for this view.
+ *
+ * <p>The framework will provide haptic feedback for some built in actions,
+ * such as long presses, but you may wish to provide feedback for your
+ * own widget.
+ *
+ * <p>The feedback will only be performed if
+ * {@link #isHapticFeedbackEnabled()} is true.
+ *
+ * @param feedbackConstant One of the constants defined in
+ * {@link HapticFeedbackConstants}
+ */
+ public boolean performHapticFeedback(int feedbackConstant) {
+ return performHapticFeedback(feedbackConstant, 0);
+ }
+
+ /**
+ * BZZZTT!!1!
+ *
+ * <p>Like {@link #performHapticFeedback(int)}, with additional options.
+ *
+ * @param feedbackConstant One of the constants defined in
+ * {@link HapticFeedbackConstants}
+ * @param flags Additional flags as per {@link HapticFeedbackConstants}.
+ */
+ public boolean performHapticFeedback(int feedbackConstant, int flags) {
+ if (mAttachInfo == null) {
+ return false;
+ }
+ //noinspection SimplifiableIfStatement
+ if ((flags & HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING) == 0
+ && !isHapticFeedbackEnabled()) {
+ return false;
+ }
+ return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant,
+ (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0);
+ }
+
+ /**
+ * Request that the visibility of the status bar or other screen/window
+ * decorations be changed.
+ *
+ * <p>This method is used to put the over device UI into temporary modes
+ * where the user's attention is focused more on the application content,
+ * by dimming or hiding surrounding system affordances. This is typically
+ * used in conjunction with {@link Window#FEATURE_ACTION_BAR_OVERLAY
+ * Window.FEATURE_ACTION_BAR_OVERLAY}, allowing the applications content
+ * to be placed behind the action bar (and with these flags other system
+ * affordances) so that smooth transitions between hiding and showing them
+ * can be done.
+ *
+ * <p>Two representative examples of the use of system UI visibility is
+ * implementing a content browsing application (like a magazine reader)
+ * and a video playing application.
+ *
+ * <p>The first code shows a typical implementation of a View in a content
+ * browsing application. In this implementation, the application goes
+ * into a content-oriented mode by hiding the status bar and action bar,
+ * and putting the navigation elements into lights out mode. The user can
+ * then interact with content while in this mode. Such an application should
+ * provide an easy way for the user to toggle out of the mode (such as to
+ * check information in the status bar or access notifications). In the
+ * implementation here, this is done simply by tapping on the content.
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/view/ContentBrowserActivity.java
+ * content}
+ *
+ * <p>This second code sample shows a typical implementation of a View
+ * in a video playing application. In this situation, while the video is
+ * playing the application would like to go into a complete full-screen mode,
+ * to use as much of the display as possible for the video. When in this state
+ * the user can not interact with the application; the system intercepts
+ * touching on the screen to pop the UI out of full screen mode. See
+ * {@link #fitSystemWindows(Rect)} for a sample layout that goes with this code.
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/view/VideoPlayerActivity.java
+ * content}
+ *
+ * @param visibility Bitwise-or of flags {@link #SYSTEM_UI_FLAG_LOW_PROFILE},
+ * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, {@link #SYSTEM_UI_FLAG_FULLSCREEN},
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_STABLE}, {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION},
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}, {@link #SYSTEM_UI_FLAG_IMMERSIVE},
+ * and {@link #SYSTEM_UI_FLAG_IMMERSIVE_STICKY}.
+ *
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @Deprecated
+ public void setSystemUiVisibility(int visibility) {
+ if (visibility != mSystemUiVisibility) {
+ mSystemUiVisibility = visibility;
+ if (mParent != null && mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) {
+ mParent.recomputeViewAttributes(this);
+ }
+ }
+ }
+
+ /**
+ * Returns the last {@link #setSystemUiVisibility(int)} that this view has requested.
+ * @return Bitwise-or of flags {@link #SYSTEM_UI_FLAG_LOW_PROFILE},
+ * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, {@link #SYSTEM_UI_FLAG_FULLSCREEN},
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_STABLE}, {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION},
+ * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}, {@link #SYSTEM_UI_FLAG_IMMERSIVE},
+ * and {@link #SYSTEM_UI_FLAG_IMMERSIVE_STICKY}.
+ *
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @Deprecated
+ public int getSystemUiVisibility() {
+ return mSystemUiVisibility;
+ }
+
+ /**
+ * Returns the current system UI visibility that is currently set for
+ * the entire window. This is the combination of the
+ * {@link #setSystemUiVisibility(int)} values supplied by all of the
+ * views in the window.
+ *
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @Deprecated
+ public int getWindowSystemUiVisibility() {
+ return mAttachInfo != null ? mAttachInfo.mSystemUiVisibility : 0;
+ }
+
+ /**
+ * Override to find out when the window's requested system UI visibility
+ * has changed, that is the value returned by {@link #getWindowSystemUiVisibility()}.
+ * This is different from the callbacks received through
+ * {@link #setOnSystemUiVisibilityChangeListener(OnSystemUiVisibilityChangeListener)}
+ * in that this is only telling you about the local request of the window,
+ * not the actual values applied by the system.
+ *
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @Deprecated
+ public void onWindowSystemUiVisibilityChanged(int visible) {
+ }
+
+ /**
+ * Dispatch callbacks to {@link #onWindowSystemUiVisibilityChanged(int)} down
+ * the view hierarchy.
+ *
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @Deprecated
+ public void dispatchWindowSystemUiVisiblityChanged(int visible) {
+ onWindowSystemUiVisibilityChanged(visible);
+ }
+
+ /**
+ * Set a listener to receive callbacks when the visibility of the system bar changes.
+ * @param l The {@link OnSystemUiVisibilityChangeListener} to receive callbacks.
+ *
+ * @deprecated Use {@link WindowInsets#isVisible(int)} to find out about system bar visibilities
+ * by setting a {@link OnApplyWindowInsetsListener} on this view.
+ */
+ @Deprecated
+ public void setOnSystemUiVisibilityChangeListener(OnSystemUiVisibilityChangeListener l) {
+ getListenerInfo().mOnSystemUiVisibilityChangeListener = l;
+ if (mParent != null && mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) {
+ mParent.recomputeViewAttributes(this);
+ }
+ }
+
+ /**
+ * Dispatch callbacks to {@link #setOnSystemUiVisibilityChangeListener} down
+ * the view hierarchy.
+ *
+ * @deprecated Use {@link WindowInsets#isVisible(int)} to find out about system bar visibilities
+ * by setting a {@link OnApplyWindowInsetsListener} on this view.
+ */
+ @Deprecated
+ public void dispatchSystemUiVisibilityChanged(int visibility) {
+ ListenerInfo li = mListenerInfo;
+ if (li != null && li.mOnSystemUiVisibilityChangeListener != null) {
+ li.mOnSystemUiVisibilityChangeListener.onSystemUiVisibilityChange(
+ visibility & PUBLIC_STATUS_BAR_VISIBILITY_MASK);
+ }
+ }
+
+ boolean updateLocalSystemUiVisibility(int localValue, int localChanges) {
+ int val = (mSystemUiVisibility&~localChanges) | (localValue&localChanges);
+ if (val != mSystemUiVisibility) {
+ setSystemUiVisibility(val);
+ return true;
+ }
+ return false;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void setDisabledSystemUiVisibility(int flags) {
+ if (mAttachInfo != null) {
+ if (mAttachInfo.mDisabledSystemUiVisibility != flags) {
+ mAttachInfo.mDisabledSystemUiVisibility = flags;
+ if (mParent != null) {
+ mParent.recomputeViewAttributes(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * This needs to be a better API before it is exposed. For now, only the root view will get
+ * notified.
+ * @hide
+ */
+ public void onSystemBarAppearanceChanged(@WindowInsetsController.Appearance int appearance) {
+ }
+
+ /**
+ * Creates an image that the system displays during the drag and drop
+ * operation. This is called a "drag shadow". The default implementation
+ * for a DragShadowBuilder based on a View returns an image that has exactly the same
+ * appearance as the given View. The default also positions the center of the drag shadow
+ * directly under the touch point. If no View is provided (the constructor with no parameters
+ * is used), and {@link #onProvideShadowMetrics(Point,Point) onProvideShadowMetrics()} and
+ * {@link #onDrawShadow(Canvas) onDrawShadow()} are not overridden, then the
+ * default is an invisible drag shadow.
+ * <p>
+ * You are not required to use the View you provide to the constructor as the basis of the
+ * drag shadow. The {@link #onDrawShadow(Canvas) onDrawShadow()} method allows you to draw
+ * anything you want as the drag shadow.
+ * </p>
+ * <p>
+ * You pass a DragShadowBuilder object to the system when you start the drag. The system
+ * calls {@link #onProvideShadowMetrics(Point,Point) onProvideShadowMetrics()} to get the
+ * size and position of the drag shadow. It uses this data to construct a
+ * {@link android.graphics.Canvas} object, then it calls {@link #onDrawShadow(Canvas) onDrawShadow()}
+ * so that your application can draw the shadow image in the Canvas.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a guide to implementing drag and drop features, read the
+ * <a href="{@docRoot}guide/topics/ui/drag-drop.html">Drag and Drop</a> developer guide.</p>
+ * </div>
+ */
+ public static class DragShadowBuilder {
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private final WeakReference<View> mView;
+
+ /**
+ * Constructs a shadow image builder based on a View. By default, the resulting drag
+ * shadow will have the same appearance and dimensions as the View, with the touch point
+ * over the center of the View.
+ * @param view A View. Any View in scope can be used.
+ */
+ public DragShadowBuilder(View view) {
+ mView = new WeakReference<View>(view);
+ }
+
+ /**
+ * Construct a shadow builder object with no associated View. This
+ * constructor variant is only useful when the {@link #onProvideShadowMetrics(Point, Point)}
+ * and {@link #onDrawShadow(Canvas)} methods are also overridden in order
+ * to supply the drag shadow's dimensions and appearance without
+ * reference to any View object.
+ */
+ public DragShadowBuilder() {
+ mView = new WeakReference<View>(null);
+ }
+
+ /**
+ * Returns the View object that had been passed to the
+ * {@link #DragShadowBuilder(View)}
+ * constructor. If that View parameter was {@code null} or if the
+ * {@link #DragShadowBuilder()}
+ * constructor was used to instantiate the builder object, this method will return
+ * null.
+ *
+ * @return The View object associate with this builder object.
+ */
+ @SuppressWarnings({"JavadocReference"})
+ final public View getView() {
+ return mView.get();
+ }
+
+ /**
+ * Provides the metrics for the shadow image. These include the dimensions of
+ * the shadow image, and the point within that shadow that should
+ * be centered under the touch location while dragging.
+ * <p>
+ * The default implementation sets the dimensions of the shadow to be the
+ * same as the dimensions of the View itself and centers the shadow under
+ * the touch point.
+ * </p>
+ *
+ * @param outShadowSize A {@link android.graphics.Point} containing the width and height
+ * of the shadow image. Your application must set {@link android.graphics.Point#x} to the
+ * desired width and must set {@link android.graphics.Point#y} to the desired height of the
+ * image. Since Android P, the width and height must be positive values.
+ *
+ * @param outShadowTouchPoint A {@link android.graphics.Point} for the position within the
+ * shadow image that should be underneath the touch point during the drag and drop
+ * operation. Your application must set {@link android.graphics.Point#x} to the
+ * X coordinate and {@link android.graphics.Point#y} to the Y coordinate of this position.
+ */
+ public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
+ final View view = mView.get();
+ if (view != null) {
+ outShadowSize.set(view.getWidth(), view.getHeight());
+ outShadowTouchPoint.set(outShadowSize.x / 2, outShadowSize.y / 2);
+ } else {
+ Log.e(View.VIEW_LOG_TAG, "Asked for drag thumb metrics but no view");
+ }
+ }
+
+ /**
+ * Draws the shadow image. The system creates the {@link android.graphics.Canvas} object
+ * based on the dimensions it received from the
+ * {@link #onProvideShadowMetrics(Point, Point)} callback.
+ *
+ * @param canvas A {@link android.graphics.Canvas} object in which to draw the shadow image.
+ */
+ public void onDrawShadow(Canvas canvas) {
+ final View view = mView.get();
+ if (view != null) {
+ view.draw(canvas);
+ } else {
+ Log.e(View.VIEW_LOG_TAG, "Asked to draw drag shadow but no view");
+ }
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)
+ * startDragAndDrop()} for newer platform versions.
+ */
+ @Deprecated
+ public final boolean startDrag(ClipData data, DragShadowBuilder shadowBuilder,
+ Object myLocalState, int flags) {
+ return startDragAndDrop(data, shadowBuilder, myLocalState, flags);
+ }
+
+ /**
+ * Starts a drag and drop operation. When your application calls this method, it passes a
+ * {@link android.view.View.DragShadowBuilder} object to the system. The
+ * system calls this object's {@link DragShadowBuilder#onProvideShadowMetrics(Point, Point)}
+ * to get metrics for the drag shadow, and then calls the object's
+ * {@link DragShadowBuilder#onDrawShadow(Canvas)} to draw the drag shadow itself.
+ * <p>
+ * Once the system has the drag shadow, it begins the drag and drop operation by sending
+ * drag events to all the View objects in your application that are currently visible. It does
+ * this either by calling the View object's drag listener (an implementation of
+ * {@link android.view.View.OnDragListener#onDrag(View,DragEvent) onDrag()} or by calling the
+ * View object's {@link android.view.View#onDragEvent(DragEvent) onDragEvent()} method.
+ * Both are passed a {@link android.view.DragEvent} object that has a
+ * {@link android.view.DragEvent#getAction()} value of
+ * {@link android.view.DragEvent#ACTION_DRAG_STARTED}.
+ * </p>
+ * <p>
+ * Your application can invoke {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object,
+ * int) startDragAndDrop()} on any attached View object. The View object does not need to be
+ * the one used in {@link android.view.View.DragShadowBuilder}, nor does it need to be related
+ * to the View the user selected for dragging.
+ * </p>
+ * @param data A {@link android.content.ClipData} object pointing to the data to be
+ * transferred by the drag and drop operation.
+ * @param shadowBuilder A {@link android.view.View.DragShadowBuilder} object for building the
+ * drag shadow.
+ * @param myLocalState An {@link java.lang.Object} containing local data about the drag and
+ * drop operation. When dispatching drag events to views in the same activity this object
+ * will be available through {@link android.view.DragEvent#getLocalState()}. Views in other
+ * activities will not have access to this data ({@link android.view.DragEvent#getLocalState()}
+ * will return null).
+ * <p>
+ * myLocalState is a lightweight mechanism for the sending information from the dragged View
+ * to the target Views. For example, it can contain flags that differentiate between a
+ * a copy operation and a move operation.
+ * </p>
+ * @param flags Flags that control the drag and drop operation. This can be set to 0 for no
+ * flags, or any combination of the following:
+ * <ul>
+ * <li>{@link #DRAG_FLAG_GLOBAL}</li>
+ * <li>{@link #DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION}</li>
+ * <li>{@link #DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION}</li>
+ * <li>{@link #DRAG_FLAG_GLOBAL_URI_READ}</li>
+ * <li>{@link #DRAG_FLAG_GLOBAL_URI_WRITE}</li>
+ * <li>{@link #DRAG_FLAG_OPAQUE}</li>
+ * </ul>
+ * @return {@code true} if the method completes successfully, or
+ * {@code false} if it fails anywhere. Returning {@code false} means the system was unable to
+ * do a drag because of another ongoing operation or some other reasons.
+ */
+ public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder,
+ Object myLocalState, int flags) {
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "startDragAndDrop: data=" + data + " flags=" + flags);
+ }
+ if (mAttachInfo == null) {
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called on a detached view.");
+ return false;
+ }
+ if (!mAttachInfo.mViewRootImpl.mSurface.isValid()) {
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with an invalid surface.");
+ return false;
+ }
+
+ if (data != null) {
+ data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
+ }
+
+ Point shadowSize = new Point();
+ Point shadowTouchPoint = new Point();
+ shadowBuilder.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
+
+ if ((shadowSize.x < 0) || (shadowSize.y < 0)
+ || (shadowTouchPoint.x < 0) || (shadowTouchPoint.y < 0)) {
+ throw new IllegalStateException("Drag shadow dimensions must not be negative");
+ }
+
+ // Create 1x1 surface when zero surface size is specified because SurfaceControl.Builder
+ // does not accept zero size surface.
+ if (shadowSize.x == 0 || shadowSize.y == 0) {
+ if (!sAcceptZeroSizeDragShadow) {
+ throw new IllegalStateException("Drag shadow dimensions must be positive");
+ }
+ shadowSize.x = 1;
+ shadowSize.y = 1;
+ }
+
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "drag shadow: width=" + shadowSize.x + " height=" + shadowSize.y
+ + " shadowX=" + shadowTouchPoint.x + " shadowY=" + shadowTouchPoint.y);
+ }
+
+ final ViewRootImpl root = mAttachInfo.mViewRootImpl;
+ final SurfaceSession session = new SurfaceSession();
+ final SurfaceControl surfaceControl = new SurfaceControl.Builder(session)
+ .setName("drag surface")
+ .setParent(root.getSurfaceControl())
+ .setBufferSize(shadowSize.x, shadowSize.y)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .setCallsite("View.startDragAndDrop")
+ .build();
+ final Surface surface = new Surface();
+ surface.copyFrom(surfaceControl);
+ IBinder token = null;
+ try {
+ final Canvas canvas = isHardwareAccelerated()
+ ? surface.lockHardwareCanvas()
+ : surface.lockCanvas(null);
+ try {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ shadowBuilder.onDrawShadow(canvas);
+ } finally {
+ surface.unlockCanvasAndPost(canvas);
+ }
+
+ // repurpose 'shadowSize' for the last touch point
+ root.getLastTouchPoint(shadowSize);
+
+ token = mAttachInfo.mSession.performDrag(
+ mAttachInfo.mWindow, flags, surfaceControl, root.getLastTouchSource(),
+ shadowSize.x, shadowSize.y, shadowTouchPoint.x, shadowTouchPoint.y, data);
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "performDrag returned " + token);
+ }
+ if (token != null) {
+ if (mAttachInfo.mDragSurface != null) {
+ mAttachInfo.mDragSurface.release();
+ }
+ mAttachInfo.mDragSurface = surface;
+ mAttachInfo.mDragToken = token;
+ // Cache the local state object for delivery with DragEvents
+ root.setLocalDragState(myLocalState);
+ }
+ return token != null;
+ } catch (Exception e) {
+ Log.e(VIEW_LOG_TAG, "Unable to initiate drag", e);
+ return false;
+ } finally {
+ if (token == null) {
+ surface.destroy();
+ }
+ session.kill();
+ }
+ }
+
+ /**
+ * Cancels an ongoing drag and drop operation.
+ * <p>
+ * A {@link android.view.DragEvent} object with
+ * {@link android.view.DragEvent#getAction()} value of
+ * {@link android.view.DragEvent#ACTION_DRAG_ENDED} and
+ * {@link android.view.DragEvent#getResult()} value of {@code false}
+ * will be sent to every
+ * View that received {@link android.view.DragEvent#ACTION_DRAG_STARTED}
+ * even if they are not currently visible.
+ * </p>
+ * <p>
+ * This method can be called on any View in the same window as the View on which
+ * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int) startDragAndDrop}
+ * was called.
+ * </p>
+ */
+ public final void cancelDragAndDrop() {
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "cancelDragAndDrop");
+ }
+ if (mAttachInfo == null) {
+ Log.w(VIEW_LOG_TAG, "cancelDragAndDrop called on a detached view.");
+ return;
+ }
+ if (mAttachInfo.mDragToken != null) {
+ try {
+ mAttachInfo.mSession.cancelDragAndDrop(mAttachInfo.mDragToken, false);
+ } catch (Exception e) {
+ Log.e(VIEW_LOG_TAG, "Unable to cancel drag", e);
+ }
+ mAttachInfo.mDragToken = null;
+ } else {
+ Log.e(VIEW_LOG_TAG, "No active drag to cancel");
+ }
+ }
+
+ /**
+ * Updates the drag shadow for the ongoing drag and drop operation.
+ *
+ * @param shadowBuilder A {@link android.view.View.DragShadowBuilder} object for building the
+ * new drag shadow.
+ */
+ public final void updateDragShadow(DragShadowBuilder shadowBuilder) {
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "updateDragShadow");
+ }
+ if (mAttachInfo == null) {
+ Log.w(VIEW_LOG_TAG, "updateDragShadow called on a detached view.");
+ return;
+ }
+ if (mAttachInfo.mDragToken != null) {
+ try {
+ Canvas canvas = isHardwareAccelerated()
+ ? mAttachInfo.mDragSurface.lockHardwareCanvas()
+ : mAttachInfo.mDragSurface.lockCanvas(null);
+ try {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ shadowBuilder.onDrawShadow(canvas);
+ } finally {
+ mAttachInfo.mDragSurface.unlockCanvasAndPost(canvas);
+ }
+ } catch (Exception e) {
+ Log.e(VIEW_LOG_TAG, "Unable to update drag shadow", e);
+ }
+ } else {
+ Log.e(VIEW_LOG_TAG, "No active drag");
+ }
+ }
+
+ /**
+ * Starts a move from {startX, startY}, the amount of the movement will be the offset
+ * between {startX, startY} and the new cursor positon.
+ * @param startX horizontal coordinate where the move started.
+ * @param startY vertical coordinate where the move started.
+ * @return whether moving was started successfully.
+ * @hide
+ */
+ public final boolean startMovingTask(float startX, float startY) {
+ if (ViewDebug.DEBUG_POSITIONING) {
+ Log.d(VIEW_LOG_TAG, "startMovingTask: {" + startX + "," + startY + "}");
+ }
+ try {
+ return mAttachInfo.mSession.startMovingTask(mAttachInfo.mWindow, startX, startY);
+ } catch (RemoteException e) {
+ Log.e(VIEW_LOG_TAG, "Unable to start moving", e);
+ }
+ return false;
+ }
+
+ /**
+ * Finish a window move task.
+ * @hide
+ */
+ public void finishMovingTask() {
+ if (ViewDebug.DEBUG_POSITIONING) {
+ Log.d(VIEW_LOG_TAG, "finishMovingTask");
+ }
+ try {
+ mAttachInfo.mSession.finishMovingTask(mAttachInfo.mWindow);
+ } catch (RemoteException e) {
+ Log.e(VIEW_LOG_TAG, "Unable to finish moving", e);
+ }
+ }
+
+ /**
+ * Handles drag events sent by the system following a call to
+ * {@link android.view.View#startDragAndDrop(ClipData,DragShadowBuilder,Object,int)
+ * startDragAndDrop()}.
+ *<p>
+ * When the system calls this method, it passes a
+ * {@link android.view.DragEvent} object. A call to
+ * {@link android.view.DragEvent#getAction()} returns one of the action type constants defined
+ * in DragEvent. The method uses these to determine what is happening in the drag and drop
+ * operation.
+ * </p>
+ * <p>
+ * The default implementation returns false, except if an {@link OnReceiveContentListener}
+ * is {@link #setOnReceiveContentListener set} for this view. If an
+ * {@link OnReceiveContentListener} is set, the default implementation...
+ * <ul>
+ * <li>returns true for an
+ * {@link android.view.DragEvent#ACTION_DRAG_STARTED ACTION_DRAG_STARTED} event
+ * <li>calls {@link #performReceiveContent} for an
+ * {@link android.view.DragEvent#ACTION_DROP ACTION_DROP} event
+ * <li>returns true for an {@link android.view.DragEvent#ACTION_DROP ACTION_DROP} event, if
+ * the listener consumed some or all of the content
+ * </ul>
+ * </p>
+ *
+ * @param event The {@link android.view.DragEvent} sent by the system.
+ * The {@link android.view.DragEvent#getAction()} method returns an action type constant defined
+ * in DragEvent, indicating the type of drag event represented by this object.
+ * @return {@code true} if the method was successful, otherwise {@code false}.
+ * <p>
+ * The method should return {@code true} in response to an action type of
+ * {@link android.view.DragEvent#ACTION_DRAG_STARTED} to receive drag events for the current
+ * operation.
+ * </p>
+ * <p>
+ * The method should also return {@code true} in response to an action type of
+ * {@link android.view.DragEvent#ACTION_DROP} if it consumed the drop, or
+ * {@code false} if it didn't.
+ * </p>
+ * <p>
+ * For all other events, the return value is ignored.
+ * </p>
+ */
+ public boolean onDragEvent(DragEvent event) {
+ if (mListenerInfo == null || mListenerInfo.mOnReceiveContentListener == null) {
+ return false;
+ }
+ // Accept drag events by default if there's an OnReceiveContentListener set.
+ if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+ return true;
+ }
+ if (event.getAction() == DragEvent.ACTION_DROP) {
+ final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
+ if (permissions != null) {
+ permissions.takeTransient();
+ }
+ final ContentInfo payload =
+ new ContentInfo.Builder(event.getClipData(), SOURCE_DRAG_AND_DROP)
+ .setDragAndDropPermissions(permissions)
+ .build();
+ ContentInfo remainingPayload = performReceiveContent(payload);
+ // Return true unless none of the payload was consumed.
+ return remainingPayload != payload;
+ }
+ return false;
+ }
+
+ // Dispatches ACTION_DRAG_ENTERED and ACTION_DRAG_EXITED events for pre-Nougat apps.
+ boolean dispatchDragEnterExitInPreN(DragEvent event) {
+ return callDragEventHandler(event);
+ }
+
+ /**
+ * Detects if this View is enabled and has a drag event listener.
+ * If both are true, then it calls the drag event listener with the
+ * {@link android.view.DragEvent} it received. If the drag event listener returns
+ * {@code true}, then dispatchDragEvent() returns {@code true}.
+ * <p>
+ * For all other cases, the method calls the
+ * {@link android.view.View#onDragEvent(DragEvent) onDragEvent()} drag event handler
+ * method and returns its result.
+ * </p>
+ * <p>
+ * This ensures that a drag event is always consumed, even if the View does not have a drag
+ * event listener. However, if the View has a listener and the listener returns true, then
+ * onDragEvent() is not called.
+ * </p>
+ */
+ public boolean dispatchDragEvent(DragEvent event) {
+ event.mEventHandlerWasCalled = true;
+ if (event.mAction == DragEvent.ACTION_DRAG_LOCATION ||
+ event.mAction == DragEvent.ACTION_DROP) {
+ // About to deliver an event with coordinates to this view. Notify that now this view
+ // has drag focus. This will send exit/enter events as needed.
+ getViewRootImpl().setDragFocus(this, event);
+ }
+ return callDragEventHandler(event);
+ }
+
+ final boolean callDragEventHandler(DragEvent event) {
+ final boolean result;
+
+ ListenerInfo li = mListenerInfo;
+ //noinspection SimplifiableIfStatement
+ if (li != null && li.mOnDragListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
+ && li.mOnDragListener.onDrag(this, event)) {
+ result = true;
+ } else {
+ result = onDragEvent(event);
+ }
+
+ switch (event.mAction) {
+ case DragEvent.ACTION_DRAG_ENTERED: {
+ mPrivateFlags2 |= View.PFLAG2_DRAG_HOVERED;
+ refreshDrawableState();
+ } break;
+ case DragEvent.ACTION_DRAG_EXITED: {
+ mPrivateFlags2 &= ~View.PFLAG2_DRAG_HOVERED;
+ refreshDrawableState();
+ } break;
+ case DragEvent.ACTION_DRAG_ENDED: {
+ mPrivateFlags2 &= ~View.DRAG_MASK;
+ refreshDrawableState();
+ } break;
+ }
+
+ return result;
+ }
+
+ boolean canAcceptDrag() {
+ return (mPrivateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0;
+ }
+
+ /**
+ * This needs to be a better API (NOT ON VIEW) before it is exposed. If
+ * it is ever exposed at all.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void onCloseSystemDialogs(String reason) {
+ }
+
+ /**
+ * Given a Drawable whose bounds have been set to draw into this view,
+ * update a Region being computed for
+ * {@link #gatherTransparentRegion(android.graphics.Region)} so
+ * that any non-transparent parts of the Drawable are removed from the
+ * given transparent region.
+ *
+ * @param dr The Drawable whose transparency is to be applied to the region.
+ * @param region A Region holding the current transparency information,
+ * where any parts of the region that are set are considered to be
+ * transparent. On return, this region will be modified to have the
+ * transparency information reduced by the corresponding parts of the
+ * Drawable that are not transparent.
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void applyDrawableToTransparentRegion(Drawable dr, Region region) {
+ if (DBG) {
+ Log.i("View", "Getting transparent region for: " + this);
+ }
+ final Region r = dr.getTransparentRegion();
+ final Rect db = dr.getBounds();
+ final AttachInfo attachInfo = mAttachInfo;
+ if (r != null && attachInfo != null) {
+ final int w = getRight()-getLeft();
+ final int h = getBottom()-getTop();
+ if (db.left > 0) {
+ //Log.i("VIEW", "Drawable left " + db.left + " > view 0");
+ r.op(0, 0, db.left, h, Region.Op.UNION);
+ }
+ if (db.right < w) {
+ //Log.i("VIEW", "Drawable right " + db.right + " < view " + w);
+ r.op(db.right, 0, w, h, Region.Op.UNION);
+ }
+ if (db.top > 0) {
+ //Log.i("VIEW", "Drawable top " + db.top + " > view 0");
+ r.op(0, 0, w, db.top, Region.Op.UNION);
+ }
+ if (db.bottom < h) {
+ //Log.i("VIEW", "Drawable bottom " + db.bottom + " < view " + h);
+ r.op(0, db.bottom, w, h, Region.Op.UNION);
+ }
+ final int[] location = attachInfo.mTransparentLocation;
+ getLocationInWindow(location);
+ r.translate(location[0], location[1]);
+ region.op(r, Region.Op.INTERSECT);
+ } else {
+ region.op(db, Region.Op.DIFFERENCE);
+ }
+ }
+
+ private void checkForLongClick(long delay, float x, float y, int classification) {
+ if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
+ mHasPerformedLongPress = false;
+
+ if (mPendingCheckForLongPress == null) {
+ mPendingCheckForLongPress = new CheckForLongPress();
+ }
+ mPendingCheckForLongPress.setAnchor(x, y);
+ mPendingCheckForLongPress.rememberWindowAttachCount();
+ mPendingCheckForLongPress.rememberPressedState();
+ mPendingCheckForLongPress.setClassification(classification);
+ postDelayed(mPendingCheckForLongPress, delay);
+ }
+ }
+
+ /**
+ * Inflate a view from an XML resource. This convenience method wraps the {@link
+ * LayoutInflater} class, which provides a full range of options for view inflation.
+ *
+ * @param context The Context object for your activity or application.
+ * @param resource The resource ID to inflate
+ * @param root A view group that will be the parent. Used to properly inflate the
+ * layout_* parameters.
+ * @see LayoutInflater
+ */
+ public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
+ LayoutInflater factory = LayoutInflater.from(context);
+ return factory.inflate(resource, root);
+ }
+
+ /**
+ * Scroll the view with standard behavior for scrolling beyond the normal
+ * content boundaries. Views that call this method should override
+ * {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the
+ * results of an over-scroll operation.
+ *
+ * Views can use this method to handle any touch or fling-based scrolling.
+ *
+ * @param deltaX Change in X in pixels
+ * @param deltaY Change in Y in pixels
+ * @param scrollX Current X scroll value in pixels before applying deltaX
+ * @param scrollY Current Y scroll value in pixels before applying deltaY
+ * @param scrollRangeX Maximum content scroll range along the X axis
+ * @param scrollRangeY Maximum content scroll range along the Y axis
+ * @param maxOverScrollX Number of pixels to overscroll by in either direction
+ * along the X axis.
+ * @param maxOverScrollY Number of pixels to overscroll by in either direction
+ * along the Y axis.
+ * @param isTouchEvent true if this scroll operation is the result of a touch event.
+ * @return true if scrolling was clamped to an over-scroll boundary along either
+ * axis, false otherwise.
+ */
+ @SuppressWarnings({"UnusedParameters"})
+ protected boolean overScrollBy(int deltaX, int deltaY,
+ int scrollX, int scrollY,
+ int scrollRangeX, int scrollRangeY,
+ int maxOverScrollX, int maxOverScrollY,
+ boolean isTouchEvent) {
+ final int overScrollMode = mOverScrollMode;
+ final boolean canScrollHorizontal =
+ computeHorizontalScrollRange() > computeHorizontalScrollExtent();
+ final boolean canScrollVertical =
+ computeVerticalScrollRange() > computeVerticalScrollExtent();
+ final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
+ (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
+ final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
+ (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
+
+ int newScrollX = scrollX + deltaX;
+ if (!overScrollHorizontal) {
+ maxOverScrollX = 0;
+ }
+
+ int newScrollY = scrollY + deltaY;
+ if (!overScrollVertical) {
+ maxOverScrollY = 0;
+ }
+
+ // Clamp values if at the limits and record
+ final int left = -maxOverScrollX;
+ final int right = maxOverScrollX + scrollRangeX;
+ final int top = -maxOverScrollY;
+ final int bottom = maxOverScrollY + scrollRangeY;
+
+ boolean clampedX = false;
+ if (newScrollX > right) {
+ newScrollX = right;
+ clampedX = true;
+ } else if (newScrollX < left) {
+ newScrollX = left;
+ clampedX = true;
+ }
+
+ boolean clampedY = false;
+ if (newScrollY > bottom) {
+ newScrollY = bottom;
+ clampedY = true;
+ } else if (newScrollY < top) {
+ newScrollY = top;
+ clampedY = true;
+ }
+
+ onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
+
+ return clampedX || clampedY;
+ }
+
+ /**
+ * Called by {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)} to
+ * respond to the results of an over-scroll operation.
+ *
+ * @param scrollX New X scroll value in pixels
+ * @param scrollY New Y scroll value in pixels
+ * @param clampedX True if scrollX was clamped to an over-scroll boundary
+ * @param clampedY True if scrollY was clamped to an over-scroll boundary
+ */
+ protected void onOverScrolled(int scrollX, int scrollY,
+ boolean clampedX, boolean clampedY) {
+ // Intentionally empty.
+ }
+
+ /**
+ * Returns the over-scroll mode for this view. The result will be
+ * one of {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS}
+ * (allow over-scrolling only if the view content is larger than the container),
+ * or {@link #OVER_SCROLL_NEVER}.
+ *
+ * @return This view's over-scroll mode.
+ */
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = OVER_SCROLL_ALWAYS, name = "always"),
+ @EnumEntry(value = OVER_SCROLL_IF_CONTENT_SCROLLS, name = "ifContentScrolls"),
+ @EnumEntry(value = OVER_SCROLL_NEVER, name = "never")
+ })
+ public int getOverScrollMode() {
+ return mOverScrollMode;
+ }
+
+ /**
+ * Set the over-scroll mode for this view. Valid over-scroll modes are
+ * {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS}
+ * (allow over-scrolling only if the view content is larger than the container),
+ * or {@link #OVER_SCROLL_NEVER}.
+ *
+ * Setting the over-scroll mode of a view will have an effect only if the
+ * view is capable of scrolling.
+ *
+ * @param overScrollMode The new over-scroll mode for this view.
+ */
+ public void setOverScrollMode(int overScrollMode) {
+ if (overScrollMode != OVER_SCROLL_ALWAYS &&
+ overScrollMode != OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ overScrollMode != OVER_SCROLL_NEVER) {
+ throw new IllegalArgumentException("Invalid overscroll mode " + overScrollMode);
+ }
+ mOverScrollMode = overScrollMode;
+ }
+
+ /**
+ * Enable or disable nested scrolling for this view.
+ *
+ * <p>If this property is set to true the view will be permitted to initiate nested
+ * scrolling operations with a compatible parent view in the current hierarchy. If this
+ * view does not implement nested scrolling this will have no effect. Disabling nested scrolling
+ * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping}
+ * the nested scroll.</p>
+ *
+ * @param enabled true to enable nested scrolling, false to disable
+ *
+ * @see #isNestedScrollingEnabled()
+ */
+ public void setNestedScrollingEnabled(boolean enabled) {
+ if (enabled) {
+ mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED;
+ } else {
+ stopNestedScroll();
+ mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED;
+ }
+ }
+
+ /**
+ * Returns true if nested scrolling is enabled for this view.
+ *
+ * <p>If nested scrolling is enabled and this View class implementation supports it,
+ * this view will act as a nested scrolling child view when applicable, forwarding data
+ * about the scroll operation in progress to a compatible and cooperating nested scrolling
+ * parent.</p>
+ *
+ * @return true if nested scrolling is enabled
+ *
+ * @see #setNestedScrollingEnabled(boolean)
+ */
+ @InspectableProperty
+ public boolean isNestedScrollingEnabled() {
+ return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) ==
+ PFLAG3_NESTED_SCROLLING_ENABLED;
+ }
+
+ /**
+ * Begin a nestable scroll operation along the given axes.
+ *
+ * <p>A view starting a nested scroll promises to abide by the following contract:</p>
+ *
+ * <p>The view will call startNestedScroll upon initiating a scroll operation. In the case
+ * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}.
+ * In the case of touch scrolling the nested scroll will be terminated automatically in
+ * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}.
+ * In the event of programmatic scrolling the caller must explicitly call
+ * {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p>
+ *
+ * <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found.
+ * If it returns false the caller may ignore the rest of this contract until the next scroll.
+ * Calling startNestedScroll while a nested scroll is already in progress will return true.</p>
+ *
+ * <p>At each incremental step of the scroll the caller should invoke
+ * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll}
+ * once it has calculated the requested scrolling delta. If it returns true the nested scrolling
+ * parent at least partially consumed the scroll and the caller should adjust the amount it
+ * scrolls by.</p>
+ *
+ * <p>After applying the remainder of the scroll delta the caller should invoke
+ * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing
+ * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat
+ * these values differently. See {@link ViewParent#onNestedScroll(View, int, int, int, int)}.
+ * </p>
+ *
+ * @param axes Flags consisting of a combination of {@link #SCROLL_AXIS_HORIZONTAL} and/or
+ * {@link #SCROLL_AXIS_VERTICAL}.
+ * @return true if a cooperative parent was found and nested scrolling has been enabled for
+ * the current gesture.
+ *
+ * @see #stopNestedScroll()
+ * @see #dispatchNestedPreScroll(int, int, int[], int[])
+ * @see #dispatchNestedScroll(int, int, int, int, int[])
+ */
+ public boolean startNestedScroll(int axes) {
+ if (hasNestedScrollingParent()) {
+ // Already in progress
+ return true;
+ }
+ if (isNestedScrollingEnabled()) {
+ ViewParent p = getParent();
+ View child = this;
+ while (p != null) {
+ try {
+ if (p.onStartNestedScroll(child, this, axes)) {
+ mNestedScrollingParent = p;
+ p.onNestedScrollAccepted(child, this, axes);
+ return true;
+ }
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
+ "method onStartNestedScroll", e);
+ // Allow the search upward to continue
+ }
+ if (p instanceof View) {
+ child = (View) p;
+ }
+ p = p.getParent();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Stop a nested scroll in progress.
+ *
+ * <p>Calling this method when a nested scroll is not currently in progress is harmless.</p>
+ *
+ * @see #startNestedScroll(int)
+ */
+ public void stopNestedScroll() {
+ if (mNestedScrollingParent != null) {
+ mNestedScrollingParent.onStopNestedScroll(this);
+ mNestedScrollingParent = null;
+ }
+ }
+
+ /**
+ * Returns true if this view has a nested scrolling parent.
+ *
+ * <p>The presence of a nested scrolling parent indicates that this view has initiated
+ * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.</p>
+ *
+ * @return whether this view has a nested scrolling parent
+ */
+ public boolean hasNestedScrollingParent() {
+ return mNestedScrollingParent != null;
+ }
+
+ /**
+ * Dispatch one step of a nested scroll in progress.
+ *
+ * <p>Implementations of views that support nested scrolling should call this to report
+ * info about a scroll in progress to the current nested scrolling parent. If a nested scroll
+ * is not currently in progress or nested scrolling is not
+ * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p>
+ *
+ * <p>Compatible View implementations should also call
+ * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before
+ * consuming a component of the scroll event themselves.</p>
+ *
+ * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
+ * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
+ * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view
+ * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view
+ * @param offsetInWindow Optional. If not null, on return this will contain the offset
+ * in local view coordinates of this view from before this operation
+ * to after it completes. View implementations may use this to adjust
+ * expected input coordinate tracking.
+ * @return true if the event was dispatched, false if it could not be dispatched.
+ * @see #dispatchNestedPreScroll(int, int, int[], int[])
+ */
+ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) {
+ if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
+ if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
+ int startX = 0;
+ int startY = 0;
+ if (offsetInWindow != null) {
+ getLocationInWindow(offsetInWindow);
+ startX = offsetInWindow[0];
+ startY = offsetInWindow[1];
+ }
+
+ mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
+ dxUnconsumed, dyUnconsumed);
+
+ if (offsetInWindow != null) {
+ getLocationInWindow(offsetInWindow);
+ offsetInWindow[0] -= startX;
+ offsetInWindow[1] -= startY;
+ }
+ return true;
+ } else if (offsetInWindow != null) {
+ // No motion, no dispatch. Keep offsetInWindow up to date.
+ offsetInWindow[0] = 0;
+ offsetInWindow[1] = 0;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
+ *
+ * <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
+ * <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested
+ * scrolling operation to consume some or all of the scroll operation before the child view
+ * consumes it.</p>
+ *
+ * @param dx Horizontal scroll distance in pixels
+ * @param dy Vertical scroll distance in pixels
+ * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx
+ * and consumed[1] the consumed dy.
+ * @param offsetInWindow Optional. If not null, on return this will contain the offset
+ * in local view coordinates of this view from before this operation
+ * to after it completes. View implementations may use this to adjust
+ * expected input coordinate tracking.
+ * @return true if the parent consumed some or all of the scroll delta
+ * @see #dispatchNestedScroll(int, int, int, int, int[])
+ */
+ public boolean dispatchNestedPreScroll(int dx, int dy,
+ @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
+ if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
+ if (dx != 0 || dy != 0) {
+ int startX = 0;
+ int startY = 0;
+ if (offsetInWindow != null) {
+ getLocationInWindow(offsetInWindow);
+ startX = offsetInWindow[0];
+ startY = offsetInWindow[1];
+ }
+
+ if (consumed == null) {
+ if (mTempNestedScrollConsumed == null) {
+ mTempNestedScrollConsumed = new int[2];
+ }
+ consumed = mTempNestedScrollConsumed;
+ }
+ consumed[0] = 0;
+ consumed[1] = 0;
+ mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
+
+ if (offsetInWindow != null) {
+ getLocationInWindow(offsetInWindow);
+ offsetInWindow[0] -= startX;
+ offsetInWindow[1] -= startY;
+ }
+ return consumed[0] != 0 || consumed[1] != 0;
+ } else if (offsetInWindow != null) {
+ offsetInWindow[0] = 0;
+ offsetInWindow[1] = 0;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Dispatch a fling to a nested scrolling parent.
+ *
+ * <p>This method should be used to indicate that a nested scrolling child has detected
+ * suitable conditions for a fling. Generally this means that a touch scroll has ended with a
+ * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
+ * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
+ * along a scrollable axis.</p>
+ *
+ * <p>If a nested scrolling child view would normally fling but it is at the edge of
+ * its own content, it can use this method to delegate the fling to its nested scrolling
+ * parent instead. The parent may optionally consume the fling or observe a child fling.</p>
+ *
+ * @param velocityX Horizontal fling velocity in pixels per second
+ * @param velocityY Vertical fling velocity in pixels per second
+ * @param consumed true if the child consumed the fling, false otherwise
+ * @return true if the nested scrolling parent consumed or otherwise reacted to the fling
+ */
+ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+ if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
+ return mNestedScrollingParent.onNestedFling(this, velocityX, velocityY, consumed);
+ }
+ return false;
+ }
+
+ /**
+ * Dispatch a fling to a nested scrolling parent before it is processed by this view.
+ *
+ * <p>Nested pre-fling events are to nested fling events what touch intercept is to touch
+ * and what nested pre-scroll is to nested scroll. <code>dispatchNestedPreFling</code>
+ * offsets an opportunity for the parent view in a nested fling to fully consume the fling
+ * before the child view consumes it. If this method returns <code>true</code>, a nested
+ * parent view consumed the fling and this view should not scroll as a result.</p>
+ *
+ * <p>For a better user experience, only one view in a nested scrolling chain should consume
+ * the fling at a time. If a parent view consumed the fling this method will return false.
+ * Custom view implementations should account for this in two ways:</p>
+ *
+ * <ul>
+ * <li>If a custom view is paged and needs to settle to a fixed page-point, do not
+ * call <code>dispatchNestedPreFling</code>; consume the fling and settle to a valid
+ * position regardless.</li>
+ * <li>If a nested parent does consume the fling, this view should not scroll at all,
+ * even to settle back to a valid idle position.</li>
+ * </ul>
+ *
+ * <p>Views should also not offer fling velocities to nested parent views along an axis
+ * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView}
+ * should not offer a horizontal fling velocity to its parents since scrolling along that
+ * axis is not permitted and carrying velocity along that motion does not make sense.</p>
+ *
+ * @param velocityX Horizontal fling velocity in pixels per second
+ * @param velocityY Vertical fling velocity in pixels per second
+ * @return true if a nested scrolling parent consumed the fling
+ */
+ public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+ if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
+ return mNestedScrollingParent.onNestedPreFling(this, velocityX, velocityY);
+ }
+ return false;
+ }
+
+ /**
+ * Gets a scale factor that determines the distance the view should scroll
+ * vertically in response to {@link MotionEvent#ACTION_SCROLL}.
+ * @return The vertical scroll scale factor.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected float getVerticalScrollFactor() {
+ if (mVerticalScrollFactor == 0) {
+ TypedValue outValue = new TypedValue();
+ if (!mContext.getTheme().resolveAttribute(
+ com.android.internal.R.attr.listPreferredItemHeight, outValue, true)) {
+ throw new IllegalStateException(
+ "Expected theme to define listPreferredItemHeight.");
+ }
+ mVerticalScrollFactor = outValue.getDimension(
+ mContext.getResources().getDisplayMetrics());
+ }
+ return mVerticalScrollFactor;
+ }
+
+ /**
+ * Gets a scale factor that determines the distance the view should scroll
+ * horizontally in response to {@link MotionEvent#ACTION_SCROLL}.
+ * @return The horizontal scroll scale factor.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected float getHorizontalScrollFactor() {
+ // TODO: Should use something else.
+ return getVerticalScrollFactor();
+ }
+
+ /**
+ * Return the value specifying the text direction or policy that was set with
+ * {@link #setTextDirection(int)}.
+ *
+ * @return the defined text direction. It can be one of:
+ *
+ * {@link #TEXT_DIRECTION_INHERIT},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG},
+ * {@link #TEXT_DIRECTION_ANY_RTL},
+ * {@link #TEXT_DIRECTION_LTR},
+ * {@link #TEXT_DIRECTION_RTL},
+ * {@link #TEXT_DIRECTION_LOCALE},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG_LTR},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG_RTL}
+ *
+ * @attr ref android.R.styleable#View_textDirection
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "text", mapping = {
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_INHERIT, to = "INHERIT"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_LOCALE, to = "LOCALE"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG_LTR, to = "FIRST_STRONG_LTR"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG_RTL, to = "FIRST_STRONG_RTL")
+ })
+ @InspectableProperty(hasAttributeId = false, enumMapping = {
+ @EnumEntry(value = TEXT_DIRECTION_INHERIT, name = "inherit"),
+ @EnumEntry(value = TEXT_DIRECTION_LOCALE, name = "locale"),
+ @EnumEntry(value = TEXT_DIRECTION_ANY_RTL, name = "anyRtl"),
+ @EnumEntry(value = TEXT_DIRECTION_LTR, name = "ltr"),
+ @EnumEntry(value = TEXT_DIRECTION_RTL, name = "rtl"),
+ @EnumEntry(value = TEXT_DIRECTION_FIRST_STRONG, name = "firstStrong"),
+ @EnumEntry(value = TEXT_DIRECTION_FIRST_STRONG_LTR, name = "firstStrongLtr"),
+ @EnumEntry(value = TEXT_DIRECTION_FIRST_STRONG_RTL, name = "firstStrongRtl"),
+ })
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getRawTextDirection() {
+ return (mPrivateFlags2 & PFLAG2_TEXT_DIRECTION_MASK) >> PFLAG2_TEXT_DIRECTION_MASK_SHIFT;
+ }
+
+ /**
+ * Set the text direction.
+ *
+ * @param textDirection the direction to set. Should be one of:
+ *
+ * {@link #TEXT_DIRECTION_INHERIT},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG},
+ * {@link #TEXT_DIRECTION_ANY_RTL},
+ * {@link #TEXT_DIRECTION_LTR},
+ * {@link #TEXT_DIRECTION_RTL},
+ * {@link #TEXT_DIRECTION_LOCALE}
+ * {@link #TEXT_DIRECTION_FIRST_STRONG_LTR},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG_RTL},
+ *
+ * Resolution will be done if the value is set to TEXT_DIRECTION_INHERIT. The resolution
+ * proceeds up the parent chain of the view to get the value. If there is no parent, then it will
+ * return the default {@link #TEXT_DIRECTION_FIRST_STRONG}.
+ *
+ * @attr ref android.R.styleable#View_textDirection
+ */
+ public void setTextDirection(int textDirection) {
+ if (getRawTextDirection() != textDirection) {
+ // Reset the current text direction and the resolved one
+ mPrivateFlags2 &= ~PFLAG2_TEXT_DIRECTION_MASK;
+ resetResolvedTextDirection();
+ // Set the new text direction
+ mPrivateFlags2 |= ((textDirection << PFLAG2_TEXT_DIRECTION_MASK_SHIFT) & PFLAG2_TEXT_DIRECTION_MASK);
+ // Do resolution
+ resolveTextDirection();
+ // Notify change
+ onRtlPropertiesChanged(getLayoutDirection());
+ // Refresh
+ requestLayout();
+ invalidate(true);
+ }
+ }
+
+ /**
+ * Return the resolved text direction.
+ *
+ * @return the resolved text direction. Returns one of:
+ *
+ * {@link #TEXT_DIRECTION_FIRST_STRONG},
+ * {@link #TEXT_DIRECTION_ANY_RTL},
+ * {@link #TEXT_DIRECTION_LTR},
+ * {@link #TEXT_DIRECTION_RTL},
+ * {@link #TEXT_DIRECTION_LOCALE},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG_LTR},
+ * {@link #TEXT_DIRECTION_FIRST_STRONG_RTL}
+ *
+ * @attr ref android.R.styleable#View_textDirection
+ */
+ @ViewDebug.ExportedProperty(category = "text", mapping = {
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_INHERIT, to = "INHERIT"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_LOCALE, to = "LOCALE"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG_LTR, to = "FIRST_STRONG_LTR"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG_RTL, to = "FIRST_STRONG_RTL")
+ })
+ @InspectableProperty(hasAttributeId = false, enumMapping = {
+ @EnumEntry(value = TEXT_DIRECTION_LOCALE, name = "locale"),
+ @EnumEntry(value = TEXT_DIRECTION_ANY_RTL, name = "anyRtl"),
+ @EnumEntry(value = TEXT_DIRECTION_LTR, name = "ltr"),
+ @EnumEntry(value = TEXT_DIRECTION_RTL, name = "rtl"),
+ @EnumEntry(value = TEXT_DIRECTION_FIRST_STRONG, name = "firstStrong"),
+ @EnumEntry(value = TEXT_DIRECTION_FIRST_STRONG_LTR, name = "firstStrongLtr"),
+ @EnumEntry(value = TEXT_DIRECTION_FIRST_STRONG_RTL, name = "firstStrongRtl"),
+ })
+ public int getTextDirection() {
+ return (mPrivateFlags2 & PFLAG2_TEXT_DIRECTION_RESOLVED_MASK) >> PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT;
+ }
+
+ /**
+ * Resolve the text direction.
+ *
+ * @return true if resolution has been done, false otherwise.
+ *
+ * @hide
+ */
+ public boolean resolveTextDirection() {
+ // Reset any previous text direction resolution
+ mPrivateFlags2 &= ~(PFLAG2_TEXT_DIRECTION_RESOLVED | PFLAG2_TEXT_DIRECTION_RESOLVED_MASK);
+
+ if (hasRtlSupport()) {
+ // Set resolved text direction flag depending on text direction flag
+ final int textDirection = getRawTextDirection();
+ switch(textDirection) {
+ case TEXT_DIRECTION_INHERIT:
+ if (!canResolveTextDirection()) {
+ // We cannot do the resolution if there is no parent, so use the default one
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ // Resolution will need to happen again later
+ return false;
+ }
+
+ // Parent has not yet resolved, so we still return the default
+ try {
+ if (!mParent.isTextDirectionResolved()) {
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ // Resolution will need to happen again later
+ return false;
+ }
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED |
+ PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ return true;
+ }
+
+ // Set current resolved direction to the same value as the parent's one
+ int parentResolvedDirection;
+ try {
+ parentResolvedDirection = mParent.getTextDirection();
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ parentResolvedDirection = TEXT_DIRECTION_LTR;
+ }
+ switch (parentResolvedDirection) {
+ case TEXT_DIRECTION_FIRST_STRONG:
+ case TEXT_DIRECTION_ANY_RTL:
+ case TEXT_DIRECTION_LTR:
+ case TEXT_DIRECTION_RTL:
+ case TEXT_DIRECTION_LOCALE:
+ case TEXT_DIRECTION_FIRST_STRONG_LTR:
+ case TEXT_DIRECTION_FIRST_STRONG_RTL:
+ mPrivateFlags2 |=
+ (parentResolvedDirection << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT);
+ break;
+ default:
+ // Default resolved direction is "first strong" heuristic
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ }
+ break;
+ case TEXT_DIRECTION_FIRST_STRONG:
+ case TEXT_DIRECTION_ANY_RTL:
+ case TEXT_DIRECTION_LTR:
+ case TEXT_DIRECTION_RTL:
+ case TEXT_DIRECTION_LOCALE:
+ case TEXT_DIRECTION_FIRST_STRONG_LTR:
+ case TEXT_DIRECTION_FIRST_STRONG_RTL:
+ // Resolved direction is the same as text direction
+ mPrivateFlags2 |= (textDirection << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT);
+ break;
+ default:
+ // Default resolved direction is "first strong" heuristic
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ }
+ } else {
+ // Default resolved direction is "first strong" heuristic
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ }
+
+ // Set to resolved
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED;
+ return true;
+ }
+
+ /**
+ * Check if text direction resolution can be done.
+ *
+ * @return true if text direction resolution can be done otherwise return false.
+ */
+ public boolean canResolveTextDirection() {
+ switch (getRawTextDirection()) {
+ case TEXT_DIRECTION_INHERIT:
+ if (mParent != null) {
+ try {
+ return mParent.canResolveTextDirection();
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Reset resolved text direction. Text direction will be resolved during a call to
+ * {@link #onMeasure(int, int)}.
+ *
+ * @hide
+ */
+ @TestApi
+ public void resetResolvedTextDirection() {
+ // Reset any previous text direction resolution
+ mPrivateFlags2 &= ~(PFLAG2_TEXT_DIRECTION_RESOLVED | PFLAG2_TEXT_DIRECTION_RESOLVED_MASK);
+ // Set to default value
+ mPrivateFlags2 |= PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT;
+ }
+
+ /**
+ * @return true if text direction is inherited.
+ *
+ * @hide
+ */
+ public boolean isTextDirectionInherited() {
+ return (getRawTextDirection() == TEXT_DIRECTION_INHERIT);
+ }
+
+ /**
+ * @return true if text direction is resolved.
+ */
+ public boolean isTextDirectionResolved() {
+ return (mPrivateFlags2 & PFLAG2_TEXT_DIRECTION_RESOLVED) == PFLAG2_TEXT_DIRECTION_RESOLVED;
+ }
+
+ /**
+ * Return the value specifying the text alignment or policy that was set with
+ * {@link #setTextAlignment(int)}.
+ *
+ * @return the defined text alignment. It can be one of:
+ *
+ * {@link #TEXT_ALIGNMENT_INHERIT},
+ * {@link #TEXT_ALIGNMENT_GRAVITY},
+ * {@link #TEXT_ALIGNMENT_CENTER},
+ * {@link #TEXT_ALIGNMENT_TEXT_START},
+ * {@link #TEXT_ALIGNMENT_TEXT_END},
+ * {@link #TEXT_ALIGNMENT_VIEW_START},
+ * {@link #TEXT_ALIGNMENT_VIEW_END}
+ *
+ * @attr ref android.R.styleable#View_textAlignment
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "text", mapping = {
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_INHERIT, to = "INHERIT"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_GRAVITY, to = "GRAVITY"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_TEXT_START, to = "TEXT_START"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_TEXT_END, to = "TEXT_END"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_CENTER, to = "CENTER"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_START, to = "VIEW_START"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_END, to = "VIEW_END")
+ })
+ @InspectableProperty(hasAttributeId = false, enumMapping = {
+ @EnumEntry(value = TEXT_ALIGNMENT_INHERIT, name = "inherit"),
+ @EnumEntry(value = TEXT_ALIGNMENT_GRAVITY, name = "gravity"),
+ @EnumEntry(value = TEXT_ALIGNMENT_TEXT_START, name = "textStart"),
+ @EnumEntry(value = TEXT_ALIGNMENT_TEXT_END, name = "textEnd"),
+ @EnumEntry(value = TEXT_ALIGNMENT_CENTER, name = "center"),
+ @EnumEntry(value = TEXT_ALIGNMENT_VIEW_START, name = "viewStart"),
+ @EnumEntry(value = TEXT_ALIGNMENT_VIEW_END, name = "viewEnd")
+ })
+ @TextAlignment
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getRawTextAlignment() {
+ return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_MASK) >> PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT;
+ }
+
+ /**
+ * Set the text alignment.
+ *
+ * @param textAlignment The text alignment to set. Should be one of
+ *
+ * {@link #TEXT_ALIGNMENT_INHERIT},
+ * {@link #TEXT_ALIGNMENT_GRAVITY},
+ * {@link #TEXT_ALIGNMENT_CENTER},
+ * {@link #TEXT_ALIGNMENT_TEXT_START},
+ * {@link #TEXT_ALIGNMENT_TEXT_END},
+ * {@link #TEXT_ALIGNMENT_VIEW_START},
+ * {@link #TEXT_ALIGNMENT_VIEW_END}
+ *
+ * Resolution will be done if the value is set to TEXT_ALIGNMENT_INHERIT. The resolution
+ * proceeds up the parent chain of the view to get the value. If there is no parent, then it
+ * will return the default {@link #TEXT_ALIGNMENT_GRAVITY}.
+ *
+ * @attr ref android.R.styleable#View_textAlignment
+ */
+ public void setTextAlignment(@TextAlignment int textAlignment) {
+ if (textAlignment != getRawTextAlignment()) {
+ // Reset the current and resolved text alignment
+ mPrivateFlags2 &= ~PFLAG2_TEXT_ALIGNMENT_MASK;
+ resetResolvedTextAlignment();
+ // Set the new text alignment
+ mPrivateFlags2 |=
+ ((textAlignment << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT) & PFLAG2_TEXT_ALIGNMENT_MASK);
+ // Do resolution
+ resolveTextAlignment();
+ // Notify change
+ onRtlPropertiesChanged(getLayoutDirection());
+ // Refresh
+ requestLayout();
+ invalidate(true);
+ }
+ }
+
+ /**
+ * Return the resolved text alignment.
+ *
+ * @return the resolved text alignment. Returns one of:
+ *
+ * {@link #TEXT_ALIGNMENT_GRAVITY},
+ * {@link #TEXT_ALIGNMENT_CENTER},
+ * {@link #TEXT_ALIGNMENT_TEXT_START},
+ * {@link #TEXT_ALIGNMENT_TEXT_END},
+ * {@link #TEXT_ALIGNMENT_VIEW_START},
+ * {@link #TEXT_ALIGNMENT_VIEW_END}
+ *
+ * @attr ref android.R.styleable#View_textAlignment
+ */
+ @ViewDebug.ExportedProperty(category = "text", mapping = {
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_INHERIT, to = "INHERIT"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_GRAVITY, to = "GRAVITY"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_TEXT_START, to = "TEXT_START"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_TEXT_END, to = "TEXT_END"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_CENTER, to = "CENTER"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_START, to = "VIEW_START"),
+ @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_END, to = "VIEW_END")
+ })
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = TEXT_ALIGNMENT_GRAVITY, name = "gravity"),
+ @EnumEntry(value = TEXT_ALIGNMENT_TEXT_START, name = "textStart"),
+ @EnumEntry(value = TEXT_ALIGNMENT_TEXT_END, name = "textEnd"),
+ @EnumEntry(value = TEXT_ALIGNMENT_CENTER, name = "center"),
+ @EnumEntry(value = TEXT_ALIGNMENT_VIEW_START, name = "viewStart"),
+ @EnumEntry(value = TEXT_ALIGNMENT_VIEW_END, name = "viewEnd")
+ })
+ @TextAlignment
+ public int getTextAlignment() {
+ return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK) >>
+ PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT;
+ }
+
+ /**
+ * Resolve the text alignment.
+ *
+ * @return true if resolution has been done, false otherwise.
+ *
+ * @hide
+ */
+ public boolean resolveTextAlignment() {
+ // Reset any previous text alignment resolution
+ mPrivateFlags2 &= ~(PFLAG2_TEXT_ALIGNMENT_RESOLVED | PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK);
+
+ if (hasRtlSupport()) {
+ // Set resolved text alignment flag depending on text alignment flag
+ final int textAlignment = getRawTextAlignment();
+ switch (textAlignment) {
+ case TEXT_ALIGNMENT_INHERIT:
+ // Check if we can resolve the text alignment
+ if (!canResolveTextAlignment()) {
+ // We cannot do the resolution if there is no parent so use the default
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ // Resolution will need to happen again later
+ return false;
+ }
+
+ // Parent has not yet resolved, so we still return the default
+ try {
+ if (!mParent.isTextAlignmentResolved()) {
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ // Resolution will need to happen again later
+ return false;
+ }
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED |
+ PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ return true;
+ }
+
+ int parentResolvedTextAlignment;
+ try {
+ parentResolvedTextAlignment = mParent.getTextAlignment();
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ parentResolvedTextAlignment = TEXT_ALIGNMENT_GRAVITY;
+ }
+ switch (parentResolvedTextAlignment) {
+ case TEXT_ALIGNMENT_GRAVITY:
+ case TEXT_ALIGNMENT_TEXT_START:
+ case TEXT_ALIGNMENT_TEXT_END:
+ case TEXT_ALIGNMENT_CENTER:
+ case TEXT_ALIGNMENT_VIEW_START:
+ case TEXT_ALIGNMENT_VIEW_END:
+ // Resolved text alignment is the same as the parent resolved
+ // text alignment
+ mPrivateFlags2 |=
+ (parentResolvedTextAlignment << PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT);
+ break;
+ default:
+ // Use default resolved text alignment
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ }
+ break;
+ case TEXT_ALIGNMENT_GRAVITY:
+ case TEXT_ALIGNMENT_TEXT_START:
+ case TEXT_ALIGNMENT_TEXT_END:
+ case TEXT_ALIGNMENT_CENTER:
+ case TEXT_ALIGNMENT_VIEW_START:
+ case TEXT_ALIGNMENT_VIEW_END:
+ // Resolved text alignment is the same as text alignment
+ mPrivateFlags2 |= (textAlignment << PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT);
+ break;
+ default:
+ // Use default resolved text alignment
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ }
+ } else {
+ // Use default resolved text alignment
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ }
+
+ // Set the resolved
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED;
+ return true;
+ }
+
+ /**
+ * Check if text alignment resolution can be done.
+ *
+ * @return true if text alignment resolution can be done otherwise return false.
+ */
+ public boolean canResolveTextAlignment() {
+ switch (getRawTextAlignment()) {
+ case TEXT_DIRECTION_INHERIT:
+ if (mParent != null) {
+ try {
+ return mParent.canResolveTextAlignment();
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Reset resolved text alignment. Text alignment will be resolved during a call to
+ * {@link #onMeasure(int, int)}.
+ *
+ * @hide
+ */
+ @TestApi
+ public void resetResolvedTextAlignment() {
+ // Reset any previous text alignment resolution
+ mPrivateFlags2 &= ~(PFLAG2_TEXT_ALIGNMENT_RESOLVED | PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK);
+ // Set to default
+ mPrivateFlags2 |= PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ }
+
+ /**
+ * @return true if text alignment is inherited.
+ *
+ * @hide
+ */
+ public boolean isTextAlignmentInherited() {
+ return (getRawTextAlignment() == TEXT_ALIGNMENT_INHERIT);
+ }
+
+ /**
+ * @return true if text alignment is resolved.
+ */
+ public boolean isTextAlignmentResolved() {
+ return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_RESOLVED) == PFLAG2_TEXT_ALIGNMENT_RESOLVED;
+ }
+
+ /**
+ * Generate a value suitable for use in {@link #setId(int)}.
+ * This value will not collide with ID values generated at build time by aapt for R.id.
+ *
+ * @return a generated ID value
+ */
+ public static int generateViewId() {
+ for (;;) {
+ final int result = sNextGeneratedId.get();
+ // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
+ int newValue = result + 1;
+ if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
+ if (sNextGeneratedId.compareAndSet(result, newValue)) {
+ return result;
+ }
+ }
+ }
+
+ private static boolean isViewIdGenerated(int id) {
+ return (id & 0xFF000000) == 0 && (id & 0x00FFFFFF) != 0;
+ }
+
+ /**
+ * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions.
+ * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and
+ * a normal View or a ViewGroup with
+ * {@link android.view.ViewGroup#isTransitionGroup()} true.
+ * @hide
+ */
+ public void captureTransitioningViews(List<View> transitioningViews) {
+ if (getVisibility() == View.VISIBLE) {
+ transitioningViews.add(this);
+ }
+ }
+
+ /**
+ * Adds all Views that have {@link #getTransitionName()} non-null to namedElements.
+ * @param namedElements Will contain all Views in the hierarchy having a transitionName.
+ * @hide
+ */
+ public void findNamedViews(Map<String, View> namedElements) {
+ if (getVisibility() == VISIBLE || mGhostView != null) {
+ String transitionName = getTransitionName();
+ if (transitionName != null) {
+ namedElements.put(transitionName, this);
+ }
+ }
+ }
+
+ /**
+ * Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
+ * The default implementation does not care the location or event types, but some subclasses
+ * may use it (such as WebViews).
+ * @param event The MotionEvent from a mouse
+ * @param pointerIndex The index of the pointer for which to retrieve the {@link PointerIcon}.
+ * This will be between 0 and {@link MotionEvent#getPointerCount()}.
+ * @see PointerIcon
+ */
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ if (isDraggingScrollBar() || isOnScrollbarThumb(x, y)) {
+ return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_ARROW);
+ }
+ return mPointerIcon;
+ }
+
+ /**
+ * Set the pointer icon for the current view.
+ * Passing {@code null} will restore the pointer icon to its default value.
+ * @param pointerIcon A PointerIcon instance which will be shown when the mouse hovers.
+ */
+ public void setPointerIcon(PointerIcon pointerIcon) {
+ mPointerIcon = pointerIcon;
+ if (mAttachInfo == null || mAttachInfo.mHandlingPointerEvent) {
+ return;
+ }
+ try {
+ mAttachInfo.mSession.updatePointerIcon(mAttachInfo.mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Gets the pointer icon for the current view.
+ */
+ @InspectableProperty
+ public PointerIcon getPointerIcon() {
+ return mPointerIcon;
+ }
+
+ /**
+ * Checks pointer capture status.
+ *
+ * @return true if the view has pointer capture.
+ * @see #requestPointerCapture()
+ * @see #hasPointerCapture()
+ */
+ public boolean hasPointerCapture() {
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl == null) {
+ return false;
+ }
+ return viewRootImpl.hasPointerCapture();
+ }
+
+ /**
+ * Requests pointer capture mode.
+ * <p>
+ * When the window has pointer capture, the mouse pointer icon will disappear and will not
+ * change its position. Enabling pointer capture will change the behavior of input devices in
+ * the following ways:
+ * <ul>
+ * <li>Events from a mouse will be delivered with the source
+ * {@link InputDevice#SOURCE_MOUSE_RELATIVE}, and relative position changes will be
+ * available through {@link MotionEvent#getX} and {@link MotionEvent#getY}.</li>
+ *
+ * <li>Events from a touchpad will be delivered with the source
+ * {@link InputDevice#SOURCE_TOUCHPAD}, where the absolute position of each of the pointers
+ * on the touchpad will be available through {@link MotionEvent#getX(int)} and
+ * {@link MotionEvent#getY(int)}, and their relative movements are stored in
+ * {@link MotionEvent#AXIS_RELATIVE_X} and {@link MotionEvent#AXIS_RELATIVE_Y}.</li>
+ *
+ * <li>Events from other types of devices, such as touchscreens, will not be affected.</li>
+ * </ul>
+ * <p>
+ * Events captured through pointer capture will be dispatched to
+ * {@link OnCapturedPointerListener#onCapturedPointer(View, MotionEvent)} if an
+ * {@link OnCapturedPointerListener} is set, and otherwise to
+ * {@link #onCapturedPointerEvent(MotionEvent)}.
+ * <p>
+ * If the window already has pointer capture, this call does nothing.
+ * <p>
+ * The capture may be released through {@link #releasePointerCapture()}, or will be lost
+ * automatically when the window loses focus.
+ *
+ * @see #releasePointerCapture()
+ * @see #hasPointerCapture()
+ * @see #onPointerCaptureChange(boolean)
+ */
+ public void requestPointerCapture() {
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.requestPointerCapture(true);
+ }
+ }
+
+
+ /**
+ * Releases the pointer capture.
+ * <p>
+ * If the window does not have pointer capture, this call will do nothing.
+ * @see #requestPointerCapture()
+ * @see #hasPointerCapture()
+ * @see #onPointerCaptureChange(boolean)
+ */
+ public void releasePointerCapture() {
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.requestPointerCapture(false);
+ }
+ }
+
+ /**
+ * Called when the window has just acquired or lost pointer capture.
+ *
+ * @param hasCapture True if the view now has pointerCapture, false otherwise.
+ */
+ @CallSuper
+ public void onPointerCaptureChange(boolean hasCapture) {
+ }
+
+ /**
+ * @see #onPointerCaptureChange
+ */
+ public void dispatchPointerCaptureChanged(boolean hasCapture) {
+ onPointerCaptureChange(hasCapture);
+ }
+
+ /**
+ * Implement this method to handle captured pointer events
+ *
+ * @param event The captured pointer event.
+ * @return True if the event was handled, false otherwise.
+ * @see #requestPointerCapture()
+ */
+ public boolean onCapturedPointerEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a captured pointer event
+ * is being dispatched this view. The callback will be invoked before the event is
+ * given to the view.
+ */
+ public interface OnCapturedPointerListener {
+ /**
+ * Called when a captured pointer event is dispatched to a view.
+ * @param view The view this event has been dispatched to.
+ * @param event The captured event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onCapturedPointer(View view, MotionEvent event);
+ }
+
+ /**
+ * Set a listener to receive callbacks when the pointer capture state of a view changes.
+ * @param l The {@link OnCapturedPointerListener} to receive callbacks.
+ */
+ public void setOnCapturedPointerListener(OnCapturedPointerListener l) {
+ getListenerInfo().mOnCapturedPointerListener = l;
+ }
+
+ // Properties
+ //
+ /**
+ * A Property wrapper around the <code>alpha</code> functionality handled by the
+ * {@link View#setAlpha(float)} and {@link View#getAlpha()} methods.
+ */
+ public static final Property<View, Float> ALPHA = new FloatProperty<View>("alpha") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setAlpha(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getAlpha();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>translationX</code> functionality handled by the
+ * {@link View#setTranslationX(float)} and {@link View#getTranslationX()} methods.
+ */
+ public static final Property<View, Float> TRANSLATION_X = new FloatProperty<View>("translationX") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setTranslationX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getTranslationX();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>translationY</code> functionality handled by the
+ * {@link View#setTranslationY(float)} and {@link View#getTranslationY()} methods.
+ */
+ public static final Property<View, Float> TRANSLATION_Y = new FloatProperty<View>("translationY") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setTranslationY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getTranslationY();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>translationZ</code> functionality handled by the
+ * {@link View#setTranslationZ(float)} and {@link View#getTranslationZ()} methods.
+ */
+ public static final Property<View, Float> TRANSLATION_Z = new FloatProperty<View>("translationZ") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setTranslationZ(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getTranslationZ();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>x</code> functionality handled by the
+ * {@link View#setX(float)} and {@link View#getX()} methods.
+ */
+ public static final Property<View, Float> X = new FloatProperty<View>("x") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getX();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>y</code> functionality handled by the
+ * {@link View#setY(float)} and {@link View#getY()} methods.
+ */
+ public static final Property<View, Float> Y = new FloatProperty<View>("y") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getY();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>z</code> functionality handled by the
+ * {@link View#setZ(float)} and {@link View#getZ()} methods.
+ */
+ public static final Property<View, Float> Z = new FloatProperty<View>("z") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setZ(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getZ();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>rotation</code> functionality handled by the
+ * {@link View#setRotation(float)} and {@link View#getRotation()} methods.
+ */
+ public static final Property<View, Float> ROTATION = new FloatProperty<View>("rotation") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setRotation(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getRotation();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>rotationX</code> functionality handled by the
+ * {@link View#setRotationX(float)} and {@link View#getRotationX()} methods.
+ */
+ public static final Property<View, Float> ROTATION_X = new FloatProperty<View>("rotationX") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setRotationX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getRotationX();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>rotationY</code> functionality handled by the
+ * {@link View#setRotationY(float)} and {@link View#getRotationY()} methods.
+ */
+ public static final Property<View, Float> ROTATION_Y = new FloatProperty<View>("rotationY") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setRotationY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getRotationY();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>scaleX</code> functionality handled by the
+ * {@link View#setScaleX(float)} and {@link View#getScaleX()} methods.
+ */
+ public static final Property<View, Float> SCALE_X = new FloatProperty<View>("scaleX") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setScaleX(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getScaleX();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>scaleY</code> functionality handled by the
+ * {@link View#setScaleY(float)} and {@link View#getScaleY()} methods.
+ */
+ public static final Property<View, Float> SCALE_Y = new FloatProperty<View>("scaleY") {
+ @Override
+ public void setValue(View object, float value) {
+ object.setScaleY(value);
+ }
+
+ @Override
+ public Float get(View object) {
+ return object.getScaleY();
+ }
+ };
+
+ /**
+ * A MeasureSpec encapsulates the layout requirements passed from parent to child.
+ * Each MeasureSpec represents a requirement for either the width or the height.
+ * A MeasureSpec is comprised of a size and a mode. There are three possible
+ * modes:
+ * <dl>
+ * <dt>UNSPECIFIED</dt>
+ * <dd>
+ * The parent has not imposed any constraint on the child. It can be whatever size
+ * it wants.
+ * </dd>
+ *
+ * <dt>EXACTLY</dt>
+ * <dd>
+ * The parent has determined an exact size for the child. The child is going to be
+ * given those bounds regardless of how big it wants to be.
+ * </dd>
+ *
+ * <dt>AT_MOST</dt>
+ * <dd>
+ * The child can be as large as it wants up to the specified size.
+ * </dd>
+ * </dl>
+ *
+ * MeasureSpecs are implemented as ints to reduce object allocation. This class
+ * is provided to pack and unpack the <size, mode> tuple into the int.
+ */
+ public static class MeasureSpec {
+ private static final int MODE_SHIFT = 30;
+ private static final int MODE_MASK = 0x3 << MODE_SHIFT;
+
+ /** @hide */
+ @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MeasureSpecMode {}
+
+ /**
+ * Measure specification mode: The parent has not imposed any constraint
+ * on the child. It can be whatever size it wants.
+ */
+ public static final int UNSPECIFIED = 0 << MODE_SHIFT;
+
+ /**
+ * Measure specification mode: The parent has determined an exact size
+ * for the child. The child is going to be given those bounds regardless
+ * of how big it wants to be.
+ */
+ public static final int EXACTLY = 1 << MODE_SHIFT;
+
+ /**
+ * Measure specification mode: The child can be as large as it wants up
+ * to the specified size.
+ */
+ public static final int AT_MOST = 2 << MODE_SHIFT;
+
+ /**
+ * Creates a measure specification based on the supplied size and mode.
+ *
+ * The mode must always be one of the following:
+ * <ul>
+ * <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
+ * <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
+ * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
+ * </ul>
+ *
+ * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
+ * implementation was such that the order of arguments did not matter
+ * and overflow in either value could impact the resulting MeasureSpec.
+ * {@link android.widget.RelativeLayout} was affected by this bug.
+ * Apps targeting API levels greater than 17 will get the fixed, more strict
+ * behavior.</p>
+ *
+ * @param size the size of the measure specification
+ * @param mode the mode of the measure specification
+ * @return the measure specification based on size and mode
+ */
+ public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
+ @MeasureSpecMode int mode) {
+ if (sUseBrokenMakeMeasureSpec) {
+ return size + mode;
+ } else {
+ return (size & ~MODE_MASK) | (mode & MODE_MASK);
+ }
+ }
+
+ /**
+ * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
+ * will automatically get a size of 0. Older apps expect this.
+ *
+ * @hide internal use only for compatibility with system widgets and older apps
+ */
+ @UnsupportedAppUsage
+ public static int makeSafeMeasureSpec(int size, int mode) {
+ if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
+ return 0;
+ }
+ return makeMeasureSpec(size, mode);
+ }
+
+ /**
+ * Extracts the mode from the supplied measure specification.
+ *
+ * @param measureSpec the measure specification to extract the mode from
+ * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
+ * {@link android.view.View.MeasureSpec#AT_MOST} or
+ * {@link android.view.View.MeasureSpec#EXACTLY}
+ */
+ @MeasureSpecMode
+ public static int getMode(int measureSpec) {
+ //noinspection ResourceType
+ return (measureSpec & MODE_MASK);
+ }
+
+ /**
+ * Extracts the size from the supplied measure specification.
+ *
+ * @param measureSpec the measure specification to extract the size from
+ * @return the size in pixels defined in the supplied measure specification
+ */
+ public static int getSize(int measureSpec) {
+ return (measureSpec & ~MODE_MASK);
+ }
+
+ static int adjust(int measureSpec, int delta) {
+ final int mode = getMode(measureSpec);
+ int size = getSize(measureSpec);
+ if (mode == UNSPECIFIED) {
+ // No need to adjust size for UNSPECIFIED mode.
+ return makeMeasureSpec(size, UNSPECIFIED);
+ }
+ size += delta;
+ if (size < 0) {
+ Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
+ ") spec: " + toString(measureSpec) + " delta: " + delta);
+ size = 0;
+ }
+ return makeMeasureSpec(size, mode);
+ }
+
+ /**
+ * Returns a String representation of the specified measure
+ * specification.
+ *
+ * @param measureSpec the measure specification to convert to a String
+ * @return a String with the following format: "MeasureSpec: MODE SIZE"
+ */
+ public static String toString(int measureSpec) {
+ int mode = getMode(measureSpec);
+ int size = getSize(measureSpec);
+
+ StringBuilder sb = new StringBuilder("MeasureSpec: ");
+
+ if (mode == UNSPECIFIED)
+ sb.append("UNSPECIFIED ");
+ else if (mode == EXACTLY)
+ sb.append("EXACTLY ");
+ else if (mode == AT_MOST)
+ sb.append("AT_MOST ");
+ else
+ sb.append(mode).append(" ");
+
+ sb.append(size);
+ return sb.toString();
+ }
+ }
+
+ private final class CheckForLongPress implements Runnable {
+ private int mOriginalWindowAttachCount;
+ private float mX;
+ private float mY;
+ private boolean mOriginalPressedState;
+ /**
+ * The classification of the long click being checked: one of the
+ * FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__* constants.
+ */
+ private int mClassification;
+
+ @UnsupportedAppUsage
+ private CheckForLongPress() {
+ }
+
+ @Override
+ public void run() {
+ if ((mOriginalPressedState == isPressed()) && (mParent != null)
+ && mOriginalWindowAttachCount == mWindowAttachCount) {
+ recordGestureClassification(mClassification);
+ if (performLongClick(mX, mY)) {
+ mHasPerformedLongPress = true;
+ }
+ }
+ }
+
+ public void setAnchor(float x, float y) {
+ mX = x;
+ mY = y;
+ }
+
+ public void rememberWindowAttachCount() {
+ mOriginalWindowAttachCount = mWindowAttachCount;
+ }
+
+ public void rememberPressedState() {
+ mOriginalPressedState = isPressed();
+ }
+
+ public void setClassification(int classification) {
+ mClassification = classification;
+ }
+ }
+
+ private final class CheckForTap implements Runnable {
+ public float x;
+ public float y;
+
+ @Override
+ public void run() {
+ mPrivateFlags &= ~PFLAG_PREPRESSED;
+ setPressed(true, x, y);
+ final long delay =
+ ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout();
+ checkForLongClick(delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
+ }
+ }
+
+ private final class PerformClick implements Runnable {
+ @Override
+ public void run() {
+ recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
+ performClickInternal();
+ }
+ }
+
+ /** Records a classification for the current event stream. */
+ private void recordGestureClassification(int classification) {
+ if (classification == TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION) {
+ return;
+ }
+ // To avoid negatively impacting View performance, the latency and displacement metrics
+ // are omitted.
+ FrameworkStatsLog.write(FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED, getClass().getName(),
+ classification);
+ }
+
+ /**
+ * This method returns a ViewPropertyAnimator object, which can be used to animate
+ * specific properties on this View.
+ *
+ * @return ViewPropertyAnimator The ViewPropertyAnimator associated with this View.
+ */
+ public ViewPropertyAnimator animate() {
+ if (mAnimator == null) {
+ mAnimator = new ViewPropertyAnimator(this);
+ }
+ return mAnimator;
+ }
+
+ /**
+ * Sets the name of the View to be used to identify Views in Transitions.
+ * Names should be unique in the View hierarchy.
+ *
+ * @param transitionName The name of the View to uniquely identify it for Transitions.
+ */
+ public final void setTransitionName(String transitionName) {
+ mTransitionName = transitionName;
+ }
+
+ /**
+ * Returns the name of the View to be used to identify Views in Transitions.
+ * Names should be unique in the View hierarchy.
+ *
+ * <p>This returns null if the View has not been given a name.</p>
+ *
+ * @return The name used of the View to be used to identify Views in Transitions or null
+ * if no name has been given.
+ */
+ @ViewDebug.ExportedProperty
+ @InspectableProperty
+ public String getTransitionName() {
+ return mTransitionName;
+ }
+
+ /**
+ * @hide
+ */
+ public void requestKeyboardShortcuts(List<KeyboardShortcutGroup> data, int deviceId) {
+ // Do nothing.
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a hardware key event is
+ * dispatched to this view. The callback will be invoked before the key event is
+ * given to the view. This is only useful for hardware keyboards; a software input
+ * method has no obligation to trigger this listener.
+ */
+ public interface OnKeyListener {
+ /**
+ * Called when a hardware key is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ * <p>Key presses in software keyboards will generally NOT trigger this method,
+ * although some may elect to do so in some situations. Do not assume a
+ * software input method has to be key-based; even if it is, it may use key presses
+ * in a different way than you expect, so there is no way to reliably catch soft
+ * input key presses.
+ *
+ * @param v The view the key has been dispatched to.
+ * @param keyCode The code for the physical key that was pressed
+ * @param event The KeyEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onKey(View v, int keyCode, KeyEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a hardware key event hasn't
+ * been handled by the view hierarchy.
+ */
+ public interface OnUnhandledKeyEventListener {
+ /**
+ * Called when a hardware key is dispatched to a view after being unhandled during normal
+ * {@link KeyEvent} dispatch.
+ *
+ * @param v The view the key has been dispatched to.
+ * @param event The KeyEvent object containing information about the event.
+ * @return {@code true} if the listener has consumed the event, {@code false} otherwise.
+ */
+ boolean onUnhandledKeyEvent(View v, KeyEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a touch event is
+ * dispatched to this view. The callback will be invoked before the touch
+ * event is given to the view.
+ */
+ public interface OnTouchListener {
+ /**
+ * Called when a touch event is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ *
+ * @param v The view the touch event has been dispatched to.
+ * @param event The MotionEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onTouch(View v, MotionEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a hover event is
+ * dispatched to this view. The callback will be invoked before the hover
+ * event is given to the view.
+ */
+ public interface OnHoverListener {
+ /**
+ * Called when a hover event is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ *
+ * @param v The view the hover event has been dispatched to.
+ * @param event The MotionEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onHover(View v, MotionEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a generic motion event is
+ * dispatched to this view. The callback will be invoked before the generic motion
+ * event is given to the view.
+ */
+ public interface OnGenericMotionListener {
+ /**
+ * Called when a generic motion event is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ *
+ * @param v The view the generic motion event has been dispatched to.
+ * @param event The MotionEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onGenericMotion(View v, MotionEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a view has been clicked and held.
+ */
+ public interface OnLongClickListener {
+ /**
+ * Called when a view has been clicked and held.
+ *
+ * @param v The view that was clicked and held.
+ *
+ * @return true if the callback consumed the long click, false otherwise.
+ */
+ boolean onLongClick(View v);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a drag is being dispatched
+ * to this view. The callback will be invoked before the hosting view's own
+ * onDrag(event) method. If the listener wants to fall back to the hosting view's
+ * onDrag(event) behavior, it should return 'false' from this callback.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For a guide to implementing drag and drop features, read the
+ * <a href="{@docRoot}guide/topics/ui/drag-drop.html">Drag and Drop</a> developer guide.</p>
+ * </div>
+ */
+ public interface OnDragListener {
+ /**
+ * Called when a drag event is dispatched to a view. This allows listeners
+ * to get a chance to override base View behavior.
+ *
+ * @param v The View that received the drag event.
+ * @param event The {@link android.view.DragEvent} object for the drag event.
+ * @return {@code true} if the drag event was handled successfully, or {@code false}
+ * if the drag event was not handled. Note that {@code false} will trigger the View
+ * to call its {@link #onDragEvent(DragEvent) onDragEvent()} handler.
+ */
+ boolean onDrag(View v, DragEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the focus state of
+ * a view changed.
+ */
+ public interface OnFocusChangeListener {
+ /**
+ * Called when the focus state of a view has changed.
+ *
+ * @param v The view whose state has changed.
+ * @param hasFocus The new focus state of v.
+ */
+ void onFocusChange(View v, boolean hasFocus);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a view is clicked.
+ */
+ public interface OnClickListener {
+ /**
+ * Called when a view has been clicked.
+ *
+ * @param v The view that was clicked.
+ */
+ void onClick(View v);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a view is context clicked.
+ */
+ public interface OnContextClickListener {
+ /**
+ * Called when a view is context clicked.
+ *
+ * @param v The view that has been context clicked.
+ * @return true if the callback consumed the context click, false otherwise.
+ */
+ boolean onContextClick(View v);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the context menu
+ * for this view is being built.
+ */
+ public interface OnCreateContextMenuListener {
+ /**
+ * Called when the context menu for this view is being built. It is not
+ * safe to hold onto the menu after this method returns.
+ *
+ * @param menu The context menu that is being built
+ * @param v The view for which the context menu is being built
+ * @param menuInfo Extra information about the item for which the
+ * context menu should be shown. This information will vary
+ * depending on the class of v.
+ */
+ void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the status bar changes
+ * visibility. This reports <strong>global</strong> changes to the system UI
+ * state, not what the application is requesting.
+ *
+ * @see View#setOnSystemUiVisibilityChangeListener(android.view.View.OnSystemUiVisibilityChangeListener)
+ *
+ * @deprecated Use {@link WindowInsets#isVisible(int)} to find out about system bar visibilities
+ * by setting a {@link OnApplyWindowInsetsListener} on this view.
+ */
+ @Deprecated
+ public interface OnSystemUiVisibilityChangeListener {
+ /**
+ * Called when the status bar changes visibility because of a call to
+ * {@link View#setSystemUiVisibility(int)}.
+ *
+ * @param visibility Bitwise-or of flags {@link #SYSTEM_UI_FLAG_LOW_PROFILE},
+ * {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION}, and {@link #SYSTEM_UI_FLAG_FULLSCREEN}.
+ * This tells you the <strong>global</strong> state of these UI visibility
+ * flags, not what your app is currently applying.
+ */
+ public void onSystemUiVisibilityChange(int visibility);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when this view is attached
+ * or detached from its window.
+ */
+ public interface OnAttachStateChangeListener {
+ /**
+ * Called when the view is attached to a window.
+ * @param v The view that was attached
+ */
+ public void onViewAttachedToWindow(View v);
+ /**
+ * Called when the view is detached from a window.
+ * @param v The view that was detached
+ */
+ public void onViewDetachedFromWindow(View v);
+ }
+
+ /**
+ * Listener for applying window insets on a view in a custom way.
+ *
+ * <p>Apps may choose to implement this interface if they want to apply custom policy
+ * to the way that window insets are treated for a view. If an OnApplyWindowInsetsListener
+ * is set, its
+ * {@link OnApplyWindowInsetsListener#onApplyWindowInsets(View, WindowInsets) onApplyWindowInsets}
+ * method will be called instead of the View's own
+ * {@link #onApplyWindowInsets(WindowInsets) onApplyWindowInsets} method. The listener
+ * may optionally call the parameter View's <code>onApplyWindowInsets</code> method to apply
+ * the View's normal behavior as part of its own.</p>
+ */
+ public interface OnApplyWindowInsetsListener {
+ /**
+ * When {@link View#setOnApplyWindowInsetsListener(View.OnApplyWindowInsetsListener) set}
+ * on a View, this listener method will be called instead of the view's own
+ * {@link View#onApplyWindowInsets(WindowInsets) onApplyWindowInsets} method.
+ *
+ * @param v The view applying window insets
+ * @param insets The insets to apply
+ * @return The insets supplied, minus any insets that were consumed
+ */
+ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets);
+ }
+
+ private final class UnsetPressedState implements Runnable {
+ @Override
+ public void run() {
+ setPressed(false);
+ }
+ }
+
+ /**
+ * When a view becomes invisible checks if autofill considers the view invisible too. This
+ * happens after the regular removal operation to make sure the operation is finished by the
+ * time this is called.
+ */
+ private static class VisibilityChangeForAutofillHandler extends Handler {
+ private final AutofillManager mAfm;
+ private final View mView;
+
+ private VisibilityChangeForAutofillHandler(@NonNull AutofillManager afm,
+ @NonNull View view) {
+ mAfm = afm;
+ mView = view;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ mAfm.notifyViewVisibilityChanged(mView, mView.isShown());
+ }
+ }
+
+ /**
+ * Base class for derived classes that want to save and restore their own
+ * state in {@link android.view.View#onSaveInstanceState()}.
+ */
+ public static class BaseSavedState extends AbsSavedState {
+ static final int START_ACTIVITY_REQUESTED_WHO_SAVED = 0b1;
+ static final int IS_AUTOFILLED = 0b10;
+ static final int AUTOFILL_ID = 0b100;
+
+ // Flags that describe what data in this state is valid
+ int mSavedData;
+ String mStartActivityRequestWhoSaved;
+ boolean mIsAutofilled;
+ boolean mHideHighlight;
+ int mAutofillViewId;
+
+ /**
+ * Constructor used when reading from a parcel. Reads the state of the superclass.
+ *
+ * @param source parcel to read from
+ */
+ public BaseSavedState(Parcel source) {
+ this(source, null);
+ }
+
+ /**
+ * Constructor used when reading from a parcel using a given class loader.
+ * Reads the state of the superclass.
+ *
+ * @param source parcel to read from
+ * @param loader ClassLoader to use for reading
+ */
+ public BaseSavedState(Parcel source, ClassLoader loader) {
+ super(source, loader);
+ mSavedData = source.readInt();
+ mStartActivityRequestWhoSaved = source.readString();
+ mIsAutofilled = source.readBoolean();
+ mHideHighlight = source.readBoolean();
+ mAutofillViewId = source.readInt();
+ }
+
+ /**
+ * Constructor called by derived classes when creating their SavedState objects
+ *
+ * @param superState The state of the superclass of this view
+ */
+ public BaseSavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+
+ out.writeInt(mSavedData);
+ out.writeString(mStartActivityRequestWhoSaved);
+ out.writeBoolean(mIsAutofilled);
+ out.writeBoolean(mHideHighlight);
+ out.writeInt(mAutofillViewId);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BaseSavedState> CREATOR
+ = new Parcelable.ClassLoaderCreator<BaseSavedState>() {
+ @Override
+ public BaseSavedState createFromParcel(Parcel in) {
+ return new BaseSavedState(in);
+ }
+
+ @Override
+ public BaseSavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return new BaseSavedState(in, loader);
+ }
+
+ @Override
+ public BaseSavedState[] newArray(int size) {
+ return new BaseSavedState[size];
+ }
+ };
+ }
+
+ /**
+ * A set of information given to a view when it is attached to its parent
+ * window.
+ */
+ final static class AttachInfo {
+
+ interface Callbacks {
+ void playSoundEffect(int effectId);
+ boolean performHapticFeedback(int effectId, boolean always);
+ }
+
+ /**
+ * InvalidateInfo is used to post invalidate(int, int, int, int) messages
+ * to a Handler. This class contains the target (View) to invalidate and
+ * the coordinates of the dirty rectangle.
+ *
+ * For performance purposes, this class also implements a pool of up to
+ * POOL_LIMIT objects that get reused. This reduces memory allocations
+ * whenever possible.
+ */
+ static class InvalidateInfo {
+
+ @UnsupportedAppUsage
+ InvalidateInfo() {
+ }
+
+ private static final int POOL_LIMIT = 10;
+
+ private static final SynchronizedPool<InvalidateInfo> sPool =
+ new SynchronizedPool<InvalidateInfo>(POOL_LIMIT);
+
+ @UnsupportedAppUsage
+ View target;
+
+ @UnsupportedAppUsage
+ int left;
+ @UnsupportedAppUsage
+ int top;
+ @UnsupportedAppUsage
+ int right;
+ @UnsupportedAppUsage
+ int bottom;
+
+ public static InvalidateInfo obtain() {
+ InvalidateInfo instance = sPool.acquire();
+ return (instance != null) ? instance : new InvalidateInfo();
+ }
+
+ public void recycle() {
+ target = null;
+ sPool.release(this);
+ }
+ }
+
+ @UnsupportedAppUsage
+ final IWindowSession mSession;
+
+ @UnsupportedAppUsage
+ final IWindow mWindow;
+
+ final IBinder mWindowToken;
+
+ Display mDisplay;
+
+ final Callbacks mRootCallbacks;
+
+ IWindowId mIWindowId;
+ WindowId mWindowId;
+
+ /**
+ * The top view of the hierarchy.
+ */
+ View mRootView;
+
+ IBinder mPanelParentWindowToken;
+
+ boolean mHardwareAccelerated;
+ boolean mHardwareAccelerationRequested;
+ ThreadedRenderer mThreadedRenderer;
+ List<RenderNode> mPendingAnimatingRenderNodes;
+
+ /**
+ * The state of the display to which the window is attached, as reported
+ * by {@link Display#getState()}. Note that the display state constants
+ * declared by {@link Display} do not exactly line up with the screen state
+ * constants declared by {@link View} (there are more display states than
+ * screen states).
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ int mDisplayState = Display.STATE_UNKNOWN;
+
+ /**
+ * Scale factor used by the compatibility mode
+ */
+ @UnsupportedAppUsage
+ float mApplicationScale;
+
+ /**
+ * Indicates whether the application is in compatibility mode
+ */
+ @UnsupportedAppUsage
+ boolean mScalingRequired;
+
+ /**
+ * Left position of this view's window
+ */
+ int mWindowLeft;
+
+ /**
+ * Top position of this view's window
+ */
+ int mWindowTop;
+
+ /**
+ * Indicates whether views need to use 32-bit drawing caches
+ */
+ boolean mUse32BitDrawingCache;
+
+ /**
+ * For windows that are full-screen but using insets to layout inside
+ * of the screen decorations, these are the current insets for the
+ * content of the window.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+ publicAlternatives = "Use {@link WindowInsets#getInsets(int)}")
+ final Rect mContentInsets = new Rect();
+
+ /**
+ * For windows that are full-screen but using insets to layout inside
+ * of the screen decorations, these are the current insets for the
+ * actual visible parts of the window.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+ publicAlternatives = "Use {@link WindowInsets#getInsets(int)}")
+ final Rect mVisibleInsets = new Rect();
+
+ /**
+ * For windows that are full-screen but using insets to layout inside
+ * of the screen decorations, these are the current insets for the
+ * stable system windows.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q,
+ publicAlternatives = "Use {@link WindowInsets#getInsets(int)}")
+ final Rect mStableInsets = new Rect();
+
+ /**
+ * Current caption insets to the display coordinate.
+ */
+ final Rect mCaptionInsets = new Rect();
+
+ /**
+ * In multi-window we force show the system bars. Because we don't want that the surface
+ * size changes in this mode, we instead have a flag whether the system bars sizes should
+ * always be consumed, so the app is treated like there are no virtual system bars at all.
+ */
+ boolean mAlwaysConsumeSystemBars;
+
+ /**
+ * The internal insets given by this window. This value is
+ * supplied by the client (through
+ * {@link ViewTreeObserver.OnComputeInternalInsetsListener}) and will
+ * be given to the window manager when changed to be used in laying
+ * out windows behind it.
+ */
+ @UnsupportedAppUsage
+ final ViewTreeObserver.InternalInsetsInfo mGivenInternalInsets
+ = new ViewTreeObserver.InternalInsetsInfo();
+
+ /**
+ * Set to true when mGivenInternalInsets is non-empty.
+ */
+ boolean mHasNonEmptyGivenInternalInsets;
+
+ /**
+ * All views in the window's hierarchy that serve as scroll containers,
+ * used to determine if the window can be resized or must be panned
+ * to adjust for a soft input area.
+ */
+ @UnsupportedAppUsage
+ final ArrayList<View> mScrollContainers = new ArrayList<View>();
+
+ @UnsupportedAppUsage
+ final KeyEvent.DispatcherState mKeyDispatchState
+ = new KeyEvent.DispatcherState();
+
+ /**
+ * Indicates whether the view's window currently has the focus.
+ */
+ @UnsupportedAppUsage
+ boolean mHasWindowFocus;
+
+ /**
+ * The current visibility of the window.
+ */
+ int mWindowVisibility;
+
+ /**
+ * Indicates the time at which drawing started to occur.
+ */
+ @UnsupportedAppUsage
+ long mDrawingTime;
+
+ /**
+ * Indicates whether the view's window is currently in touch mode.
+ */
+ @UnsupportedAppUsage
+ boolean mInTouchMode;
+
+ /**
+ * Indicates whether the view has requested unbuffered input dispatching for the current
+ * event stream.
+ */
+ boolean mUnbufferedDispatchRequested;
+
+ /**
+ * Indicates that ViewAncestor should trigger a global layout change
+ * the next time it performs a traversal
+ */
+ @UnsupportedAppUsage
+ boolean mRecomputeGlobalAttributes;
+
+ /**
+ * Always report new attributes at next traversal.
+ */
+ boolean mForceReportNewAttributes;
+
+ /**
+ * Set during a traveral if any views want to keep the screen on.
+ */
+ @UnsupportedAppUsage
+ boolean mKeepScreenOn;
+
+ /**
+ * Set during a traveral if the light center needs to be updated.
+ */
+ boolean mNeedsUpdateLightCenter;
+
+ /**
+ * Bitwise-or of all of the values that views have passed to setSystemUiVisibility().
+ */
+ int mSystemUiVisibility;
+
+ /**
+ * Hack to force certain system UI visibility flags to be cleared.
+ */
+ int mDisabledSystemUiVisibility;
+
+ /**
+ * True if a view in this hierarchy has an OnSystemUiVisibilityChangeListener
+ * attached.
+ */
+ boolean mHasSystemUiListeners;
+
+ /**
+ * Set if the visibility of any views has changed.
+ */
+ @UnsupportedAppUsage
+ boolean mViewVisibilityChanged;
+
+ /**
+ * Set to true if a view has been scrolled.
+ */
+ @UnsupportedAppUsage
+ boolean mViewScrollChanged;
+
+ /**
+ * Set to true if a pointer event is currently being handled.
+ */
+ boolean mHandlingPointerEvent;
+
+ /**
+ * The offset of this view's window when it's on an embedded display that is re-parented
+ * to another window.
+ */
+ final Point mLocationInParentDisplay = new Point();
+
+ /**
+ * The screen matrix of this view when it's on a {@link SurfaceControlViewHost} that is
+ * embedded within a SurfaceView.
+ */
+ Matrix mScreenMatrixInEmbeddedHierarchy;
+
+ /**
+ * Global to the view hierarchy used as a temporary for dealing with
+ * x/y points in the transparent region computations.
+ */
+ final int[] mTransparentLocation = new int[2];
+
+ /**
+ * Global to the view hierarchy used as a temporary for dealing with
+ * x/y points in the ViewGroup.invalidateChild implementation.
+ */
+ final int[] mInvalidateChildLocation = new int[2];
+
+ /**
+ * Global to the view hierarchy used as a temporary for dealing with
+ * computing absolute on-screen location.
+ */
+ final int[] mTmpLocation = new int[2];
+
+ /**
+ * Global to the view hierarchy used as a temporary for dealing with
+ * x/y location when view is transformed.
+ */
+ final float[] mTmpTransformLocation = new float[2];
+
+ /**
+ * The view tree observer used to dispatch global events like
+ * layout, pre-draw, touch mode change, etc.
+ */
+ @UnsupportedAppUsage
+ final ViewTreeObserver mTreeObserver;
+
+ /**
+ * A Canvas used by the view hierarchy to perform bitmap caching.
+ */
+ Canvas mCanvas;
+
+ /**
+ * The view root impl.
+ */
+ final ViewRootImpl mViewRootImpl;
+
+ /**
+ * A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
+ * handler can be used to pump events in the UI events queue.
+ */
+ @UnsupportedAppUsage
+ final Handler mHandler;
+
+ /**
+ * Temporary for use in computing invalidate rectangles while
+ * calling up the hierarchy.
+ */
+ final Rect mTmpInvalRect = new Rect();
+
+ /**
+ * Temporary for use in computing hit areas with transformed views
+ */
+ final RectF mTmpTransformRect = new RectF();
+
+ /**
+ * Temporary for use in computing hit areas with transformed views
+ */
+ final RectF mTmpTransformRect1 = new RectF();
+
+ /**
+ * Temporary list of rectanges.
+ */
+ final List<RectF> mTmpRectList = new ArrayList<>();
+
+ /**
+ * Temporary for use in transforming invalidation rect
+ */
+ final Matrix mTmpMatrix = new Matrix();
+
+ /**
+ * Temporary for use in transforming invalidation rect
+ */
+ final Transformation mTmpTransformation = new Transformation();
+
+ /**
+ * Temporary for use in querying outlines from OutlineProviders
+ */
+ final Outline mTmpOutline = new Outline();
+
+ /**
+ * Temporary list for use in collecting focusable descendents of a view.
+ */
+ final ArrayList<View> mTempArrayList = new ArrayList<View>(24);
+
+ /**
+ * The id of the window for accessibility purposes.
+ */
+ int mAccessibilityWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+
+ /**
+ * Flags related to accessibility processing.
+ *
+ * @see AccessibilityNodeInfo#FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ * @see AccessibilityNodeInfo#FLAG_REPORT_VIEW_IDS
+ */
+ int mAccessibilityFetchFlags;
+
+ /**
+ * The drawable for highlighting accessibility focus.
+ */
+ Drawable mAccessibilityFocusDrawable;
+
+ /**
+ * The drawable for highlighting autofilled views.
+ *
+ * @see #isAutofilled()
+ */
+ Drawable mAutofilledDrawable;
+
+ /**
+ * Show where the margins, bounds and layout bounds are for each view.
+ */
+ boolean mDebugLayout = DisplayProperties.debug_layout().orElse(false);
+
+ /**
+ * Point used to compute visible regions.
+ */
+ final Point mPoint = new Point();
+
+ /**
+ * Used to track which View originated a requestLayout() call, used when
+ * requestLayout() is called during layout.
+ */
+ View mViewRequestingLayout;
+
+ /**
+ * Used to track the identity of the current drag operation.
+ */
+ IBinder mDragToken;
+
+ /**
+ * The drag shadow surface for the current drag operation.
+ */
+ public Surface mDragSurface;
+
+
+ /**
+ * The view that currently has a tooltip displayed.
+ */
+ View mTooltipHost;
+
+ /**
+ * The initial structure has been reported so the view is ready to report updates.
+ */
+ boolean mReadyForContentCaptureUpdates;
+
+ /**
+ * Map(keyed by session) of content capture events that need to be notified after the view
+ * hierarchy is traversed: value is either the view itself for appearead events, or its
+ * autofill id for disappeared.
+ */
+ SparseArray<ArrayList<Object>> mContentCaptureEvents;
+
+ /**
+ * Cached reference to the {@link ContentCaptureManager}.
+ */
+ ContentCaptureManager mContentCaptureManager;
+
+ /**
+ * Listener used to fit content on window level.
+ */
+ OnContentApplyWindowInsetsListener mContentOnApplyWindowInsetsListener;
+
+ /**
+ * The leash token of this view's parent when it's in an embedded hierarchy that is
+ * re-parented to another window.
+ */
+ IBinder mLeashedParentToken;
+
+ /**
+ * The accessibility view id of this view's parent when it's in an embedded
+ * hierarchy that is re-parented to another window.
+ */
+ int mLeashedParentAccessibilityViewId;
+
+ /**
+ *
+ */
+ ScrollCaptureInternal mScrollCaptureInternal;
+
+ /**
+ * Creates a new set of attachment information with the specified
+ * events handler and thread.
+ *
+ * @param handler the events handler the view must use
+ */
+ AttachInfo(IWindowSession session, IWindow window, Display display,
+ ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
+ Context context) {
+ mSession = session;
+ mWindow = window;
+ mWindowToken = window.asBinder();
+ mDisplay = display;
+ mViewRootImpl = viewRootImpl;
+ mHandler = handler;
+ mRootCallbacks = effectPlayer;
+ mTreeObserver = new ViewTreeObserver(context);
+ }
+
+ @Nullable
+ ContentCaptureManager getContentCaptureManager(@NonNull Context context) {
+ if (mContentCaptureManager != null) {
+ return mContentCaptureManager;
+ }
+ mContentCaptureManager = context.getSystemService(ContentCaptureManager.class);
+ return mContentCaptureManager;
+ }
+
+ void delayNotifyContentCaptureInsetsEvent(@NonNull Insets insets) {
+ if (mContentCaptureManager == null) {
+ return;
+ }
+
+ ArrayList<Object> events = ensureEvents(
+ mContentCaptureManager.getMainContentCaptureSession());
+ events.add(insets);
+ }
+
+ private void delayNotifyContentCaptureEvent(@NonNull ContentCaptureSession session,
+ @NonNull View view, boolean appeared) {
+ ArrayList<Object> events = ensureEvents(session);
+ events.add(appeared ? view : view.getAutofillId());
+ }
+
+ @NonNull
+ private ArrayList<Object> ensureEvents(@NonNull ContentCaptureSession session) {
+ if (mContentCaptureEvents == null) {
+ // Most of the time there will be just one session, so intial capacity is 1
+ mContentCaptureEvents = new SparseArray<>(1);
+ }
+ int sessionId = session.getId();
+ // TODO: life would be much easier if we provided a MultiMap implementation somwhere...
+ ArrayList<Object> events = mContentCaptureEvents.get(sessionId);
+ if (events == null) {
+ events = new ArrayList<>();
+ mContentCaptureEvents.put(sessionId, events);
+ }
+
+ return events;
+ }
+
+ @Nullable
+ ScrollCaptureInternal getScrollCaptureInternal() {
+ if (mScrollCaptureInternal != null) {
+ mScrollCaptureInternal = new ScrollCaptureInternal();
+ }
+ return mScrollCaptureInternal;
+ }
+
+ AttachedSurfaceControl getRootSurfaceControl() {
+ return mViewRootImpl;
+ }
+
+ public void dump(String prefix, PrintWriter writer) {
+ String innerPrefix = prefix + " ";
+ writer.println(prefix + "AttachInfo:");
+ writer.println(innerPrefix + "mHasWindowFocus=" + mHasWindowFocus);
+ writer.println(innerPrefix + "mWindowVisibility=" + mWindowVisibility);
+ writer.println(innerPrefix + "mInTouchMode=" + mInTouchMode);
+ writer.println(innerPrefix + "mUnbufferedDispatchRequested="
+ + mUnbufferedDispatchRequested);
+ }
+ }
+
+ /**
+ * <p>ScrollabilityCache holds various fields used by a View when scrolling
+ * is supported. This avoids keeping too many unused fields in most
+ * instances of View.</p>
+ */
+ private static class ScrollabilityCache implements Runnable {
+
+ /**
+ * Scrollbars are not visible
+ */
+ public static final int OFF = 0;
+
+ /**
+ * Scrollbars are visible
+ */
+ public static final int ON = 1;
+
+ /**
+ * Scrollbars are fading away
+ */
+ public static final int FADING = 2;
+
+ public boolean fadeScrollBars;
+
+ public int fadingEdgeLength;
+ public int scrollBarDefaultDelayBeforeFade;
+ public int scrollBarFadeDuration;
+
+ public int scrollBarSize;
+ public int scrollBarMinTouchTarget;
+ @UnsupportedAppUsage
+ public ScrollBarDrawable scrollBar;
+ public float[] interpolatorValues;
+ @UnsupportedAppUsage
+ public View host;
+
+ public final Paint paint;
+ public final Matrix matrix;
+ public Shader shader;
+
+ public final Interpolator scrollBarInterpolator = new Interpolator(1, 2);
+
+ private static final float[] OPAQUE = { 255 };
+ private static final float[] TRANSPARENT = { 0.0f };
+
+ /**
+ * When fading should start. This time moves into the future every time
+ * a new scroll happens. Measured based on SystemClock.uptimeMillis()
+ */
+ public long fadeStartTime;
+
+
+ /**
+ * The current state of the scrollbars: ON, OFF, or FADING
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int state = OFF;
+
+ private int mLastColor;
+
+ public final Rect mScrollBarBounds = new Rect();
+ public final Rect mScrollBarTouchBounds = new Rect();
+
+ public static final int NOT_DRAGGING = 0;
+ public static final int DRAGGING_VERTICAL_SCROLL_BAR = 1;
+ public static final int DRAGGING_HORIZONTAL_SCROLL_BAR = 2;
+ public int mScrollBarDraggingState = NOT_DRAGGING;
+
+ public float mScrollBarDraggingPos = 0;
+
+ public ScrollabilityCache(ViewConfiguration configuration, View host) {
+ fadingEdgeLength = configuration.getScaledFadingEdgeLength();
+ scrollBarSize = configuration.getScaledScrollBarSize();
+ scrollBarMinTouchTarget = configuration.getScaledMinScrollbarTouchTarget();
+ scrollBarDefaultDelayBeforeFade = ViewConfiguration.getScrollDefaultDelay();
+ scrollBarFadeDuration = ViewConfiguration.getScrollBarFadeDuration();
+
+ paint = new Paint();
+ matrix = new Matrix();
+ // use use a height of 1, and then wack the matrix each time we
+ // actually use it.
+ shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+
+ this.host = host;
+ }
+
+ public void setFadeColor(int color) {
+ if (color != mLastColor) {
+ mLastColor = color;
+
+ if (color != 0) {
+ shader = new LinearGradient(0, 0, 0, 1, color | 0xFF000000,
+ color & 0x00FFFFFF, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ // Restore the default transfer mode (src_over)
+ paint.setXfermode(null);
+ } else {
+ shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP);
+ paint.setShader(shader);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+ }
+ }
+ }
+
+ public void run() {
+ long now = AnimationUtils.currentAnimationTimeMillis();
+ if (now >= fadeStartTime) {
+
+ // the animation fades the scrollbars out by changing
+ // the opacity (alpha) from fully opaque to fully
+ // transparent
+ int nextFrame = (int) now;
+ int framesCount = 0;
+
+ Interpolator interpolator = scrollBarInterpolator;
+
+ // Start opaque
+ interpolator.setKeyFrame(framesCount++, nextFrame, OPAQUE);
+
+ // End transparent
+ nextFrame += scrollBarFadeDuration;
+ interpolator.setKeyFrame(framesCount, nextFrame, TRANSPARENT);
+
+ state = FADING;
+
+ // Kick off the fade animation
+ host.invalidate(true);
+ }
+ }
+ }
+
+ private class SendAccessibilityEventThrottle implements Runnable {
+ public volatile boolean mIsPending;
+ private AccessibilityEvent mAccessibilityEvent;
+
+ public void post(AccessibilityEvent accessibilityEvent) {
+ updateWithAccessibilityEvent(accessibilityEvent);
+ if (!mIsPending) {
+ mIsPending = true;
+ postDelayed(this,
+ ViewConfiguration.getSendRecurringAccessibilityEventsInterval());
+ }
+ }
+
+ @Override
+ public void run() {
+ if (AccessibilityManager.getInstance(mContext).isEnabled() && isShown()) {
+ requestParentSendAccessibilityEvent(mAccessibilityEvent);
+ }
+ reset();
+ }
+
+ public void updateWithAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
+ mAccessibilityEvent = accessibilityEvent;
+ }
+
+ public void reset() {
+ mIsPending = false;
+ mAccessibilityEvent = null;
+ }
+
+ }
+
+ /**
+ * Resuable callback for sending
+ * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
+ */
+ private class SendViewScrolledAccessibilityEvent extends SendAccessibilityEventThrottle {
+ public int mDeltaX;
+ public int mDeltaY;
+
+ @Override
+ public void updateWithAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
+ super.updateWithAccessibilityEvent(accessibilityEvent);
+ mDeltaX += accessibilityEvent.getScrollDeltaX();
+ mDeltaY += accessibilityEvent.getScrollDeltaY();
+ accessibilityEvent.setScrollDeltaX(mDeltaX);
+ accessibilityEvent.setScrollDeltaY(mDeltaY);
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ mDeltaX = 0;
+ mDeltaY = 0;
+ }
+ }
+ /**
+ * Remove the pending callback for sending a throttled accessibility event.
+ */
+ @UnsupportedAppUsage
+ private void cancel(@Nullable SendAccessibilityEventThrottle callback) {
+ if (callback == null || !callback.mIsPending) return;
+ removeCallbacks(callback);
+ callback.reset();
+ }
+
+ /**
+ * <p>
+ * This class represents a delegate that can be registered in a {@link View}
+ * to enhance accessibility support via composition rather via inheritance.
+ * It is specifically targeted to widget developers that extend basic View
+ * classes i.e. classes in package android.view, that would like their
+ * applications to be backwards compatible.
+ * </p>
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about making applications accessible, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ * <p>
+ * A scenario in which a developer would like to use an accessibility delegate
+ * is overriding a method introduced in a later API version than the minimal API
+ * version supported by the application. For example, the method
+ * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} is not available
+ * in API version 4 when the accessibility APIs were first introduced. If a
+ * developer would like their application to run on API version 4 devices (assuming
+ * all other APIs used by the application are version 4 or lower) and take advantage
+ * of this method, instead of overriding the method which would break the application's
+ * backwards compatibility, they can override the corresponding method in this
+ * delegate and register the delegate in the target View if the API version of
+ * the system is high enough, i.e. the API version is the same as or higher than the API
+ * version that introduced
+ * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)}.
+ * </p>
+ * <p>
+ * Here is an example implementation:
+ * </p>
+ * <code><pre><p>
+ * if (Build.VERSION.SDK_INT >= 14) {
+ * // If the API version is equal of higher than the version in
+ * // which onInitializeAccessibilityNodeInfo was introduced we
+ * // register a delegate with a customized implementation.
+ * View view = findViewById(R.id.view_id);
+ * view.setAccessibilityDelegate(new AccessibilityDelegate() {
+ * public void onInitializeAccessibilityNodeInfo(View host,
+ * AccessibilityNodeInfo info) {
+ * // Let the default implementation populate the info.
+ * super.onInitializeAccessibilityNodeInfo(host, info);
+ * // Set some other information.
+ * info.setEnabled(host.isEnabled());
+ * }
+ * });
+ * }
+ * </code></pre></p>
+ * <p>
+ * This delegate contains methods that correspond to the accessibility methods
+ * in View. If a delegate has been specified the implementation in View hands
+ * off handling to the corresponding method in this delegate. The default
+ * implementation the delegate methods behaves exactly as the corresponding
+ * method in View for the case of no accessibility delegate been set. Hence,
+ * to customize the behavior of a View method, clients can override only the
+ * corresponding delegate method without altering the behavior of the rest
+ * accessibility related methods of the host view.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> On platform versions prior to
+ * {@link android.os.Build.VERSION_CODES#M API 23}, delegate methods on
+ * views in the {@code android.widget.*} package are called <i>before</i>
+ * host methods. This prevents certain properties such as class name from
+ * being modified by overriding
+ * {@link AccessibilityDelegate#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfo)},
+ * as any changes will be overwritten by the host class.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#M API 23}, delegate
+ * methods are called <i>after</i> host methods, which all properties to be
+ * modified without being overwritten by the host class.
+ */
+ public static class AccessibilityDelegate {
+
+ /**
+ * Sends an accessibility event of the given type. If accessibility is not
+ * enabled this method has no effect.
+ * <p>
+ * The default implementation behaves as {@link View#sendAccessibilityEvent(int)
+ * View#sendAccessibilityEvent(int)} for the case of no accessibility delegate
+ * been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param eventType The type of the event to send.
+ *
+ * @see View#sendAccessibilityEvent(int) View#sendAccessibilityEvent(int)
+ */
+ public void sendAccessibilityEvent(View host, int eventType) {
+ host.sendAccessibilityEventInternal(eventType);
+ }
+
+ /**
+ * Performs the specified accessibility action on the view. For
+ * possible accessibility actions look at {@link AccessibilityNodeInfo}.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#performAccessibilityAction(int, Bundle)
+ * View#performAccessibilityAction(int, Bundle)} for the case of
+ * no accessibility delegate been set.
+ * </p>
+ *
+ * @param action The action to perform.
+ * @return Whether the action was performed.
+ *
+ * @see View#performAccessibilityAction(int, Bundle)
+ * View#performAccessibilityAction(int, Bundle)
+ */
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ return host.performAccessibilityActionInternal(action, args);
+ }
+
+ /**
+ * Sends an accessibility event. This method behaves exactly as
+ * {@link #sendAccessibilityEvent(View, int)} but takes as an argument an
+ * empty {@link AccessibilityEvent} and does not perform a check whether
+ * accessibility is enabled.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent)
+ * View#sendAccessibilityEventUnchecked(AccessibilityEvent)} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param event The event to send.
+ *
+ * @see View#sendAccessibilityEventUnchecked(AccessibilityEvent)
+ * View#sendAccessibilityEventUnchecked(AccessibilityEvent)
+ */
+ public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
+ host.sendAccessibilityEventUncheckedInternal(event);
+ }
+
+ /**
+ * Dispatches an {@link AccessibilityEvent} to the host {@link View} first and then
+ * to its children for adding their text content to the event.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param event The event.
+ * @return True if the event population was completed.
+ *
+ * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
+ */
+ public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+ return host.dispatchPopulateAccessibilityEventInternal(event);
+ }
+
+ /**
+ * Gives a chance to the host View to populate the accessibility event with its
+ * text content.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#onPopulateAccessibilityEvent(AccessibilityEvent)
+ * View#onPopulateAccessibilityEvent(AccessibilityEvent)} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param event The accessibility event which to populate.
+ *
+ * @see View#onPopulateAccessibilityEvent(AccessibilityEvent)
+ * View#onPopulateAccessibilityEvent(AccessibilityEvent)
+ */
+ public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+ host.onPopulateAccessibilityEventInternal(event);
+ }
+
+ /**
+ * Initializes an {@link AccessibilityEvent} with information about the
+ * the host View which is the event source.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#onInitializeAccessibilityEvent(AccessibilityEvent)
+ * View#onInitializeAccessibilityEvent(AccessibilityEvent)} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param event The event to initialize.
+ *
+ * @see View#onInitializeAccessibilityEvent(AccessibilityEvent)
+ * View#onInitializeAccessibilityEvent(AccessibilityEvent)
+ */
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ host.onInitializeAccessibilityEventInternal(event);
+ }
+
+ /**
+ * Initializes an {@link AccessibilityNodeInfo} with information about the host view.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)
+ * View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param info The instance to initialize.
+ *
+ * @see View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)
+ * View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)
+ */
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ host.onInitializeAccessibilityNodeInfoInternal(info);
+ }
+
+ /**
+ * Adds extra data to an {@link AccessibilityNodeInfo} based on an explicit request for the
+ * additional data.
+ * <p>
+ * This method only needs to be implemented if the View offers to provide additional data.
+ * </p>
+ * <p>
+ * The default implementation behaves as
+ * {@link View#addExtraDataToAccessibilityNodeInfo(AccessibilityNodeInfo, String, Bundle)
+ * for the case where no accessibility delegate is set.
+ * </p>
+ *
+ * @param host The View hosting the delegate. Never {@code null}.
+ * @param info The info to which to add the extra data. Never {@code null}.
+ * @param extraDataKey A key specifying the type of extra data to add to the info. The
+ * extra data should be added to the {@link Bundle} returned by
+ * the info's {@link AccessibilityNodeInfo#getExtras} method. Never
+ * {@code null}.
+ * @param arguments A {@link Bundle} holding any arguments relevant for this request.
+ * May be {@code null} if the if the service provided no arguments.
+ *
+ * @see AccessibilityNodeInfo#setAvailableExtraData(List)
+ */
+ public void addExtraDataToAccessibilityNodeInfo(@NonNull View host,
+ @NonNull AccessibilityNodeInfo info, @NonNull String extraDataKey,
+ @Nullable Bundle arguments) {
+ host.addExtraDataToAccessibilityNodeInfo(info, extraDataKey, arguments);
+ }
+
+ /**
+ * Called when a child of the host View has requested sending an
+ * {@link AccessibilityEvent} and gives an opportunity to the parent (the host)
+ * to augment the event.
+ * <p>
+ * The default implementation behaves as
+ * {@link ViewGroup#onRequestSendAccessibilityEvent(View, AccessibilityEvent)
+ * ViewGroup#onRequestSendAccessibilityEvent(View, AccessibilityEvent)} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @param host The View hosting the delegate.
+ * @param child The child which requests sending the event.
+ * @param event The event to be sent.
+ * @return True if the event should be sent
+ *
+ * @see ViewGroup#onRequestSendAccessibilityEvent(View, AccessibilityEvent)
+ * ViewGroup#onRequestSendAccessibilityEvent(View, AccessibilityEvent)
+ */
+ public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
+ AccessibilityEvent event) {
+ return host.onRequestSendAccessibilityEventInternal(child, event);
+ }
+
+ /**
+ * Gets the provider for managing a virtual view hierarchy rooted at this View
+ * and reported to {@link android.accessibilityservice.AccessibilityService}s
+ * that explore the window content.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#getAccessibilityNodeProvider() View#getAccessibilityNodeProvider()} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @return The provider.
+ *
+ * @see AccessibilityNodeProvider
+ */
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ return null;
+ }
+
+ /**
+ * Returns an {@link AccessibilityNodeInfo} representing the host view from the
+ * point of view of an {@link android.accessibilityservice.AccessibilityService}.
+ * This method is responsible for obtaining an accessibility node info from a
+ * pool of reusable instances and calling
+ * {@link #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} on the host
+ * view to initialize the former.
+ * <p>
+ * <strong>Note:</strong> The client is responsible for recycling the obtained
+ * instance by calling {@link AccessibilityNodeInfo#recycle()} to minimize object
+ * creation.
+ * </p>
+ * <p>
+ * The default implementation behaves as
+ * {@link View#createAccessibilityNodeInfo() View#createAccessibilityNodeInfo()} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ * @return A populated {@link AccessibilityNodeInfo}.
+ *
+ * @see AccessibilityNodeInfo
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(View host) {
+ return host.createAccessibilityNodeInfoInternal();
+ }
+ }
+
+ private static class MatchIdPredicate implements Predicate<View> {
+ public int mId;
+
+ @Override
+ public boolean test(View view) {
+ return (view.mID == mId);
+ }
+ }
+
+ private static class MatchLabelForPredicate implements Predicate<View> {
+ private int mLabeledId;
+
+ @Override
+ public boolean test(View view) {
+ return (view.mLabelForId == mLabeledId);
+ }
+ }
+
+
+ /**
+ * Returns the current scroll capture hint for this view.
+ *
+ * @return the current scroll capture hint
+ */
+ @ScrollCaptureHint
+ public int getScrollCaptureHint() {
+ return (mPrivateFlags4 & PFLAG4_SCROLL_CAPTURE_HINT_MASK)
+ >> PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
+ }
+
+ /**
+ * Sets the scroll capture hint for this View. These flags affect the search for a potential
+ * scroll capture targets.
+ *
+ * @param hint the scrollCaptureHint flags value to set
+ */
+ public void setScrollCaptureHint(@ScrollCaptureHint int hint) {
+ mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK;
+ // Since include/exclude are mutually exclusive, exclude takes precedence.
+ if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) {
+ hint &= ~SCROLL_CAPTURE_HINT_INCLUDE;
+ }
+ mPrivateFlags4 |= ((hint << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT)
+ & PFLAG4_SCROLL_CAPTURE_HINT_MASK);
+ }
+
+ /**
+ * Sets the callback to receive scroll capture requests. This component is the adapter between
+ * the scroll capture API and application UI code. If no callback is set, the system may provide
+ * an implementation. Any value provided here will take precedence over a system version.
+ * <p>
+ * This view will be ignored when {@link #SCROLL_CAPTURE_HINT_EXCLUDE} is set in its {@link
+ * #setScrollCaptureHint(int) scrollCaptureHint}, regardless whether a callback has been set.
+ * <p>
+ * It is recommended to set the scroll capture hint {@link #SCROLL_CAPTURE_HINT_INCLUDE} when
+ * setting a custom callback to help ensure it is selected as the target.
+ *
+ * @param callback the new callback to assign
+ */
+ public final void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) {
+ getListenerInfo().mScrollCaptureCallback = callback;
+ }
+
+ /** {@hide} */
+ @Nullable
+ public ScrollCaptureCallback createScrollCaptureCallbackInternal(@NonNull Rect localVisibleRect,
+ @NonNull Point windowOffset) {
+ if (mAttachInfo == null) {
+ return null;
+ }
+ if (mAttachInfo.mScrollCaptureInternal == null) {
+ mAttachInfo.mScrollCaptureInternal = new ScrollCaptureInternal();
+ }
+ return mAttachInfo.mScrollCaptureInternal.requestCallback(this, localVisibleRect,
+ windowOffset);
+ }
+
+ /**
+ * Dispatch a scroll capture search request down the view hierarchy.
+ *
+ * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to
+ * the parent
+ * @param windowOffset the offset of this view within the window
+ * @param targets accepts potential scroll capture targets; {@link Consumer#accept
+ * results.accept} may be called zero or more times on the calling
+ * thread before onScrollCaptureSearch returns
+ */
+ public void dispatchScrollCaptureSearch(
+ @NonNull Rect localVisibleRect, @NonNull Point windowOffset,
+ @NonNull Consumer<ScrollCaptureTarget> targets) {
+ onScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+ }
+
+ /**
+ * Called when scroll capture is requested, to search for appropriate content to scroll. If
+ * applicable, this view adds itself to the provided list for consideration, subject to the
+ * flags set by {@link #setScrollCaptureHint}.
+ *
+ * @param localVisibleRect the local visible rect of this view
+ * @param windowOffset the offset of localVisibleRect within the window
+ * @param targets accepts potential scroll capture targets; {@link Consumer#accept
+ * results.accept} may be called zero or more times on the calling
+ * thread before onScrollCaptureSearch returns
+ * @throws IllegalStateException if this view is not attached to a window
+ */
+ public void onScrollCaptureSearch(@NonNull Rect localVisibleRect,
+ @NonNull Point windowOffset, @NonNull Consumer<ScrollCaptureTarget> targets) {
+ int hint = getScrollCaptureHint();
+ if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) {
+ return;
+ }
+ boolean rectIsVisible = true;
+
+ // Apply clipBounds if present.
+ if (mClipBounds != null) {
+ rectIsVisible = localVisibleRect.intersect(mClipBounds);
+ }
+ if (!rectIsVisible) {
+ return;
+ }
+
+ // Get a callback provided by the framework, library or application.
+ ScrollCaptureCallback callback =
+ (mListenerInfo == null) ? null : mListenerInfo.mScrollCaptureCallback;
+
+ // Try framework support for standard scrolling containers.
+ if (callback == null) {
+ callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset);
+ }
+
+ // If found, then add it to the list.
+ if (callback != null) {
+ // Add to the list for consideration
+ Point offset = new Point(windowOffset.x, windowOffset.y);
+ Rect rect = new Rect(localVisibleRect);
+ targets.accept(new ScrollCaptureTarget(this, rect, offset, callback));
+ }
+ }
+
+ /**
+ * Dump all private flags in readable format, useful for documentation and
+ * consistency checking.
+ */
+ private static void dumpFlags() {
+ final HashMap<String, String> found = Maps.newHashMap();
+ try {
+ for (Field field : View.class.getDeclaredFields()) {
+ final int modifiers = field.getModifiers();
+ if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
+ if (field.getType().equals(int.class)) {
+ final int value = field.getInt(null);
+ dumpFlag(found, field.getName(), value);
+ } else if (field.getType().equals(int[].class)) {
+ final int[] values = (int[]) field.get(null);
+ for (int i = 0; i < values.length; i++) {
+ dumpFlag(found, field.getName() + "[" + i + "]", values[i]);
+ }
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+
+ final ArrayList<String> keys = Lists.newArrayList();
+ keys.addAll(found.keySet());
+ Collections.sort(keys);
+ for (String key : keys) {
+ Log.d(VIEW_LOG_TAG, found.get(key));
+ }
+ }
+
+ private static void dumpFlag(HashMap<String, String> found, String name, int value) {
+ // Sort flags by prefix, then by bits, always keeping unique keys
+ final String bits = String.format("%32s", Integer.toBinaryString(value)).replace('0', ' ');
+ final int prefix = name.indexOf('_');
+ final String key = (prefix > 0 ? name.substring(0, prefix) : name) + bits + name;
+ final String output = bits + " " + name;
+ found.put(key, output);
+ }
+
+ /** {@hide} */
+ public void encode(@NonNull ViewHierarchyEncoder stream) {
+ stream.beginObject(this);
+ encodeProperties(stream);
+ stream.endObject();
+ }
+
+ /** {@hide} */
+ @CallSuper
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ Object resolveId = ViewDebug.resolveId(getContext(), mID);
+ if (resolveId instanceof String) {
+ stream.addProperty("id", (String) resolveId);
+ } else {
+ stream.addProperty("id", mID);
+ }
+
+ stream.addProperty("misc:transformation.alpha",
+ mTransformationInfo != null ? mTransformationInfo.mAlpha : 0);
+ stream.addProperty("misc:transitionName", getTransitionName());
+
+ // layout
+ stream.addProperty("layout:left", mLeft);
+ stream.addProperty("layout:right", mRight);
+ stream.addProperty("layout:top", mTop);
+ stream.addProperty("layout:bottom", mBottom);
+ stream.addProperty("layout:width", getWidth());
+ stream.addProperty("layout:height", getHeight());
+ stream.addProperty("layout:layoutDirection", getLayoutDirection());
+ stream.addProperty("layout:layoutRtl", isLayoutRtl());
+ stream.addProperty("layout:hasTransientState", hasTransientState());
+ stream.addProperty("layout:baseline", getBaseline());
+
+ // layout params
+ ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ if (layoutParams != null) {
+ stream.addPropertyKey("layoutParams");
+ layoutParams.encode(stream);
+ }
+
+ // scrolling
+ stream.addProperty("scrolling:scrollX", mScrollX);
+ stream.addProperty("scrolling:scrollY", mScrollY);
+
+ // padding
+ stream.addProperty("padding:paddingLeft", mPaddingLeft);
+ stream.addProperty("padding:paddingRight", mPaddingRight);
+ stream.addProperty("padding:paddingTop", mPaddingTop);
+ stream.addProperty("padding:paddingBottom", mPaddingBottom);
+ stream.addProperty("padding:userPaddingRight", mUserPaddingRight);
+ stream.addProperty("padding:userPaddingLeft", mUserPaddingLeft);
+ stream.addProperty("padding:userPaddingBottom", mUserPaddingBottom);
+ stream.addProperty("padding:userPaddingStart", mUserPaddingStart);
+ stream.addProperty("padding:userPaddingEnd", mUserPaddingEnd);
+
+ // measurement
+ stream.addProperty("measurement:minHeight", mMinHeight);
+ stream.addProperty("measurement:minWidth", mMinWidth);
+ stream.addProperty("measurement:measuredWidth", mMeasuredWidth);
+ stream.addProperty("measurement:measuredHeight", mMeasuredHeight);
+
+ // drawing
+ stream.addProperty("drawing:elevation", getElevation());
+ stream.addProperty("drawing:translationX", getTranslationX());
+ stream.addProperty("drawing:translationY", getTranslationY());
+ stream.addProperty("drawing:translationZ", getTranslationZ());
+ stream.addProperty("drawing:rotation", getRotation());
+ stream.addProperty("drawing:rotationX", getRotationX());
+ stream.addProperty("drawing:rotationY", getRotationY());
+ stream.addProperty("drawing:scaleX", getScaleX());
+ stream.addProperty("drawing:scaleY", getScaleY());
+ stream.addProperty("drawing:pivotX", getPivotX());
+ stream.addProperty("drawing:pivotY", getPivotY());
+ stream.addProperty("drawing:clipBounds",
+ mClipBounds == null ? null : mClipBounds.toString());
+ stream.addProperty("drawing:opaque", isOpaque());
+ stream.addProperty("drawing:alpha", getAlpha());
+ stream.addProperty("drawing:transitionAlpha", getTransitionAlpha());
+ stream.addProperty("drawing:shadow", hasShadow());
+ stream.addProperty("drawing:solidColor", getSolidColor());
+ stream.addProperty("drawing:layerType", mLayerType);
+ stream.addProperty("drawing:willNotDraw", willNotDraw());
+ stream.addProperty("drawing:hardwareAccelerated", isHardwareAccelerated());
+ stream.addProperty("drawing:willNotCacheDrawing", willNotCacheDrawing());
+ stream.addProperty("drawing:drawingCacheEnabled", isDrawingCacheEnabled());
+ stream.addProperty("drawing:overlappingRendering", hasOverlappingRendering());
+ stream.addProperty("drawing:outlineAmbientShadowColor", getOutlineAmbientShadowColor());
+ stream.addProperty("drawing:outlineSpotShadowColor", getOutlineSpotShadowColor());
+
+ // focus
+ stream.addProperty("focus:hasFocus", hasFocus());
+ stream.addProperty("focus:isFocused", isFocused());
+ stream.addProperty("focus:focusable", getFocusable());
+ stream.addProperty("focus:isFocusable", isFocusable());
+ stream.addProperty("focus:isFocusableInTouchMode", isFocusableInTouchMode());
+
+ stream.addProperty("misc:clickable", isClickable());
+ stream.addProperty("misc:pressed", isPressed());
+ stream.addProperty("misc:selected", isSelected());
+ stream.addProperty("misc:touchMode", isInTouchMode());
+ stream.addProperty("misc:hovered", isHovered());
+ stream.addProperty("misc:activated", isActivated());
+
+ stream.addProperty("misc:visibility", getVisibility());
+ stream.addProperty("misc:fitsSystemWindows", getFitsSystemWindows());
+ stream.addProperty("misc:filterTouchesWhenObscured", getFilterTouchesWhenObscured());
+
+ stream.addProperty("misc:enabled", isEnabled());
+ stream.addProperty("misc:soundEffectsEnabled", isSoundEffectsEnabled());
+ stream.addProperty("misc:hapticFeedbackEnabled", isHapticFeedbackEnabled());
+
+ // theme attributes
+ Resources.Theme theme = getContext().getTheme();
+ if (theme != null) {
+ stream.addPropertyKey("theme");
+ theme.encode(stream);
+ }
+
+ // view attribute information
+ int n = mAttributes != null ? mAttributes.length : 0;
+ stream.addProperty("meta:__attrCount__", n/2);
+ for (int i = 0; i < n; i += 2) {
+ stream.addProperty("meta:__attr__" + mAttributes[i], mAttributes[i+1]);
+ }
+
+ stream.addProperty("misc:scrollBarStyle", getScrollBarStyle());
+
+ // text
+ stream.addProperty("text:textDirection", getTextDirection());
+ stream.addProperty("text:textAlignment", getTextAlignment());
+
+ // accessibility
+ CharSequence contentDescription = getContentDescription();
+ stream.addUserProperty("accessibility:contentDescription",
+ contentDescription == null ? "" : contentDescription.toString());
+ stream.addProperty("accessibility:labelFor", getLabelFor());
+ stream.addProperty("accessibility:importantForAccessibility", getImportantForAccessibility());
+ }
+
+ /**
+ * Determine if this view is rendered on a round wearable device and is the main view
+ * on the screen.
+ */
+ boolean shouldDrawRoundScrollbar() {
+ if (!mResources.getConfiguration().isScreenRound() || mAttachInfo == null) {
+ return false;
+ }
+
+ final View rootView = getRootView();
+ final WindowInsets insets = getRootWindowInsets();
+
+ int height = getHeight();
+ int width = getWidth();
+ int displayHeight = rootView.getHeight();
+ int displayWidth = rootView.getWidth();
+
+ if (height != displayHeight || width != displayWidth) {
+ return false;
+ }
+
+ getLocationInWindow(mAttachInfo.mTmpLocation);
+ return mAttachInfo.mTmpLocation[0] == insets.getStableInsetLeft()
+ && mAttachInfo.mTmpLocation[1] == insets.getStableInsetTop();
+ }
+
+ /**
+ * Sets the tooltip text which will be displayed in a small popup next to the view.
+ * <p>
+ * The tooltip will be displayed:
+ * <ul>
+ * <li>On long click, unless it is handled otherwise (by OnLongClickListener or a context
+ * menu). </li>
+ * <li>On hover, after a brief delay since the pointer has stopped moving </li>
+ * </ul>
+ * <p>
+ * <strong>Note:</strong> Do not override this method, as it will have no
+ * effect on the text displayed in the tooltip.
+ *
+ * @param tooltipText the tooltip text, or null if no tooltip is required
+ * @see #getTooltipText()
+ * @attr ref android.R.styleable#View_tooltipText
+ */
+ public void setTooltipText(@Nullable CharSequence tooltipText) {
+ if (TextUtils.isEmpty(tooltipText)) {
+ setFlags(0, TOOLTIP);
+ hideTooltip();
+ mTooltipInfo = null;
+ } else {
+ setFlags(TOOLTIP, TOOLTIP);
+ if (mTooltipInfo == null) {
+ mTooltipInfo = new TooltipInfo();
+ mTooltipInfo.mShowTooltipRunnable = this::showHoverTooltip;
+ mTooltipInfo.mHideTooltipRunnable = this::hideTooltip;
+ mTooltipInfo.mHoverSlop = ViewConfiguration.get(mContext).getScaledHoverSlop();
+ mTooltipInfo.clearAnchorPos();
+ }
+ mTooltipInfo.mTooltipText = tooltipText;
+ }
+ }
+
+ /**
+ * @hide Binary compatibility stub. To be removed when we finalize O APIs.
+ */
+ @UnsupportedAppUsage
+ public void setTooltip(@Nullable CharSequence tooltipText) {
+ setTooltipText(tooltipText);
+ }
+
+ /**
+ * Returns the view's tooltip text.
+ *
+ * <strong>Note:</strong> Do not override this method, as it will have no
+ * effect on the text displayed in the tooltip. You must call
+ * {@link #setTooltipText(CharSequence)} to modify the tooltip text.
+ *
+ * @return the tooltip text
+ * @see #setTooltipText(CharSequence)
+ * @attr ref android.R.styleable#View_tooltipText
+ */
+ @InspectableProperty
+ @Nullable
+ public CharSequence getTooltipText() {
+ return mTooltipInfo != null ? mTooltipInfo.mTooltipText : null;
+ }
+
+ /**
+ * @hide Binary compatibility stub. To be removed when we finalize O APIs.
+ */
+ @Nullable
+ public CharSequence getTooltip() {
+ return getTooltipText();
+ }
+
+ private boolean showTooltip(int x, int y, boolean fromLongClick) {
+ if (mAttachInfo == null || mTooltipInfo == null) {
+ return false;
+ }
+ if (fromLongClick && (mViewFlags & ENABLED_MASK) != ENABLED) {
+ return false;
+ }
+ if (TextUtils.isEmpty(mTooltipInfo.mTooltipText)) {
+ return false;
+ }
+ hideTooltip();
+ mTooltipInfo.mTooltipFromLongClick = fromLongClick;
+ mTooltipInfo.mTooltipPopup = new TooltipPopup(getContext());
+ final boolean fromTouch = (mPrivateFlags3 & PFLAG3_FINGER_DOWN) == PFLAG3_FINGER_DOWN;
+ mTooltipInfo.mTooltipPopup.show(this, x, y, fromTouch, mTooltipInfo.mTooltipText);
+ mAttachInfo.mTooltipHost = this;
+ // The available accessibility actions have changed
+ notifyViewAccessibilityStateChangedIfNeeded(CONTENT_CHANGE_TYPE_UNDEFINED);
+ return true;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ void hideTooltip() {
+ if (mTooltipInfo == null) {
+ return;
+ }
+ removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
+ if (mTooltipInfo.mTooltipPopup == null) {
+ return;
+ }
+ mTooltipInfo.mTooltipPopup.hide();
+ mTooltipInfo.mTooltipPopup = null;
+ mTooltipInfo.mTooltipFromLongClick = false;
+ mTooltipInfo.clearAnchorPos();
+ if (mAttachInfo != null) {
+ mAttachInfo.mTooltipHost = null;
+ }
+ // The available accessibility actions have changed
+ notifyViewAccessibilityStateChangedIfNeeded(CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ private boolean showLongClickTooltip(int x, int y) {
+ removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
+ removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
+ return showTooltip(x, y, true);
+ }
+
+ private boolean showHoverTooltip() {
+ return showTooltip(mTooltipInfo.mAnchorX, mTooltipInfo.mAnchorY, false);
+ }
+
+ boolean dispatchTooltipHoverEvent(MotionEvent event) {
+ if (mTooltipInfo == null) {
+ return false;
+ }
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if ((mViewFlags & TOOLTIP) != TOOLTIP) {
+ break;
+ }
+ if (!mTooltipInfo.mTooltipFromLongClick && mTooltipInfo.updateAnchorPos(event)) {
+ if (mTooltipInfo.mTooltipPopup == null) {
+ // Schedule showing the tooltip after a timeout.
+ removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
+ postDelayed(mTooltipInfo.mShowTooltipRunnable,
+ ViewConfiguration.getHoverTooltipShowTimeout());
+ }
+
+ // Hide hover-triggered tooltip after a period of inactivity.
+ // Match the timeout used by NativeInputManager to hide the mouse pointer
+ // (depends on SYSTEM_UI_FLAG_LOW_PROFILE being set).
+ final int timeout;
+ if ((getWindowSystemUiVisibility() & SYSTEM_UI_FLAG_LOW_PROFILE)
+ == SYSTEM_UI_FLAG_LOW_PROFILE) {
+ timeout = ViewConfiguration.getHoverTooltipHideShortTimeout();
+ } else {
+ timeout = ViewConfiguration.getHoverTooltipHideTimeout();
+ }
+ removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
+ postDelayed(mTooltipInfo.mHideTooltipRunnable, timeout);
+ }
+ return true;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+ mTooltipInfo.clearAnchorPos();
+ if (!mTooltipInfo.mTooltipFromLongClick) {
+ hideTooltip();
+ }
+ break;
+ }
+ return false;
+ }
+
+ void handleTooltipKey(KeyEvent event) {
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (event.getRepeatCount() == 0) {
+ hideTooltip();
+ }
+ break;
+
+ case KeyEvent.ACTION_UP:
+ handleTooltipUp();
+ break;
+ }
+ }
+
+ private void handleTooltipUp() {
+ if (mTooltipInfo == null || mTooltipInfo.mTooltipPopup == null) {
+ return;
+ }
+ removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
+ postDelayed(mTooltipInfo.mHideTooltipRunnable,
+ ViewConfiguration.getLongPressTooltipHideTimeout());
+ }
+
+ private int getFocusableAttribute(TypedArray attributes) {
+ TypedValue val = new TypedValue();
+ if (attributes.getValue(com.android.internal.R.styleable.View_focusable, val)) {
+ if (val.type == TypedValue.TYPE_INT_BOOLEAN) {
+ return (val.data == 0 ? NOT_FOCUSABLE : FOCUSABLE);
+ } else {
+ return val.data;
+ }
+ } else {
+ return FOCUSABLE_AUTO;
+ }
+ }
+
+ /**
+ * @return The content view of the tooltip popup currently being shown, or null if the tooltip
+ * is not showing.
+ * @hide
+ */
+ @TestApi
+ public View getTooltipView() {
+ if (mTooltipInfo == null || mTooltipInfo.mTooltipPopup == null) {
+ return null;
+ }
+ return mTooltipInfo.mTooltipPopup.getContentView();
+ }
+
+ /**
+ * @return {@code true} if the default focus highlight is enabled, {@code false} otherwies.
+ * @hide
+ */
+ @TestApi
+ public static boolean isDefaultFocusHighlightEnabled() {
+ return sUseDefaultFocusHighlight;
+ }
+
+ /**
+ * Dispatch a previously unhandled {@link KeyEvent} to this view. Unlike normal key dispatch,
+ * this dispatches to ALL child views until it is consumed. The dispatch order is z-order
+ * (visually on-top views first).
+ *
+ * @param evt the previously unhandled {@link KeyEvent}.
+ * @return the {@link View} which consumed the event or {@code null} if not consumed.
+ */
+ View dispatchUnhandledKeyEvent(KeyEvent evt) {
+ if (onUnhandledKeyEvent(evt)) {
+ return this;
+ }
+ return null;
+ }
+
+ /**
+ * Allows this view to handle {@link KeyEvent}s which weren't handled by normal dispatch. This
+ * occurs after the normal view hierarchy dispatch, but before the window callback. By default,
+ * this will dispatch into all the listeners registered via
+ * {@link #addOnUnhandledKeyEventListener(OnUnhandledKeyEventListener)} in last-in-first-out
+ * order (most recently added will receive events first).
+ *
+ * @param event An unhandled event.
+ * @return {@code true} if the event was handled, {@code false} otherwise.
+ * @see #addOnUnhandledKeyEventListener
+ */
+ boolean onUnhandledKeyEvent(@NonNull KeyEvent event) {
+ if (mListenerInfo != null && mListenerInfo.mUnhandledKeyListeners != null) {
+ for (int i = mListenerInfo.mUnhandledKeyListeners.size() - 1; i >= 0; --i) {
+ if (mListenerInfo.mUnhandledKeyListeners.get(i).onUnhandledKeyEvent(this, event)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean hasUnhandledKeyListener() {
+ return (mListenerInfo != null && mListenerInfo.mUnhandledKeyListeners != null
+ && !mListenerInfo.mUnhandledKeyListeners.isEmpty());
+ }
+
+ /**
+ * Adds a listener which will receive unhandled {@link KeyEvent}s. This must be called on the
+ * UI thread.
+ *
+ * @param listener a receiver of unhandled {@link KeyEvent}s.
+ * @see #removeOnUnhandledKeyEventListener
+ */
+ public void addOnUnhandledKeyEventListener(OnUnhandledKeyEventListener listener) {
+ ArrayList<OnUnhandledKeyEventListener> listeners = getListenerInfo().mUnhandledKeyListeners;
+ if (listeners == null) {
+ listeners = new ArrayList<>();
+ getListenerInfo().mUnhandledKeyListeners = listeners;
+ }
+ listeners.add(listener);
+ if (listeners.size() == 1 && mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).incrementChildUnhandledKeyListeners();
+ }
+ }
+
+ /**
+ * Removes a listener which will receive unhandled {@link KeyEvent}s. This must be called on the
+ * UI thread.
+ *
+ * @param listener a receiver of unhandled {@link KeyEvent}s.
+ * @see #addOnUnhandledKeyEventListener
+ */
+ public void removeOnUnhandledKeyEventListener(OnUnhandledKeyEventListener listener) {
+ if (mListenerInfo != null) {
+ if (mListenerInfo.mUnhandledKeyListeners != null
+ && !mListenerInfo.mUnhandledKeyListeners.isEmpty()) {
+ mListenerInfo.mUnhandledKeyListeners.remove(listener);
+ if (mListenerInfo.mUnhandledKeyListeners.isEmpty()) {
+ mListenerInfo.mUnhandledKeyListeners = null;
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).decrementChildUnhandledKeyListeners();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the view to be detached or not detached.
+ *
+ * @param detached Whether the view is detached.
+ *
+ * @hide
+ */
+ protected void setDetached(boolean detached) {
+ if (detached) {
+ mPrivateFlags4 |= PFLAG4_DETACHED;
+ } else {
+ mPrivateFlags4 &= ~PFLAG4_DETACHED;
+ }
+ }
+
+ /**
+ * Collects a {@link ViewTranslationRequest} which represents the content to be translated in
+ * the view.
+ *
+ * <p>The default implementation does nothing.</p>
+ *
+ * @param supportedFormats the supported translation formats. For now, the only possible value
+ * is the {@link android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}.
+ * @param requestsCollector a {@link ViewTranslationRequest} collector that can be used to
+ * collect the information to be translated in the view. The {@code requestsCollector} only
+ * accepts one request; an IllegalStateException is thrown if more than one
+ * {@link ViewTranslationRequest} is submitted to it. The {@link AutofillId} must be set on the
+ * {@link ViewTranslationRequest}.
+ */
+ public void onCreateViewTranslationRequest(@NonNull @DataFormat int[] supportedFormats,
+ @NonNull Consumer<ViewTranslationRequest> requestsCollector) {
+ }
+
+ /**
+ * Collects {@link ViewTranslationRequest}s which represents the content to be translated
+ * for the virtual views in the host view. This is called if this view returned a virtual
+ * view structure from {@link #onProvideContentCaptureStructure} and the system determined that
+ * those virtual views were relevant for translation.
+ *
+ * <p>The default implementation does nothing.</p>
+ *
+ * @param virtualIds the virtual view ids which represents the virtual views in the host
+ * view.
+ * @param supportedFormats the supported translation formats. For now, the only possible value
+ * is the {@link android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}.
+ * @param requestsCollector a {@link ViewTranslationRequest} collector that can be called
+ * multiple times to collect the information to be translated in the host view. One
+ * {@link ViewTranslationRequest} per virtual child. The {@link ViewTranslationRequest} must
+ * contains the {@link AutofillId} corresponding to the virtualChildIds. Do not keep this
+ * Consumer after the method returns.
+ */
+ @SuppressLint("NullableCollection")
+ public void onCreateVirtualViewTranslationRequests(@NonNull long[] virtualIds,
+ @NonNull @DataFormat int[] supportedFormats,
+ @NonNull Consumer<ViewTranslationRequest> requestsCollector) {
+ // no-op
+ }
+
+ /**
+ * Returns a {@link ViewTranslationCallback} that is used to display the translated information
+ * or {@code null} if this View doesn't support translation.
+ *
+ * @hide
+ */
+ @Nullable
+ public ViewTranslationCallback getViewTranslationCallback() {
+ return mViewTranslationCallback;
+ }
+
+ /**
+ * Sets a {@link ViewTranslationCallback} that is used to display/hide the translated
+ * information. Developers can provide the customized implementation for show/hide translated
+ * information.
+ *
+ * @param callback a {@link ViewTranslationCallback} that is used to control how to display the
+ * translated information
+ */
+ public void setViewTranslationCallback(@NonNull ViewTranslationCallback callback) {
+ mViewTranslationCallback = callback;
+ }
+
+ /**
+ * Clear the {@link ViewTranslationCallback} from this view.
+ */
+ public void clearViewTranslationCallback() {
+ mViewTranslationCallback = null;
+ }
+
+ /**
+ * Returns the {@link ViewTranslationResponse} associated with this view. The response will be
+ * set when the translation is done then {@link #onViewTranslationResponse} is called. The
+ * {@link ViewTranslationCallback} can use to get {@link ViewTranslationResponse} to display the
+ * translated information.
+ *
+ * @return a {@link ViewTranslationResponse} that contains the translated information associated
+ * with this view or {@code null} if this View doesn't have the translation.
+ */
+ @Nullable
+ public ViewTranslationResponse getViewTranslationResponse() {
+ return mViewTranslationResponse;
+ }
+
+ /**
+ * Called when the content from {@link View#onCreateViewTranslationRequest} had been translated
+ * by the TranslationService. The {@link ViewTranslationResponse} should be saved here so that
+ * the {@link ViewTranslationResponse} can be used to display the translation when the system
+ * calls {@link ViewTranslationCallback#onShowTranslation}.
+ *
+ * <p> The default implementation will set the ViewTranslationResponse that can be get from
+ * {@link View#getViewTranslationResponse}. </p>
+ *
+ * @param response a {@link ViewTranslationResponse} that contains the translated information
+ * which can be shown in the view.
+ */
+ public void onViewTranslationResponse(@NonNull ViewTranslationResponse response) {
+ mViewTranslationResponse = response;
+ }
+
+ /**
+ * Clears the ViewTranslationResponse stored by the default implementation of {@link
+ * #onViewTranslationResponse}.
+ *
+ * @hide
+ */
+ public void clearViewTranslationResponse() {
+ mViewTranslationResponse = null;
+ }
+
+ /**
+ * Called when the content from {@link View#onCreateVirtualViewTranslationRequests} had been
+ * translated by the TranslationService.
+ *
+ * <p> The default implementation does nothing.</p>
+ *
+ * @param response a {@link ViewTranslationResponse} SparseArray for the request that send by
+ * {@link View#onCreateVirtualViewTranslationRequests} that contains the translated information
+ * which can be shown in the view. The key of SparseArray is the virtual child ids.
+ */
+ public void onVirtualViewTranslationResponses(
+ @NonNull LongSparseArray<ViewTranslationResponse> response) {
+ // no-op
+ }
+
+ /**
+ * Dispatch to collect the {@link ViewTranslationRequest}s for translation purpose by traversing
+ * the hierarchy when the app requests ui translation. Typically, this method should only be
+ * overridden by subclasses that provide a view hierarchy (such as {@link ViewGroup}). Other
+ * classes should override {@link View#onCreateViewTranslationRequest} for normal view or
+ * override {@link View#onVirtualViewTranslationResponses} for view contains virtual children.
+ * When requested to start the ui translation, the system will call this method to traverse the
+ * view hierarchy to collect {@link ViewTranslationRequest}s and create a
+ * {@link android.view.translation.Translator} to translate the requests. All the
+ * {@link ViewTranslationRequest}s must be added when the traversal is done.
+ *
+ * <p> The default implementation calls {@link View#onCreateViewTranslationRequest} for normal
+ * view or calls {@link View#onVirtualViewTranslationResponses} for view contains virtual
+ * children to build {@link ViewTranslationRequest} if the view should be translated.
+ * The view is marked as having {@link #setHasTransientState(boolean) transient state} so that
+ * recycling of views doesn't prevent the system from attaching the response to it. Therefore,
+ * if overriding this method, you should set or reset the transient state. </p>
+ *
+ * @param viewIds a map for the view's {@link AutofillId} and its virtual child ids or
+ * {@code null} if the view doesn't have virtual child that should be translated. The virtual
+ * child ids are the same virtual ids provided by ContentCapture.
+ * @param supportedFormats the supported translation formats. For now, the only possible value
+ * is the {@link android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}.
+ * @param capability a {@link TranslationCapability} that holds translation capability.
+ * information, e.g. source spec, target spec.
+ * @param requests fill in with {@link ViewTranslationRequest}s for translation purpose.
+ */
+ public void dispatchCreateViewTranslationRequest(@NonNull Map<AutofillId, long[]> viewIds,
+ @NonNull @DataFormat int[] supportedFormats,
+ @NonNull TranslationCapability capability,
+ @NonNull List<ViewTranslationRequest> requests) {
+ AutofillId autofillId = getAutofillId();
+ if (viewIds.containsKey(autofillId)) {
+ if (viewIds.get(autofillId) == null) {
+ // TODO: avoiding the allocation per view
+ onCreateViewTranslationRequest(supportedFormats,
+ new ViewTranslationRequestConsumer(requests));
+ } else {
+ onCreateVirtualViewTranslationRequests(viewIds.get(autofillId), supportedFormats,
+ request -> {
+ requests.add(request);
+ });
+ }
+ }
+ }
+
+ private class ViewTranslationRequestConsumer implements Consumer<ViewTranslationRequest> {
+ private final List<ViewTranslationRequest> mRequests;
+ private boolean mCalled;
+
+ ViewTranslationRequestConsumer(List<ViewTranslationRequest> requests) {
+ mRequests = requests;
+ }
+
+ @Override
+ public void accept(ViewTranslationRequest request) {
+ if (mCalled) {
+ throw new IllegalStateException("The translation Consumer is not reusable.");
+ }
+ mCalled = true;
+ if (request != null && request.getKeys().size() > 0) {
+ mRequests.add(request);
+ if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.VERBOSE)) {
+ Log.v(CONTENT_CAPTURE_LOG_TAG, "Calling setHasTransientState(true) for "
+ + getAutofillId());
+ }
+ setHasTransientState(true);
+ setHasTranslationTransientState(true);
+ }
+ }
+ }
+
+ /**
+ * Called to generate a {@link DisplayHash} for this view.
+ *
+ * @param hashAlgorithm The hash algorithm to use when hashing the display. Must be one of
+ * the values returned from
+ * {@link DisplayHashManager#getSupportedHashAlgorithms()}
+ * @param bounds The bounds for the content within the View to generate the hash for. If
+ * bounds are null, the entire View's bounds will be used. If empty, it will
+ * invoke the callback
+ * {@link DisplayHashResultCallback#onDisplayHashError} with error
+ * {@link DisplayHashResultCallback#DISPLAY_HASH_ERROR_INVALID_BOUNDS}
+ * @param executor The executor that the callback should be invoked on.
+ * @param callback The callback to handle the results of generating the display hash
+ */
+ public void generateDisplayHash(@NonNull String hashAlgorithm,
+ @Nullable Rect bounds, @NonNull Executor executor,
+ @NonNull DisplayHashResultCallback callback) {
+ IWindowSession session = getWindowSession();
+ if (session == null) {
+ callback.onDisplayHashError(DISPLAY_HASH_ERROR_MISSING_WINDOW);
+ return;
+ }
+ IWindow window = getWindow();
+ if (window == null) {
+ callback.onDisplayHashError(DISPLAY_HASH_ERROR_MISSING_WINDOW);
+ return;
+ }
+
+ Rect visibleBounds = new Rect();
+ getGlobalVisibleRect(visibleBounds);
+
+ if (bounds != null && bounds.isEmpty()) {
+ callback.onDisplayHashError(DISPLAY_HASH_ERROR_INVALID_BOUNDS);
+ return;
+ }
+
+ if (bounds != null) {
+ bounds.offset(visibleBounds.left, visibleBounds.top);
+ visibleBounds.intersectUnchecked(bounds);
+ }
+
+ if (visibleBounds.isEmpty()) {
+ callback.onDisplayHashError(DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN);
+ return;
+ }
+
+ RemoteCallback remoteCallback = new RemoteCallback(result ->
+ executor.execute(() -> {
+ DisplayHash displayHash = result.getParcelable(EXTRA_DISPLAY_HASH);
+ int errorCode = result.getInt(EXTRA_DISPLAY_HASH_ERROR_CODE,
+ DISPLAY_HASH_ERROR_UNKNOWN);
+ if (displayHash != null) {
+ callback.onDisplayHashResult(displayHash);
+ } else {
+ callback.onDisplayHashError(errorCode);
+ }
+ }));
+
+ try {
+ session.generateDisplayHash(window, visibleBounds, hashAlgorithm, remoteCallback);
+ } catch (RemoteException e) {
+ Log.e(VIEW_LOG_TAG, "Failed to call generateDisplayHash");
+ callback.onDisplayHashError(DISPLAY_HASH_ERROR_UNKNOWN);
+ }
+ }
+
+ /**
+ * The AttachedSurfaceControl itself is not a View, it is just the interface to the
+ * windowing-system object that contains the entire view hierarchy.
+ * For the root View of a given hierarchy see {@link #getRootView}.
+
+ * @return The {@link android.view.AttachedSurfaceControl} interface for this View.
+ * This will only return a non-null value when called between {@link #onAttachedToWindow}
+ * and {@link #onDetachedFromWindow}.
+ */
+ public @Nullable AttachedSurfaceControl getRootSurfaceControl() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.getRootSurfaceControl();
+ }
+ return null;
+ }
+}
diff --git a/android/view/ViewAnimationHostBridge.java b/android/view/ViewAnimationHostBridge.java
new file mode 100644
index 0000000..e0fae21
--- /dev/null
+++ b/android/view/ViewAnimationHostBridge.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.RenderNode;
+
+/**
+ * Maps a View to a RenderNode's AnimationHost
+ *
+ * @hide
+ */
+public class ViewAnimationHostBridge implements RenderNode.AnimationHost {
+ private final View mView;
+
+ /**
+ * @param view the View to bridge to an AnimationHost
+ */
+ public ViewAnimationHostBridge(View view) {
+ mView = view;
+ }
+
+ @Override
+ public void registerAnimatingRenderNode(RenderNode animator) {
+ mView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(animator);
+ }
+
+ @Override
+ public void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator) {
+ mView.mAttachInfo.mViewRootImpl.registerVectorDrawableAnimator(animator);
+ }
+
+ @Override
+ public boolean isAttached() {
+ return mView.mAttachInfo != null;
+ }
+}
diff --git a/android/view/ViewAnimationUtils.java b/android/view/ViewAnimationUtils.java
new file mode 100644
index 0000000..3e277eb
--- /dev/null
+++ b/android/view/ViewAnimationUtils.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.RevealAnimator;
+
+/**
+ * Defines common utilities for working with View's animations.
+ *
+ */
+public final class ViewAnimationUtils {
+ private ViewAnimationUtils() {}
+ /**
+ * Returns an Animator which can animate a clipping circle.
+ * <p>
+ * Any shadow cast by the View will respect the circular clip from this animator.
+ * <p>
+ * Only a single non-rectangular clip can be applied on a View at any time.
+ * Views clipped by a circular reveal animation take priority over
+ * {@link View#setClipToOutline(boolean) View Outline clipping}.
+ * <p>
+ * Note that the animation returned here is a one-shot animation. It cannot
+ * be re-used, and once started it cannot be paused or resumed. It is also
+ * an asynchronous animation that automatically runs off of the UI thread.
+ * As a result {@link AnimatorListener#onAnimationEnd(Animator)}
+ * will occur after the animation has ended, but it may be delayed depending
+ * on thread responsiveness.
+ * <p>
+ * Note that if any start delay is set on the reveal animator, the start radius
+ * will not be applied to the reveal circle until the start delay has passed.
+ * If it's desired to set a start radius on the reveal circle during the start
+ * delay, one workaround could be adding an animator with the same start and
+ * end radius. For example:
+ * <pre><code>
+ * public static Animator createRevealWithDelay(View view, int centerX, int centerY, float startRadius, float endRadius) {
+ * Animator delayAnimator = ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, startRadius);
+ * delayAnimator.setDuration(delayTimeMS);
+ * Animator revealAnimator = ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, endRadius);
+ * AnimatorSet set = new AnimatorSet();
+ * set.playSequentially(delayAnimator, revealAnimator);
+ * return set;
+ * }
+ * </code></pre>
+ *
+ * @param view The View will be clipped to the animating circle.
+ * @param centerX The x coordinate of the center of the animating circle, relative to
+ * <code>view</code>.
+ * @param centerY The y coordinate of the center of the animating circle, relative to
+ * <code>view</code>.
+ * @param startRadius The starting radius of the animating circle.
+ * @param endRadius The ending radius of the animating circle.
+ */
+ public static Animator createCircularReveal(View view,
+ int centerX, int centerY, float startRadius, float endRadius) {
+ return new RevealAnimator(view, centerX, centerY, startRadius, endRadius);
+ }
+}
diff --git a/android/view/ViewConfiguration.java b/android/view/ViewConfiguration.java
new file mode 100644
index 0000000..0a3d0da
--- /dev/null
+++ b/android/view/ViewConfiguration.java
@@ -0,0 +1,1134 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.FloatRange;
+import android.annotation.TestApi;
+import android.annotation.UiContext;
+import android.app.Activity;
+import android.app.AppGlobals;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.SparseArray;
+import android.util.TypedValue;
+
+/**
+ * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
+ */
+public class ViewConfiguration {
+ private static final String TAG = "ViewConfiguration";
+
+ /**
+ * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
+ * dips
+ */
+ private static final int SCROLL_BAR_SIZE = 4;
+
+ /**
+ * Duration of the fade when scrollbars fade away in milliseconds
+ */
+ private static final int SCROLL_BAR_FADE_DURATION = 250;
+
+ /**
+ * Default delay before the scrollbars fade in milliseconds
+ */
+ private static final int SCROLL_BAR_DEFAULT_DELAY = 300;
+
+ /**
+ * Defines the length of the fading edges in dips
+ */
+ private static final int FADING_EDGE_LENGTH = 12;
+
+ /**
+ * Defines the duration in milliseconds of the pressed state in child
+ * components.
+ */
+ private static final int PRESSED_STATE_DURATION = 64;
+
+ /**
+ * Defines the default duration in milliseconds before a press turns into
+ * a long press
+ * @hide
+ */
+ public static final int DEFAULT_LONG_PRESS_TIMEOUT = 400;
+
+ /**
+ * Defines the default duration in milliseconds between the first tap's up event and the second
+ * tap's down event for an interaction to be considered part of the same multi-press.
+ */
+ private static final int DEFAULT_MULTI_PRESS_TIMEOUT = 300;
+
+ /**
+ * Defines the time between successive key repeats in milliseconds.
+ */
+ private static final int KEY_REPEAT_DELAY = 50;
+
+ /**
+ * Defines the duration in milliseconds a user needs to hold down the
+ * appropriate button to bring up the global actions dialog (power off,
+ * lock screen, etc).
+ */
+ private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500;
+
+ /**
+ * Defines the duration in milliseconds a user needs to hold down the
+ * appropriate buttons (power + volume down) to trigger the screenshot chord.
+ */
+ private static final int SCREENSHOT_CHORD_KEY_TIMEOUT = 0;
+
+ /**
+ * Defines the duration in milliseconds a user needs to hold down the
+ * appropriate button to bring up the accessibility shortcut for the first time
+ */
+ private static final int A11Y_SHORTCUT_KEY_TIMEOUT = 3000;
+
+ /**
+ * Defines the duration in milliseconds a user needs to hold down the
+ * appropriate button to enable the accessibility shortcut once it's configured.
+ */
+ private static final int A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION = 1000;
+
+ /**
+ * Defines the duration in milliseconds we will wait to see if a touch event
+ * is a tap or a scroll. If the user does not move within this interval, it is
+ * considered to be a tap.
+ */
+ private static final int TAP_TIMEOUT = 100;
+
+ /**
+ * Defines the duration in milliseconds we will wait to see if a touch event
+ * is a jump tap. If the user does not complete the jump tap within this interval, it is
+ * considered to be a tap.
+ */
+ private static final int JUMP_TAP_TIMEOUT = 500;
+
+ /**
+ * Defines the duration in milliseconds between the first tap's up event and
+ * the second tap's down event for an interaction to be considered a
+ * double-tap.
+ */
+ private static final int DOUBLE_TAP_TIMEOUT = 300;
+
+ /**
+ * Defines the minimum duration in milliseconds between the first tap's up event and
+ * the second tap's down event for an interaction to be considered a
+ * double-tap.
+ */
+ private static final int DOUBLE_TAP_MIN_TIME = 40;
+
+ /**
+ * Defines the maximum duration in milliseconds between a touch pad
+ * touch and release for a given touch to be considered a tap (click) as
+ * opposed to a hover movement gesture.
+ */
+ private static final int HOVER_TAP_TIMEOUT = 150;
+
+ /**
+ * Defines the maximum distance in pixels that a touch pad touch can move
+ * before being released for it to be considered a tap (click) as opposed
+ * to a hover movement gesture.
+ */
+ private static final int HOVER_TAP_SLOP = 20;
+
+ /**
+ * Defines the duration in milliseconds we want to display zoom controls in response
+ * to a user panning within an application.
+ */
+ private static final int ZOOM_CONTROLS_TIMEOUT = 3000;
+
+ /**
+ * Inset in dips to look for touchable content when the user touches the edge of the screen
+ */
+ private static final int EDGE_SLOP = 12;
+
+ /**
+ * Distance a touch can wander before we think the user is scrolling in dips.
+ * Note that this value defined here is only used as a fallback by legacy/misbehaving
+ * applications that do not provide a Context for determining density/configuration-dependent
+ * values.
+ *
+ * To alter this value, see the configuration resource config_viewConfigurationTouchSlop
+ * in frameworks/base/core/res/res/values/config.xml or the appropriate device resource overlay.
+ * It may be appropriate to tweak this on a device-specific basis in an overlay based on
+ * the characteristics of the touch panel and firmware.
+ */
+ private static final int TOUCH_SLOP = 8;
+
+ /**
+ * Defines the minimum size of the touch target for a scrollbar in dips
+ */
+ private static final int MIN_SCROLLBAR_TOUCH_TARGET = 48;
+
+ /**
+ * Distance the first touch can wander before we stop considering this event a double tap
+ * (in dips)
+ */
+ private static final int DOUBLE_TAP_TOUCH_SLOP = TOUCH_SLOP;
+
+ /**
+ * Distance a touch can wander before we think the user is attempting a paged scroll
+ * (in dips)
+ *
+ * Note that this value defined here is only used as a fallback by legacy/misbehaving
+ * applications that do not provide a Context for determining density/configuration-dependent
+ * values.
+ *
+ * See the note above on {@link #TOUCH_SLOP} regarding the dimen resource
+ * config_viewConfigurationTouchSlop. ViewConfiguration will report a paging touch slop of
+ * config_viewConfigurationTouchSlop * 2 when provided with a Context.
+ */
+ private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2;
+
+ /**
+ * Distance in dips between the first touch and second touch to still be considered a double tap
+ */
+ private static final int DOUBLE_TAP_SLOP = 100;
+
+ /**
+ * Distance in dips a touch needs to be outside of a window's bounds for it to
+ * count as outside for purposes of dismissing the window.
+ */
+ private static final int WINDOW_TOUCH_SLOP = 16;
+
+ /**
+ * Minimum velocity to initiate a fling, as measured in dips per second
+ */
+ private static final int MINIMUM_FLING_VELOCITY = 50;
+
+ /**
+ * Maximum velocity to initiate a fling, as measured in dips per second
+ */
+ private static final int MAXIMUM_FLING_VELOCITY = 8000;
+
+ /**
+ * Delay before dispatching a recurring accessibility event in milliseconds.
+ * This delay guarantees that a recurring event will be send at most once
+ * during the {@link #SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS} time
+ * frame.
+ */
+ private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 100;
+
+ /**
+ * The maximum size of View's drawing cache, expressed in bytes. This size
+ * should be at least equal to the size of the screen in ARGB888 format.
+ */
+ @Deprecated
+ private static final int MAXIMUM_DRAWING_CACHE_SIZE = 480 * 800 * 4; // ARGB8888
+
+ /**
+ * The coefficient of friction applied to flings/scrolls.
+ */
+ @UnsupportedAppUsage
+ private static final float SCROLL_FRICTION = 0.015f;
+
+ /**
+ * Max distance in dips to overscroll for edge effects
+ */
+ private static final int OVERSCROLL_DISTANCE = 0;
+
+ /**
+ * Max distance in dips to overfling for edge effects
+ */
+ private static final int OVERFLING_DISTANCE = 6;
+
+ /**
+ * Amount to scroll in response to a horizontal {@link MotionEvent#ACTION_SCROLL} event,
+ * in dips per axis value.
+ */
+ private static final float HORIZONTAL_SCROLL_FACTOR = 64;
+
+ /**
+ * Amount to scroll in response to a vertical {@link MotionEvent#ACTION_SCROLL} event,
+ * in dips per axis value.
+ */
+ private static final float VERTICAL_SCROLL_FACTOR = 64;
+
+ /**
+ * Default duration to hide an action mode for.
+ */
+ private static final long ACTION_MODE_HIDE_DURATION_DEFAULT = 2000;
+
+ /**
+ * Defines the duration in milliseconds before an end of a long press causes a tooltip to be
+ * hidden.
+ */
+ private static final int LONG_PRESS_TOOLTIP_HIDE_TIMEOUT = 1500;
+
+ /**
+ * Defines the duration in milliseconds before a hover event causes a tooltip to be shown.
+ */
+ private static final int HOVER_TOOLTIP_SHOW_TIMEOUT = 500;
+
+ /**
+ * Defines the duration in milliseconds before mouse inactivity causes a tooltip to be hidden.
+ * (default variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is not set).
+ */
+ private static final int HOVER_TOOLTIP_HIDE_TIMEOUT = 15000;
+
+ /**
+ * Defines the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
+ * (short version to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is set).
+ */
+ private static final int HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT = 3000;
+
+ /**
+ * Configuration values for overriding {@link #hasPermanentMenuKey()} behavior.
+ * These constants must match the definition in res/values/config.xml.
+ */
+ private static final int HAS_PERMANENT_MENU_KEY_AUTODETECT = 0;
+ private static final int HAS_PERMANENT_MENU_KEY_TRUE = 1;
+ private static final int HAS_PERMANENT_MENU_KEY_FALSE = 2;
+
+ /**
+ * The multiplication factor for inhibiting default gestures.
+ */
+ private static final float AMBIGUOUS_GESTURE_MULTIPLIER = 2f;
+
+ /**
+ * The timeout value in milliseconds to adjust the selection span and actions for the selected
+ * text when TextClassifier has been initialized.
+ */
+ private static final int SMART_SELECTION_INITIALIZED_TIMEOUT_IN_MILLISECOND = 200;
+
+ /**
+ * The timeout value in milliseconds to adjust the selection span and actions for the selected
+ * text when TextClassifier has not been initialized.
+ */
+ private static final int SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND = 500;
+
+ private final boolean mConstructedWithContext;
+ private final int mEdgeSlop;
+ private final int mFadingEdgeLength;
+ private final int mMinimumFlingVelocity;
+ private final int mMaximumFlingVelocity;
+ private final int mScrollbarSize;
+ private final int mTouchSlop;
+ private final int mMinScalingSpan;
+ private final int mHoverSlop;
+ private final int mMinScrollbarTouchTarget;
+ private final int mDoubleTapTouchSlop;
+ private final int mPagingTouchSlop;
+ private final int mDoubleTapSlop;
+ private final int mWindowTouchSlop;
+ private final float mAmbiguousGestureMultiplier;
+ private final int mMaximumDrawingCacheSize;
+ private final int mOverscrollDistance;
+ private final int mOverflingDistance;
+ @UnsupportedAppUsage
+ private final boolean mFadingMarqueeEnabled;
+ private final long mGlobalActionsKeyTimeout;
+ private final float mVerticalScrollFactor;
+ private final float mHorizontalScrollFactor;
+ private final boolean mShowMenuShortcutsWhenKeyboardPresent;
+ private final long mScreenshotChordKeyTimeout;
+ private final int mSmartSelectionInitializedTimeout;
+ private final int mSmartSelectionInitializingTimeout;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768915)
+ private boolean sHasPermanentMenuKey;
+ @UnsupportedAppUsage
+ private boolean sHasPermanentMenuKeySet;
+
+ @UnsupportedAppUsage
+ static final SparseArray<ViewConfiguration> sConfigurations =
+ new SparseArray<ViewConfiguration>(2);
+
+ /**
+ * @deprecated Use {@link android.view.ViewConfiguration#get(android.content.Context)} instead.
+ */
+ @Deprecated
+ public ViewConfiguration() {
+ mConstructedWithContext = false;
+ mEdgeSlop = EDGE_SLOP;
+ mFadingEdgeLength = FADING_EDGE_LENGTH;
+ mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY;
+ mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
+ mScrollbarSize = SCROLL_BAR_SIZE;
+ mTouchSlop = TOUCH_SLOP;
+ mHoverSlop = TOUCH_SLOP / 2;
+ mMinScrollbarTouchTarget = MIN_SCROLLBAR_TOUCH_TARGET;
+ mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP;
+ mPagingTouchSlop = PAGING_TOUCH_SLOP;
+ mDoubleTapSlop = DOUBLE_TAP_SLOP;
+ mWindowTouchSlop = WINDOW_TOUCH_SLOP;
+ mAmbiguousGestureMultiplier = AMBIGUOUS_GESTURE_MULTIPLIER;
+ //noinspection deprecation
+ mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
+ mOverscrollDistance = OVERSCROLL_DISTANCE;
+ mOverflingDistance = OVERFLING_DISTANCE;
+ mFadingMarqueeEnabled = true;
+ mGlobalActionsKeyTimeout = GLOBAL_ACTIONS_KEY_TIMEOUT;
+ mHorizontalScrollFactor = HORIZONTAL_SCROLL_FACTOR;
+ mVerticalScrollFactor = VERTICAL_SCROLL_FACTOR;
+ mShowMenuShortcutsWhenKeyboardPresent = false;
+ mScreenshotChordKeyTimeout = SCREENSHOT_CHORD_KEY_TIMEOUT;
+
+ // Getter throws if mConstructedWithContext is false so doesn't matter what
+ // this value is.
+ mMinScalingSpan = 0;
+ mSmartSelectionInitializedTimeout = SMART_SELECTION_INITIALIZED_TIMEOUT_IN_MILLISECOND;
+ mSmartSelectionInitializingTimeout = SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND;
+ }
+
+ /**
+ * Creates a new configuration for the specified visual {@link Context}. The configuration
+ * depends on various parameters of the {@link Context}, like the dimension of the display or
+ * the density of the display.
+ *
+ * @param context A visual {@link Context} used to initialize the view configuration. It must
+ * be {@link Activity} or other {@link Context} created with
+ * {@link Context#createWindowContext(int, Bundle)}.
+ *
+ * @see #get(android.content.Context)
+ * @see android.util.DisplayMetrics
+ */
+ private ViewConfiguration(@UiContext Context context) {
+ mConstructedWithContext = true;
+ final Resources res = context.getResources();
+ final DisplayMetrics metrics = res.getDisplayMetrics();
+ final Configuration config = res.getConfiguration();
+ final float density = metrics.density;
+ final float sizeAndDensity;
+ if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
+ sizeAndDensity = density * 1.5f;
+ } else {
+ sizeAndDensity = density;
+ }
+
+ mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
+ mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
+ mScrollbarSize = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_scrollbarSize);
+ mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
+ mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
+
+ final TypedValue multiplierValue = new TypedValue();
+ res.getValue(
+ com.android.internal.R.dimen.config_ambiguousGestureMultiplier,
+ multiplierValue,
+ true /*resolveRefs*/);
+ mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat());
+
+ // Size of the screen in bytes, in ARGB_8888 format
+ final WindowManager windowManager = context.getSystemService(WindowManager.class);
+ final Rect maxWindowBounds = windowManager.getMaximumWindowMetrics().getBounds();
+ mMaximumDrawingCacheSize = 4 * maxWindowBounds.width() * maxWindowBounds.height();
+
+ mOverscrollDistance = (int) (sizeAndDensity * OVERSCROLL_DISTANCE + 0.5f);
+ mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
+
+ if (!sHasPermanentMenuKeySet) {
+ final int configVal = res.getInteger(
+ com.android.internal.R.integer.config_overrideHasPermanentMenuKey);
+
+ switch (configVal) {
+ default:
+ case HAS_PERMANENT_MENU_KEY_AUTODETECT: {
+ IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ try {
+ sHasPermanentMenuKey = !wm.hasNavigationBar(context.getDisplayId());
+ sHasPermanentMenuKeySet = true;
+ } catch (RemoteException ex) {
+ sHasPermanentMenuKey = false;
+ }
+ }
+ break;
+
+ case HAS_PERMANENT_MENU_KEY_TRUE:
+ sHasPermanentMenuKey = true;
+ sHasPermanentMenuKeySet = true;
+ break;
+
+ case HAS_PERMANENT_MENU_KEY_FALSE:
+ sHasPermanentMenuKey = false;
+ sHasPermanentMenuKeySet = true;
+ break;
+ }
+ }
+
+ mFadingMarqueeEnabled = res.getBoolean(
+ com.android.internal.R.bool.config_ui_enableFadingMarquee);
+ mTouchSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
+ mHoverSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationHoverSlop);
+ mMinScrollbarTouchTarget = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_minScrollbarTouchTarget);
+ mPagingTouchSlop = mTouchSlop * 2;
+
+ mDoubleTapTouchSlop = mTouchSlop;
+
+ mMinimumFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMinFlingVelocity);
+ mMaximumFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMaxFlingVelocity);
+ mGlobalActionsKeyTimeout = res.getInteger(
+ com.android.internal.R.integer.config_globalActionsKeyTimeout);
+
+ mHorizontalScrollFactor = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_horizontalScrollFactor);
+ mVerticalScrollFactor = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_verticalScrollFactor);
+
+ mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean(
+ com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
+
+ mMinScalingSpan = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_minScalingSpan);
+
+ mScreenshotChordKeyTimeout = res.getInteger(
+ com.android.internal.R.integer.config_screenshotChordKeyTimeout);
+
+ mSmartSelectionInitializedTimeout = res.getInteger(
+ com.android.internal.R.integer.config_smartSelectionInitializedTimeoutMillis);
+ mSmartSelectionInitializingTimeout = res.getInteger(
+ com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis);
+ }
+
+ /**
+ * Returns a configuration for the specified visual {@link Context}. The configuration depends
+ * on various parameters of the {@link Context}, like the dimension of the display or the
+ * density of the display.
+ *
+ * @param context A visual {@link Context} used to initialize the view configuration. It must
+ * be {@link Activity} or other {@link Context} created with
+ * {@link Context#createWindowContext(int, Bundle)}.
+ */
+ // TODO(b/182007470): Use @ConfigurationContext instead
+ public static ViewConfiguration get(@UiContext Context context) {
+ StrictMode.assertConfigurationContext(context, "ViewConfiguration");
+
+ final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ final int density = (int) (100.0f * metrics.density);
+
+ ViewConfiguration configuration = sConfigurations.get(density);
+ if (configuration == null) {
+ configuration = new ViewConfiguration(context);
+ sConfigurations.put(density, configuration);
+ }
+
+ return configuration;
+ }
+
+ /**
+ * @return The width of the horizontal scrollbar and the height of the vertical
+ * scrollbar in dips
+ *
+ * @deprecated Use {@link #getScaledScrollBarSize()} instead.
+ */
+ @Deprecated
+ public static int getScrollBarSize() {
+ return SCROLL_BAR_SIZE;
+ }
+
+ /**
+ * @return The width of the horizontal scrollbar and the height of the vertical
+ * scrollbar in pixels
+ */
+ public int getScaledScrollBarSize() {
+ return mScrollbarSize;
+ }
+
+ /**
+ * @return the minimum size of the scrollbar thumb's touch target in pixels
+ * @hide
+ */
+ public int getScaledMinScrollbarTouchTarget() {
+ return mMinScrollbarTouchTarget;
+ }
+
+ /**
+ * @return Duration of the fade when scrollbars fade away in milliseconds
+ */
+ public static int getScrollBarFadeDuration() {
+ return SCROLL_BAR_FADE_DURATION;
+ }
+
+ /**
+ * @return Default delay before the scrollbars fade in milliseconds
+ */
+ public static int getScrollDefaultDelay() {
+ return SCROLL_BAR_DEFAULT_DELAY;
+ }
+
+ /**
+ * @return the length of the fading edges in dips
+ *
+ * @deprecated Use {@link #getScaledFadingEdgeLength()} instead.
+ */
+ @Deprecated
+ public static int getFadingEdgeLength() {
+ return FADING_EDGE_LENGTH;
+ }
+
+ /**
+ * @return the length of the fading edges in pixels
+ */
+ public int getScaledFadingEdgeLength() {
+ return mFadingEdgeLength;
+ }
+
+ /**
+ * @return the duration in milliseconds of the pressed state in child
+ * components.
+ */
+ public static int getPressedStateDuration() {
+ return PRESSED_STATE_DURATION;
+ }
+
+ /**
+ * @return the duration in milliseconds before a press turns into
+ * a long press
+ */
+ public static int getLongPressTimeout() {
+ return AppGlobals.getIntCoreSetting(Settings.Secure.LONG_PRESS_TIMEOUT,
+ DEFAULT_LONG_PRESS_TIMEOUT);
+ }
+
+ /**
+ * @return the duration in milliseconds between the first tap's up event and the second tap's
+ * down event for an interaction to be considered part of the same multi-press.
+ */
+ public static int getMultiPressTimeout() {
+ return AppGlobals.getIntCoreSetting(Settings.Secure.MULTI_PRESS_TIMEOUT,
+ DEFAULT_MULTI_PRESS_TIMEOUT);
+ }
+
+ /**
+ * @return the time before the first key repeat in milliseconds.
+ */
+ public static int getKeyRepeatTimeout() {
+ return getLongPressTimeout();
+ }
+
+ /**
+ * @return the time between successive key repeats in milliseconds.
+ */
+ public static int getKeyRepeatDelay() {
+ return KEY_REPEAT_DELAY;
+ }
+
+ /**
+ * @return the duration in milliseconds we will wait to see if a touch event
+ * is a tap or a scroll. If the user does not move within this interval, it is
+ * considered to be a tap.
+ */
+ public static int getTapTimeout() {
+ return TAP_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds we will wait to see if a touch event
+ * is a jump tap. If the user does not move within this interval, it is
+ * considered to be a tap.
+ */
+ public static int getJumpTapTimeout() {
+ return JUMP_TAP_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds between the first tap's up event and
+ * the second tap's down event for an interaction to be considered a
+ * double-tap.
+ */
+ public static int getDoubleTapTimeout() {
+ return DOUBLE_TAP_TIMEOUT;
+ }
+
+ /**
+ * @return the minimum duration in milliseconds between the first tap's
+ * up event and the second tap's down event for an interaction to be considered a
+ * double-tap.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static int getDoubleTapMinTime() {
+ return DOUBLE_TAP_MIN_TIME;
+ }
+
+ /**
+ * @return the maximum duration in milliseconds between a touch pad
+ * touch and release for a given touch to be considered a tap (click) as
+ * opposed to a hover movement gesture.
+ * @hide
+ */
+ public static int getHoverTapTimeout() {
+ return HOVER_TAP_TIMEOUT;
+ }
+
+ /**
+ * @return the maximum distance in pixels that a touch pad touch can move
+ * before being released for it to be considered a tap (click) as opposed
+ * to a hover movement gesture.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static int getHoverTapSlop() {
+ return HOVER_TAP_SLOP;
+ }
+
+ /**
+ * @return Inset in dips to look for touchable content when the user touches the edge of the
+ * screen
+ *
+ * @deprecated Use {@link #getScaledEdgeSlop()} instead.
+ */
+ @Deprecated
+ public static int getEdgeSlop() {
+ return EDGE_SLOP;
+ }
+
+ /**
+ * @return Inset in pixels to look for touchable content when the user touches the edge of the
+ * screen
+ */
+ public int getScaledEdgeSlop() {
+ return mEdgeSlop;
+ }
+
+ /**
+ * @return Distance in dips a touch can wander before we think the user is scrolling
+ *
+ * @deprecated Use {@link #getScaledTouchSlop()} instead.
+ */
+ @Deprecated
+ public static int getTouchSlop() {
+ return TOUCH_SLOP;
+ }
+
+ /**
+ * @return Distance in pixels a touch can wander before we think the user is scrolling
+ */
+ public int getScaledTouchSlop() {
+ return mTouchSlop;
+ }
+
+ /**
+ * @return Distance in pixels a hover can wander while it is still considered "stationary".
+ *
+ */
+ public int getScaledHoverSlop() {
+ return mHoverSlop;
+ }
+
+ /**
+ * @return Distance in pixels the first touch can wander before we do not consider this a
+ * potential double tap event
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getScaledDoubleTapTouchSlop() {
+ return mDoubleTapTouchSlop;
+ }
+
+ /**
+ * @return Distance in pixels a touch can wander before we think the user is scrolling a full
+ * page
+ */
+ public int getScaledPagingTouchSlop() {
+ return mPagingTouchSlop;
+ }
+
+ /**
+ * @return Distance in dips between the first touch and second touch to still be
+ * considered a double tap
+ * @deprecated Use {@link #getScaledDoubleTapSlop()} instead.
+ * @hide The only client of this should be GestureDetector, which needs this
+ * for clients that still use its deprecated constructor.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public static int getDoubleTapSlop() {
+ return DOUBLE_TAP_SLOP;
+ }
+
+ /**
+ * @return Distance in pixels between the first touch and second touch to still be
+ * considered a double tap
+ */
+ public int getScaledDoubleTapSlop() {
+ return mDoubleTapSlop;
+ }
+
+ /**
+ * Interval for dispatching a recurring accessibility event in milliseconds.
+ * This interval guarantees that a recurring event will be send at most once
+ * during the {@link #getSendRecurringAccessibilityEventsInterval()} time frame.
+ *
+ * @return The delay in milliseconds.
+ *
+ * @hide
+ */
+ public static long getSendRecurringAccessibilityEventsInterval() {
+ return SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS;
+ }
+
+ /**
+ * @return Distance in dips a touch must be outside the bounds of a window for it
+ * to be counted as outside the window for purposes of dismissing that
+ * window.
+ *
+ * @deprecated Use {@link #getScaledWindowTouchSlop()} instead.
+ */
+ @Deprecated
+ public static int getWindowTouchSlop() {
+ return WINDOW_TOUCH_SLOP;
+ }
+
+ /**
+ * @return Distance in pixels a touch must be outside the bounds of a window for it
+ * to be counted as outside the window for purposes of dismissing that window.
+ */
+ public int getScaledWindowTouchSlop() {
+ return mWindowTouchSlop;
+ }
+
+ /**
+ * @return Minimum velocity to initiate a fling, as measured in dips per second.
+ *
+ * @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead.
+ */
+ @Deprecated
+ public static int getMinimumFlingVelocity() {
+ return MINIMUM_FLING_VELOCITY;
+ }
+
+ /**
+ * @return Minimum velocity to initiate a fling, as measured in pixels per second.
+ */
+ public int getScaledMinimumFlingVelocity() {
+ return mMinimumFlingVelocity;
+ }
+
+ /**
+ * @return Maximum velocity to initiate a fling, as measured in dips per second.
+ *
+ * @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead.
+ */
+ @Deprecated
+ public static int getMaximumFlingVelocity() {
+ return MAXIMUM_FLING_VELOCITY;
+ }
+
+ /**
+ * @return Maximum velocity to initiate a fling, as measured in pixels per second.
+ */
+ public int getScaledMaximumFlingVelocity() {
+ return mMaximumFlingVelocity;
+ }
+
+ /**
+ * @return Amount to scroll in response to a {@link MotionEvent#ACTION_SCROLL} event. Multiply
+ * this by the event's axis value to obtain the number of pixels to be scrolled.
+ *
+ * @removed
+ */
+ public int getScaledScrollFactor() {
+ return (int) mVerticalScrollFactor;
+ }
+
+ /**
+ * @return Amount to scroll in response to a horizontal {@link MotionEvent#ACTION_SCROLL} event.
+ * Multiply this by the event's axis value to obtain the number of pixels to be scrolled.
+ */
+ public float getScaledHorizontalScrollFactor() {
+ return mHorizontalScrollFactor;
+ }
+
+ /**
+ * @return Amount to scroll in response to a vertical {@link MotionEvent#ACTION_SCROLL} event.
+ * Multiply this by the event's axis value to obtain the number of pixels to be scrolled.
+ */
+ public float getScaledVerticalScrollFactor() {
+ return mVerticalScrollFactor;
+ }
+
+ /**
+ * The maximum drawing cache size expressed in bytes.
+ *
+ * @return the maximum size of View's drawing cache expressed in bytes
+ *
+ * @deprecated Use {@link #getScaledMaximumDrawingCacheSize()} instead.
+ */
+ @Deprecated
+ public static int getMaximumDrawingCacheSize() {
+ //noinspection deprecation
+ return MAXIMUM_DRAWING_CACHE_SIZE;
+ }
+
+ /**
+ * The maximum drawing cache size expressed in bytes.
+ *
+ * @return the maximum size of View's drawing cache expressed in bytes
+ */
+ public int getScaledMaximumDrawingCacheSize() {
+ return mMaximumDrawingCacheSize;
+ }
+
+ /**
+ * @return The maximum distance a View should overscroll by when showing edge effects (in
+ * pixels).
+ */
+ public int getScaledOverscrollDistance() {
+ return mOverscrollDistance;
+ }
+
+ /**
+ * @return The maximum distance a View should overfling by when showing edge effects (in
+ * pixels).
+ */
+ public int getScaledOverflingDistance() {
+ return mOverflingDistance;
+ }
+
+ /**
+ * The amount of time that the zoom controls should be
+ * displayed on the screen expressed in milliseconds.
+ *
+ * @return the time the zoom controls should be visible expressed
+ * in milliseconds.
+ */
+ public static long getZoomControlsTimeout() {
+ return ZOOM_CONTROLS_TIMEOUT;
+ }
+
+ /**
+ * The amount of time a user needs to press the relevant key to bring up
+ * the global actions dialog.
+ *
+ * @return how long a user needs to press the relevant key to bring up
+ * the global actions dialog.
+ * @deprecated This timeout should not be used by applications
+ */
+ @Deprecated
+ public static long getGlobalActionKeyTimeout() {
+ return GLOBAL_ACTIONS_KEY_TIMEOUT;
+ }
+
+ /**
+ * The amount of time a user needs to press the relevant key to bring up
+ * the global actions dialog.
+ *
+ * @return how long a user needs to press the relevant key to bring up
+ * the global actions dialog.
+ * @hide
+ */
+ @TestApi
+ public long getDeviceGlobalActionKeyTimeout() {
+ return mGlobalActionsKeyTimeout;
+ }
+
+ /**
+ * The amount of time a user needs to press the relevant keys to trigger
+ * the screenshot chord.
+ *
+ * @return how long a user needs to press the relevant keys to trigger
+ * the screenshot chord.
+ * @hide
+ */
+ public long getScreenshotChordKeyTimeout() {
+ return mScreenshotChordKeyTimeout;
+ }
+
+ /**
+ * The amount of time a user needs to press the relevant keys to activate the accessibility
+ * shortcut.
+ *
+ * @return how long a user needs to press the relevant keys to activate the accessibility
+ * shortcut.
+ * @hide
+ */
+ public long getAccessibilityShortcutKeyTimeout() {
+ return A11Y_SHORTCUT_KEY_TIMEOUT;
+ }
+
+ /**
+ * @return The amount of time a user needs to press the relevant keys to activate the
+ * accessibility shortcut after it's confirmed that accessibility shortcut is used.
+ * @hide
+ */
+ public long getAccessibilityShortcutKeyTimeoutAfterConfirmation() {
+ return A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION;
+ }
+
+ /**
+ * The amount of friction applied to scrolls and flings.
+ *
+ * @return A scalar dimensionless value representing the coefficient of
+ * friction.
+ */
+ public static float getScrollFriction() {
+ return SCROLL_FRICTION;
+ }
+
+ /**
+ * @return the default duration in milliseconds for {@link ActionMode#hide(long)}.
+ */
+ public static long getDefaultActionModeHideDuration() {
+ return ACTION_MODE_HIDE_DURATION_DEFAULT;
+ }
+
+ /**
+ * The multiplication factor for inhibiting default gestures.
+ *
+ * If a MotionEvent has {@link android.view.MotionEvent#CLASSIFICATION_AMBIGUOUS_GESTURE} set,
+ * then certain actions, such as scrolling, will be inhibited. However, to account for the
+ * possibility of an incorrect classification, existing gesture thresholds (e.g. scrolling
+ * touch slop and the long-press timeout) should be scaled by this factor and remain in effect.
+ *
+ * @deprecated Use {@link #getScaledAmbiguousGestureMultiplier()}.
+ */
+ @Deprecated
+ @FloatRange(from = 1.0)
+ public static float getAmbiguousGestureMultiplier() {
+ return AMBIGUOUS_GESTURE_MULTIPLIER;
+ }
+
+ /**
+ * The multiplication factor for inhibiting default gestures.
+ *
+ * If a MotionEvent has {@link android.view.MotionEvent#CLASSIFICATION_AMBIGUOUS_GESTURE} set,
+ * then certain actions, such as scrolling, will be inhibited. However, to account for the
+ * possibility of an incorrect classification, existing gesture thresholds (e.g. scrolling
+ * touch slop and the long-press timeout) should be scaled by this factor and remain in effect.
+ */
+ @FloatRange(from = 1.0)
+ public float getScaledAmbiguousGestureMultiplier() {
+ return mAmbiguousGestureMultiplier;
+ }
+
+ /**
+ * Report if the device has a permanent menu key available to the user.
+ *
+ * <p>As of Android 3.0, devices may not have a permanent menu key available.
+ * Apps should use the action bar to present menu options to users.
+ * However, there are some apps where the action bar is inappropriate
+ * or undesirable. This method may be used to detect if a menu key is present.
+ * If not, applications should provide another on-screen affordance to access
+ * functionality.
+ *
+ * @return true if a permanent menu key is present, false otherwise.
+ */
+ public boolean hasPermanentMenuKey() {
+ return sHasPermanentMenuKey;
+ }
+
+ /**
+ * Check if shortcuts should be displayed in menus.
+ *
+ * @return {@code True} if shortcuts should be displayed in menus.
+ */
+ public boolean shouldShowMenuShortcutsWhenKeyboardPresent() {
+ return mShowMenuShortcutsWhenKeyboardPresent;
+ }
+
+ /**
+ * Retrieves the distance in pixels between touches that must be reached for a gesture to be
+ * interpreted as scaling.
+ *
+ * In general, scaling shouldn't start until this distance has been met or surpassed, and
+ * scaling should end when the distance in pixels between touches drops below this distance.
+ *
+ * @return The distance in pixels
+ * @throws IllegalStateException if this method is called on a ViewConfiguration that was
+ * instantiated using a constructor with no Context parameter.
+ */
+ public int getScaledMinimumScalingSpan() {
+ if (!mConstructedWithContext) {
+ throw new IllegalStateException("Min scaling span cannot be determined when this "
+ + "method is called on a ViewConfiguration that was instantiated using a "
+ + "constructor with no Context parameter");
+ }
+ return mMinScalingSpan;
+ }
+
+ /**
+ * @hide
+ * @return Whether or not marquee should use fading edges.
+ */
+ @UnsupportedAppUsage
+ public boolean isFadingMarqueeEnabled() {
+ return mFadingMarqueeEnabled;
+ }
+
+ /**
+ * @return the timeout value in milliseconds to adjust the selection span and actions for the
+ * selected text when TextClassifier has been initialized.
+ * @hide
+ */
+ public int getSmartSelectionInitializedTimeout() {
+ return mSmartSelectionInitializedTimeout;
+ }
+
+ /**
+ * @return the timeout value in milliseconds to adjust the selection span and actions for the
+ * selected text when TextClassifier has not been initialized.
+ * @hide
+ */
+ public int getSmartSelectionInitializingTimeout() {
+ return mSmartSelectionInitializingTimeout;
+ }
+
+ /**
+ * @return the duration in milliseconds before an end of a long press causes a tooltip to be
+ * hidden
+ * @hide
+ */
+ @TestApi
+ public static int getLongPressTooltipHideTimeout() {
+ return LONG_PRESS_TOOLTIP_HIDE_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds before a hover event causes a tooltip to be shown
+ * @hide
+ */
+ @TestApi
+ public static int getHoverTooltipShowTimeout() {
+ return HOVER_TOOLTIP_SHOW_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
+ * (default variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is not set).
+ * @hide
+ */
+ @TestApi
+ public static int getHoverTooltipHideTimeout() {
+ return HOVER_TOOLTIP_HIDE_TIMEOUT;
+ }
+
+ /**
+ * @return the duration in milliseconds before mouse inactivity causes a tooltip to be hidden
+ * (shorter variant to be used when {@link View#SYSTEM_UI_FLAG_LOW_PROFILE} is set).
+ * @hide
+ */
+ @TestApi
+ public static int getHoverTooltipHideShortTimeout() {
+ return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
+ }
+}
diff --git a/android/view/ViewConfiguration_Accessor.java b/android/view/ViewConfiguration_Accessor.java
new file mode 100644
index 0000000..c3533e0
--- /dev/null
+++ b/android/view/ViewConfiguration_Accessor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * Class allowing access to package-protected methods/fields.
+ */
+public class ViewConfiguration_Accessor {
+
+ public static void clearConfigurations() {
+ // clear the stored ViewConfiguration since the map is per density and not per context.
+ ViewConfiguration.sConfigurations.clear();
+ }
+
+}
diff --git a/android/view/ViewDebug.java b/android/view/ViewDebug.java
new file mode 100644
index 0000000..73294b3
--- /dev/null
+++ b/android/view/ViewDebug.java
@@ -0,0 +1,1963 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.HardwareRenderer;
+import android.graphics.Picture;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.os.Build;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+
+import libcore.util.HexEncoding;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Various debugging/tracing tools related to {@link View} and the view hierarchy.
+ */
+public class ViewDebug {
+ /**
+ * @deprecated This flag is now unused
+ */
+ @Deprecated
+ public static final boolean TRACE_HIERARCHY = false;
+
+ /**
+ * @deprecated This flag is now unused
+ */
+ @Deprecated
+ public static final boolean TRACE_RECYCLER = false;
+
+ /**
+ * Enables detailed logging of drag/drop operations.
+ * @hide
+ */
+ public static final boolean DEBUG_DRAG = false;
+
+ /**
+ * Enables detailed logging of task positioning operations.
+ * @hide
+ */
+ public static final boolean DEBUG_POSITIONING = false;
+
+ /**
+ * This annotation can be used to mark fields and methods to be dumped by
+ * the view server. Only non-void methods with no arguments can be annotated
+ * by this annotation.
+ */
+ @Target({ ElementType.FIELD, ElementType.METHOD })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface ExportedProperty {
+ /**
+ * When resolveId is true, and if the annotated field/method return value
+ * is an int, the value is converted to an Android's resource name.
+ *
+ * @return true if the property's value must be transformed into an Android
+ * resource name, false otherwise
+ */
+ boolean resolveId() default false;
+
+ /**
+ * A mapping can be defined to map int values to specific strings. For
+ * instance, View.getVisibility() returns 0, 4 or 8. However, these values
+ * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
+ * these human readable values:
+ *
+ * <pre>
+ * {@literal @}ViewDebug.ExportedProperty(mapping = {
+ * {@literal @}ViewDebug.IntToString(from = 0, to = "VISIBLE"),
+ * {@literal @}ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
+ * {@literal @}ViewDebug.IntToString(from = 8, to = "GONE")
+ * })
+ * public int getVisibility() { ...
+ * <pre>
+ *
+ * @return An array of int to String mappings
+ *
+ * @see android.view.ViewDebug.IntToString
+ */
+ IntToString[] mapping() default { };
+
+ /**
+ * A mapping can be defined to map array indices to specific strings.
+ * A mapping can be used to see human readable values for the indices
+ * of an array:
+ *
+ * <pre>
+ * {@literal @}ViewDebug.ExportedProperty(indexMapping = {
+ * {@literal @}ViewDebug.IntToString(from = 0, to = "INVALID"),
+ * {@literal @}ViewDebug.IntToString(from = 1, to = "FIRST"),
+ * {@literal @}ViewDebug.IntToString(from = 2, to = "SECOND")
+ * })
+ * private int[] mElements;
+ * <pre>
+ *
+ * @return An array of int to String mappings
+ *
+ * @see android.view.ViewDebug.IntToString
+ * @see #mapping()
+ */
+ IntToString[] indexMapping() default { };
+
+ /**
+ * A flags mapping can be defined to map flags encoded in an integer to
+ * specific strings. A mapping can be used to see human readable values
+ * for the flags of an integer:
+ *
+ * <pre>
+ * {@literal @}ViewDebug.ExportedProperty(flagMapping = {
+ * {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED,
+ * name = "ENABLED"),
+ * {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED,
+ * name = "DISABLED"),
+ * })
+ * private int mFlags;
+ * <pre>
+ *
+ * A specified String is output when the following is true:
+ *
+ * @return An array of int to String mappings
+ */
+ FlagToString[] flagMapping() default { };
+
+ /**
+ * When deep export is turned on, this property is not dumped. Instead, the
+ * properties contained in this property are dumped. Each child property
+ * is prefixed with the name of this property.
+ *
+ * @return true if the properties of this property should be dumped
+ *
+ * @see #prefix()
+ */
+ boolean deepExport() default false;
+
+ /**
+ * The prefix to use on child properties when deep export is enabled
+ *
+ * @return a prefix as a String
+ *
+ * @see #deepExport()
+ */
+ String prefix() default "";
+
+ /**
+ * Specifies the category the property falls into, such as measurement,
+ * layout, drawing, etc.
+ *
+ * @return the category as String
+ */
+ String category() default "";
+
+ /**
+ * Indicates whether or not to format an {@code int} or {@code byte} value as a hex string.
+ *
+ * @return true if the supported values should be formatted as a hex string.
+ */
+ boolean formatToHexString() default false;
+
+ /**
+ * Indicates whether or not the key to value mappings are held in adjacent indices.
+ *
+ * Note: Applies only to fields and methods that return String[].
+ *
+ * @return true if the key to value mappings are held in adjacent indices.
+ */
+ boolean hasAdjacentMapping() default false;
+ }
+
+ /**
+ * Defines a mapping from an int value to a String. Such a mapping can be used
+ * in an @ExportedProperty to provide more meaningful values to the end user.
+ *
+ * @see android.view.ViewDebug.ExportedProperty
+ */
+ @Target({ ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface IntToString {
+ /**
+ * The original int value to map to a String.
+ *
+ * @return An arbitrary int value.
+ */
+ int from();
+
+ /**
+ * The String to use in place of the original int value.
+ *
+ * @return An arbitrary non-null String.
+ */
+ String to();
+ }
+
+ /**
+ * Defines a mapping from a flag to a String. Such a mapping can be used
+ * in an @ExportedProperty to provide more meaningful values to the end user.
+ *
+ * @see android.view.ViewDebug.ExportedProperty
+ */
+ @Target({ ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface FlagToString {
+ /**
+ * The mask to apply to the original value.
+ *
+ * @return An arbitrary int value.
+ */
+ int mask();
+
+ /**
+ * The value to compare to the result of:
+ * <code>original value & {@link #mask()}</code>.
+ *
+ * @return An arbitrary value.
+ */
+ int equals();
+
+ /**
+ * The String to use in place of the original int value.
+ *
+ * @return An arbitrary non-null String.
+ */
+ String name();
+
+ /**
+ * Indicates whether to output the flag when the test is true,
+ * or false. Defaults to true.
+ */
+ boolean outputIf() default true;
+ }
+
+ /**
+ * This annotation can be used to mark fields and methods to be dumped when
+ * the view is captured. Methods with this annotation must have no arguments
+ * and must return a valid type of data.
+ */
+ @Target({ ElementType.FIELD, ElementType.METHOD })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface CapturedViewProperty {
+ /**
+ * When retrieveReturn is true, we need to retrieve second level methods
+ * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
+ * we will set retrieveReturn = true on the annotation of
+ * myView.getFirstLevelMethod()
+ * @return true if we need the second level methods
+ */
+ boolean retrieveReturn() default false;
+ }
+
+ /**
+ * Allows a View to inject custom children into HierarchyViewer. For example,
+ * WebView uses this to add its internal layer tree as a child to itself
+ * @hide
+ */
+ public interface HierarchyHandler {
+ /**
+ * Dumps custom children to hierarchy viewer.
+ * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int)
+ * for the format
+ *
+ * An empty implementation should simply do nothing
+ *
+ * @param out The output writer
+ * @param level The indentation level
+ */
+ public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
+
+ /**
+ * Returns a View to enable grabbing screenshots from custom children
+ * returned in dumpViewHierarchyWithProperties.
+ *
+ * @param className The className of the view to find
+ * @param hashCode The hashCode of the view to find
+ * @return the View to capture from, or null if not found
+ */
+ public View findHierarchyView(String className, int hashCode);
+ }
+
+ private abstract static class PropertyInfo<T extends Annotation,
+ R extends AccessibleObject & Member> {
+
+ public final R member;
+ public final T property;
+ public final String name;
+ public final Class<?> returnType;
+
+ public String entrySuffix = "";
+ public String valueSuffix = "";
+
+ PropertyInfo(Class<T> property, R member, Class<?> returnType) {
+ this.member = member;
+ this.name = member.getName();
+ this.property = member.getAnnotation(property);
+ this.returnType = returnType;
+ }
+
+ public abstract Object invoke(Object target) throws Exception;
+
+ static <T extends Annotation> PropertyInfo<T, ?> forMethod(Method method,
+ Class<T> property) {
+ // Ensure the method return and parameter types can be resolved.
+ try {
+ if ((method.getReturnType() == Void.class)
+ || (method.getParameterTypes().length != 0)) {
+ return null;
+ }
+ } catch (NoClassDefFoundError e) {
+ return null;
+ }
+ if (!method.isAnnotationPresent(property)) {
+ return null;
+ }
+ method.setAccessible(true);
+
+ PropertyInfo info = new MethodPI(method, property);
+ info.entrySuffix = "()";
+ info.valueSuffix = ";";
+ return info;
+ }
+
+ static <T extends Annotation> PropertyInfo<T, ?> forField(Field field, Class<T> property) {
+ if (!field.isAnnotationPresent(property)) {
+ return null;
+ }
+ field.setAccessible(true);
+ return new FieldPI<>(field, property);
+ }
+ }
+
+ private static class MethodPI<T extends Annotation> extends PropertyInfo<T, Method> {
+
+ MethodPI(Method method, Class<T> property) {
+ super(property, method, method.getReturnType());
+ }
+
+ @Override
+ public Object invoke(Object target) throws Exception {
+ return member.invoke(target);
+ }
+ }
+
+ private static class FieldPI<T extends Annotation> extends PropertyInfo<T, Field> {
+
+ FieldPI(Field field, Class<T> property) {
+ super(property, field, field.getType());
+ }
+
+ @Override
+ public Object invoke(Object target) throws Exception {
+ return member.get(target);
+ }
+ }
+
+ // Maximum delay in ms after which we stop trying to capture a View's drawing
+ private static final int CAPTURE_TIMEOUT = 6000;
+
+ private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
+ private static final String REMOTE_COMMAND_DUMP = "DUMP";
+ private static final String REMOTE_COMMAND_DUMP_THEME = "DUMP_THEME";
+ /**
+ * Similar to REMOTE_COMMAND_DUMP but uses ViewHierarchyEncoder instead of flat text
+ * @hide
+ */
+ public static final String REMOTE_COMMAND_DUMP_ENCODED = "DUMP_ENCODED";
+ private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
+ private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
+ private static final String REMOTE_PROFILE = "PROFILE";
+ private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
+ private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
+
+ private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties;
+ private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]>
+ sCapturedViewProperties;
+
+ /**
+ * @deprecated This enum is now unused
+ */
+ @Deprecated
+ public enum HierarchyTraceType {
+ INVALIDATE,
+ INVALIDATE_CHILD,
+ INVALIDATE_CHILD_IN_PARENT,
+ REQUEST_LAYOUT,
+ ON_LAYOUT,
+ ON_MEASURE,
+ DRAW,
+ BUILD_CACHE
+ }
+
+ /**
+ * @deprecated This enum is now unused
+ */
+ @Deprecated
+ public enum RecyclerTraceType {
+ NEW_VIEW,
+ BIND_VIEW,
+ RECYCLE_FROM_ACTIVE_HEAP,
+ RECYCLE_FROM_SCRAP_HEAP,
+ MOVE_TO_SCRAP_HEAP,
+ MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
+ }
+
+ /**
+ * Returns the number of instanciated Views.
+ *
+ * @return The number of Views instanciated in the current process.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static long getViewInstanceCount() {
+ return Debug.countInstancesOfClass(View.class);
+ }
+
+ /**
+ * Returns the number of instanciated ViewAncestors.
+ *
+ * @return The number of ViewAncestors instanciated in the current process.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static long getViewRootImplCount() {
+ return Debug.countInstancesOfClass(ViewRootImpl.class);
+ }
+
+ /**
+ * @deprecated This method is now unused and invoking it is a no-op
+ */
+ @Deprecated
+ @SuppressWarnings({ "UnusedParameters", "deprecation" })
+ public static void trace(View view, RecyclerTraceType type, int... parameters) {
+ }
+
+ /**
+ * @deprecated This method is now unused and invoking it is a no-op
+ */
+ @Deprecated
+ @SuppressWarnings("UnusedParameters")
+ public static void startRecyclerTracing(String prefix, View view) {
+ }
+
+ /**
+ * @deprecated This method is now unused and invoking it is a no-op
+ */
+ @Deprecated
+ @SuppressWarnings("UnusedParameters")
+ public static void stopRecyclerTracing() {
+ }
+
+ /**
+ * @deprecated This method is now unused and invoking it is a no-op
+ */
+ @Deprecated
+ @SuppressWarnings({ "UnusedParameters", "deprecation" })
+ public static void trace(View view, HierarchyTraceType type) {
+ }
+
+ /**
+ * @deprecated This method is now unused and invoking it is a no-op
+ */
+ @Deprecated
+ @SuppressWarnings("UnusedParameters")
+ public static void startHierarchyTracing(String prefix, View view) {
+ }
+
+ /**
+ * @deprecated This method is now unused and invoking it is a no-op
+ */
+ @Deprecated
+ public static void stopHierarchyTracing() {
+ }
+
+ @UnsupportedAppUsage
+ static void dispatchCommand(View view, String command, String parameters,
+ OutputStream clientStream) throws IOException {
+ // Just being cautious...
+ view = view.getRootView();
+
+ if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
+ dump(view, false, true, clientStream);
+ } else if (REMOTE_COMMAND_DUMP_THEME.equalsIgnoreCase(command)) {
+ dumpTheme(view, clientStream);
+ } else if (REMOTE_COMMAND_DUMP_ENCODED.equalsIgnoreCase(command)) {
+ dumpEncoded(view, clientStream);
+ } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
+ captureLayers(view, new DataOutputStream(clientStream));
+ } else {
+ final String[] params = parameters.split(" ");
+ if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
+ capture(view, clientStream, params[0]);
+ } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
+ outputDisplayList(view, params[0]);
+ } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
+ invalidate(view, params[0]);
+ } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
+ requestLayout(view, params[0]);
+ } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
+ profile(view, clientStream, params[0]);
+ }
+ }
+ }
+
+ /** @hide */
+ public static View findView(View root, String parameter) {
+ // Look by type/hashcode
+ if (parameter.indexOf('@') != -1) {
+ final String[] ids = parameter.split("@");
+ final String className = ids[0];
+ final int hashCode = (int) Long.parseLong(ids[1], 16);
+
+ View view = root.getRootView();
+ if (view instanceof ViewGroup) {
+ return findView((ViewGroup) view, className, hashCode);
+ }
+ } else {
+ // Look by id
+ final int id = root.getResources().getIdentifier(parameter, null, null);
+ return root.getRootView().findViewById(id);
+ }
+
+ return null;
+ }
+
+ private static void invalidate(View root, String parameter) {
+ final View view = findView(root, parameter);
+ if (view != null) {
+ view.postInvalidate();
+ }
+ }
+
+ private static void requestLayout(View root, String parameter) {
+ final View view = findView(root, parameter);
+ if (view != null) {
+ root.post(new Runnable() {
+ public void run() {
+ view.requestLayout();
+ }
+ });
+ }
+ }
+
+ private static void profile(View root, OutputStream clientStream, String parameter)
+ throws IOException {
+
+ final View view = findView(root, parameter);
+ BufferedWriter out = null;
+ try {
+ out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
+
+ if (view != null) {
+ profileViewAndChildren(view, out);
+ } else {
+ out.write("-1 -1 -1");
+ out.newLine();
+ }
+ out.write("DONE.");
+ out.newLine();
+ } catch (Exception e) {
+ android.util.Log.w("View", "Problem profiling the view:", e);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ /** @hide */
+ public static void profileViewAndChildren(final View view, BufferedWriter out)
+ throws IOException {
+ RenderNode node = RenderNode.create("ViewDebug", null);
+ profileViewAndChildren(view, node, out, true);
+ }
+
+ private static void profileViewAndChildren(View view, RenderNode node, BufferedWriter out,
+ boolean root) throws IOException {
+ long durationMeasure =
+ (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0)
+ ? profileViewMeasure(view) : 0;
+ long durationLayout =
+ (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0)
+ ? profileViewLayout(view) : 0;
+ long durationDraw =
+ (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0)
+ ? profileViewDraw(view, node) : 0;
+
+ out.write(String.valueOf(durationMeasure));
+ out.write(' ');
+ out.write(String.valueOf(durationLayout));
+ out.write(' ');
+ out.write(String.valueOf(durationDraw));
+ out.newLine();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ profileViewAndChildren(group.getChildAt(i), node, out, false);
+ }
+ }
+ }
+
+ private static long profileViewMeasure(final View view) {
+ return profileViewOperation(view, new ViewOperation() {
+ @Override
+ public void pre() {
+ forceLayout(view);
+ }
+
+ private void forceLayout(View view) {
+ view.forceLayout();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ forceLayout(group.getChildAt(i));
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
+ }
+ });
+ }
+
+ private static long profileViewLayout(View view) {
+ return profileViewOperation(view,
+ () -> view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom));
+ }
+
+ private static long profileViewDraw(View view, RenderNode node) {
+ DisplayMetrics dm = view.getResources().getDisplayMetrics();
+ if (dm == null) {
+ return 0;
+ }
+
+ if (view.isHardwareAccelerated()) {
+ RecordingCanvas canvas = node.beginRecording(dm.widthPixels, dm.heightPixels);
+ try {
+ return profileViewOperation(view, () -> view.draw(canvas));
+ } finally {
+ node.endRecording();
+ }
+ } else {
+ Bitmap bitmap = Bitmap.createBitmap(
+ dm, dm.widthPixels, dm.heightPixels, Bitmap.Config.RGB_565);
+ Canvas canvas = new Canvas(bitmap);
+ try {
+ return profileViewOperation(view, () -> view.draw(canvas));
+ } finally {
+ canvas.setBitmap(null);
+ bitmap.recycle();
+ }
+ }
+ }
+
+ interface ViewOperation {
+ default void pre() {}
+
+ void run();
+ }
+
+ private static long profileViewOperation(View view, final ViewOperation operation) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final long[] duration = new long[1];
+
+ view.post(() -> {
+ try {
+ operation.pre();
+ long start = Debug.threadCpuTimeNanos();
+ //noinspection unchecked
+ operation.run();
+ duration[0] = Debug.threadCpuTimeNanos() - start;
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ try {
+ if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ Log.w("View", "Could not complete the profiling of the view " + view);
+ return -1;
+ }
+ } catch (InterruptedException e) {
+ Log.w("View", "Could not complete the profiling of the view " + view);
+ Thread.currentThread().interrupt();
+ return -1;
+ }
+
+ return duration[0];
+ }
+
+ /** @hide */
+ public static void captureLayers(View root, final DataOutputStream clientStream)
+ throws IOException {
+
+ try {
+ Rect outRect = new Rect();
+ root.mAttachInfo.mViewRootImpl.getDisplayFrame(outRect);
+
+ clientStream.writeInt(outRect.width());
+ clientStream.writeInt(outRect.height());
+
+ captureViewLayer(root, clientStream, true);
+
+ clientStream.write(2);
+ } finally {
+ clientStream.close();
+ }
+ }
+
+ private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
+ throws IOException {
+
+ final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
+
+ if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) {
+ final int id = view.getId();
+ String name = view.getClass().getSimpleName();
+ if (id != View.NO_ID) {
+ name = resolveId(view.getContext(), id).toString();
+ }
+
+ clientStream.write(1);
+ clientStream.writeUTF(name);
+ clientStream.writeByte(localVisible ? 1 : 0);
+
+ int[] position = new int[2];
+ // XXX: Should happen on the UI thread
+ view.getLocationInWindow(position);
+
+ clientStream.writeInt(position[0]);
+ clientStream.writeInt(position[1]);
+ clientStream.flush();
+
+ Bitmap b = performViewCapture(view, true);
+ if (b != null) {
+ ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
+ b.getHeight() * 2);
+ b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
+ clientStream.writeInt(arrayOut.size());
+ arrayOut.writeTo(clientStream);
+ }
+ clientStream.flush();
+ }
+
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ int count = group.getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ captureViewLayer(group.getChildAt(i), clientStream, localVisible);
+ }
+ }
+
+ if (view.mOverlay != null) {
+ ViewGroup overlayContainer = view.getOverlay().mOverlayViewGroup;
+ captureViewLayer(overlayContainer, clientStream, localVisible);
+ }
+ }
+
+ private static void outputDisplayList(View root, String parameter) throws IOException {
+ final View view = findView(root, parameter);
+ view.getViewRootImpl().outputDisplayList(view);
+ }
+
+ /** @hide */
+ public static void outputDisplayList(View root, View target) {
+ root.getViewRootImpl().outputDisplayList(target);
+ }
+
+ private static class PictureCallbackHandler implements AutoCloseable,
+ HardwareRenderer.PictureCapturedCallback, Runnable {
+ private final HardwareRenderer mRenderer;
+ private final Function<Picture, Boolean> mCallback;
+ private final Executor mExecutor;
+ private final ReentrantLock mLock = new ReentrantLock(false);
+ private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
+ private boolean mStopListening;
+ private Thread mRenderThread;
+
+ private PictureCallbackHandler(HardwareRenderer renderer,
+ Function<Picture, Boolean> callback, Executor executor) {
+ mRenderer = renderer;
+ mCallback = callback;
+ mExecutor = executor;
+ mRenderer.setPictureCaptureCallback(this);
+ }
+
+ @Override
+ public void close() {
+ mLock.lock();
+ mStopListening = true;
+ mLock.unlock();
+ mRenderer.setPictureCaptureCallback(null);
+ }
+
+ @Override
+ public void onPictureCaptured(Picture picture) {
+ mLock.lock();
+ if (mStopListening) {
+ mLock.unlock();
+ mRenderer.setPictureCaptureCallback(null);
+ return;
+ }
+ if (mRenderThread == null) {
+ mRenderThread = Thread.currentThread();
+ }
+ Picture toDestroy = null;
+ if (mQueue.size() == 3) {
+ toDestroy = mQueue.removeLast();
+ }
+ mQueue.add(picture);
+ mLock.unlock();
+ if (toDestroy == null) {
+ mExecutor.execute(this);
+ } else {
+ toDestroy.close();
+ }
+ }
+
+ @Override
+ public void run() {
+ mLock.lock();
+ final Picture picture = mQueue.poll();
+ final boolean isStopped = mStopListening;
+ mLock.unlock();
+ if (Thread.currentThread() == mRenderThread) {
+ close();
+ throw new IllegalStateException(
+ "ViewDebug#startRenderingCommandsCapture must be given an executor that "
+ + "invokes asynchronously");
+ }
+ if (isStopped) {
+ picture.close();
+ return;
+ }
+ final boolean keepReceiving = mCallback.apply(picture);
+ if (!keepReceiving) {
+ close();
+ }
+ }
+ }
+
+ /**
+ * Begins capturing the entire rendering commands for the view tree referenced by the given
+ * view. The view passed may be any View in the tree as long as it is attached. That is,
+ * {@link View#isAttachedToWindow()} must be true.
+ *
+ * Every time a frame is rendered a Picture will be passed to the given callback via the given
+ * executor. As long as the callback returns 'true' it will continue to receive new frames.
+ * The system will only invoke the callback at a rate that the callback is able to keep up with.
+ * That is, if it takes 48ms for the callback to complete and there is a 60fps animation running
+ * then the callback will only receive 33% of the frames produced.
+ *
+ * This method must be called on the same thread as the View tree.
+ *
+ * @param tree The View tree to capture the rendering commands.
+ * @param callback The callback to invoke on every frame produced. Should return true to
+ * continue receiving new frames, false to stop capturing.
+ * @param executor The executor to invoke the callback on. Recommend using a background thread
+ * to avoid stalling the UI thread. Must be an asynchronous invoke or an
+ * exception will be thrown.
+ * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
+ * that the callback may continue to receive another frame or two depending on thread timings.
+ * Returns null if the capture stream cannot be started, such as if there's no
+ * HardwareRenderer for the given view tree.
+ * @hide
+ * @deprecated use {@link #startRenderingCommandsCapture(View, Executor, Callable)} instead.
+ */
+ @TestApi
+ @Nullable
+ @Deprecated
+ public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
+ Function<Picture, Boolean> callback) {
+ final View.AttachInfo attachInfo = tree.mAttachInfo;
+ if (attachInfo == null) {
+ throw new IllegalArgumentException("Given view isn't attached");
+ }
+ if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("Called on the wrong thread."
+ + " Must be called on the thread that owns the given View");
+ }
+ final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
+ if (renderer != null) {
+ return new PictureCallbackHandler(renderer, callback, executor);
+ }
+ return null;
+ }
+
+ private static class StreamingPictureCallbackHandler implements AutoCloseable,
+ HardwareRenderer.PictureCapturedCallback, Runnable {
+ private final HardwareRenderer mRenderer;
+ private final Callable<OutputStream> mCallback;
+ private final Executor mExecutor;
+ private final ReentrantLock mLock = new ReentrantLock(false);
+ private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
+ private boolean mStopListening;
+ private Thread mRenderThread;
+
+ private StreamingPictureCallbackHandler(HardwareRenderer renderer,
+ Callable<OutputStream> callback, Executor executor) {
+ mRenderer = renderer;
+ mCallback = callback;
+ mExecutor = executor;
+ mRenderer.setPictureCaptureCallback(this);
+ }
+
+ @Override
+ public void close() {
+ mLock.lock();
+ mStopListening = true;
+ mLock.unlock();
+ mRenderer.setPictureCaptureCallback(null);
+ }
+
+ @Override
+ public void onPictureCaptured(Picture picture) {
+ mLock.lock();
+ if (mStopListening) {
+ mLock.unlock();
+ mRenderer.setPictureCaptureCallback(null);
+ return;
+ }
+ if (mRenderThread == null) {
+ mRenderThread = Thread.currentThread();
+ }
+ boolean needsInvoke = true;
+ if (mQueue.size() == 3) {
+ mQueue.removeLast();
+ needsInvoke = false;
+ }
+ mQueue.add(picture);
+ mLock.unlock();
+
+ if (needsInvoke) {
+ mExecutor.execute(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ mLock.lock();
+ final Picture picture = mQueue.poll();
+ final boolean isStopped = mStopListening;
+ mLock.unlock();
+ if (Thread.currentThread() == mRenderThread) {
+ close();
+ throw new IllegalStateException(
+ "ViewDebug#startRenderingCommandsCapture must be given an executor that "
+ + "invokes asynchronously");
+ }
+ if (isStopped) {
+ return;
+ }
+ OutputStream stream = null;
+ try {
+ stream = mCallback.call();
+ } catch (Exception ex) {
+ Log.w("ViewDebug", "Aborting rendering commands capture "
+ + "because callback threw exception", ex);
+ }
+ if (stream != null) {
+ try {
+ picture.writeToStream(stream);
+ stream.flush();
+ } catch (IOException ex) {
+ Log.w("ViewDebug", "Aborting rendering commands capture "
+ + "due to IOException writing to output stream", ex);
+ }
+ } else {
+ close();
+ }
+ }
+ }
+
+ /**
+ * Begins capturing the entire rendering commands for the view tree referenced by the given
+ * view. The view passed may be any View in the tree as long as it is attached. That is,
+ * {@link View#isAttachedToWindow()} must be true.
+ *
+ * Every time a frame is rendered the callback will be invoked on the given executor to
+ * provide an OutputStream to serialize to. As long as the callback returns a valid
+ * OutputStream the capturing will continue. The system will only invoke the callback at a rate
+ * that the callback & OutputStream is able to keep up with. That is, if it takes 48ms for the
+ * callback & serialization to complete and there is a 60fps animation running
+ * then the callback will only receive 33% of the frames produced.
+ *
+ * This method must be called on the same thread as the View tree.
+ *
+ * @param tree The View tree to capture the rendering commands.
+ * @param callback The callback to invoke on every frame produced. Should return an
+ * OutputStream to write the data to. Return null to cancel capture. The
+ * same stream may be returned each time as the serialized data contains
+ * start & end markers. The callback will not be invoked while a previous
+ * serialization is being performed, so if a single continuous stream is being
+ * used it is valid for the callback to write its own metadata to that stream
+ * in response to callback invocation.
+ * @param executor The executor to invoke the callback on. Recommend using a background thread
+ * to avoid stalling the UI thread. Must be an asynchronous invoke or an
+ * exception will be thrown.
+ * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
+ * that the callback may continue to receive another frame or two depending on thread timings.
+ * Returns null if the capture stream cannot be started, such as if there's no
+ * HardwareRenderer for the given view tree.
+ * @hide
+ */
+ @TestApi
+ @Nullable
+ public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
+ Callable<OutputStream> callback) {
+ final View.AttachInfo attachInfo = tree.mAttachInfo;
+ if (attachInfo == null) {
+ throw new IllegalArgumentException("Given view isn't attached");
+ }
+ if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("Called on the wrong thread."
+ + " Must be called on the thread that owns the given View");
+ }
+ final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
+ if (renderer != null) {
+ return new StreamingPictureCallbackHandler(renderer, callback, executor);
+ }
+ return null;
+ }
+
+ private static void capture(View root, final OutputStream clientStream, String parameter)
+ throws IOException {
+
+ final View captureView = findView(root, parameter);
+ capture(root, clientStream, captureView);
+ }
+
+ /** @hide */
+ public static void capture(View root, final OutputStream clientStream, View captureView)
+ throws IOException {
+ Bitmap b = performViewCapture(captureView, false);
+
+ if (b == null) {
+ Log.w("View", "Failed to create capture bitmap!");
+ // Send an empty one so that it doesn't get stuck waiting for
+ // something.
+ b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
+ 1, 1, Bitmap.Config.ARGB_8888);
+ }
+
+ BufferedOutputStream out = null;
+ try {
+ out = new BufferedOutputStream(clientStream, 32 * 1024);
+ b.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ b.recycle();
+ }
+ }
+
+ private static Bitmap performViewCapture(final View captureView, final boolean skipChildren) {
+ if (captureView != null) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Bitmap[] cache = new Bitmap[1];
+
+ captureView.post(() -> {
+ try {
+ CanvasProvider provider = captureView.isHardwareAccelerated()
+ ? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
+ cache[0] = captureView.createSnapshot(provider, skipChildren);
+ } catch (OutOfMemoryError e) {
+ Log.w("View", "Out of memory for bitmap");
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ try {
+ latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
+ return cache[0];
+ } catch (InterruptedException e) {
+ Log.w("View", "Could not complete the capture of the view " + captureView);
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Dumps the view hierarchy starting from the given view.
+ * @deprecated See {@link #dumpv2(View, ByteArrayOutputStream)} below.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static void dump(View root, boolean skipChildren, boolean includeProperties,
+ OutputStream clientStream) throws IOException {
+ BufferedWriter out = null;
+ try {
+ out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
+ View view = root.getRootView();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ dumpViewHierarchy(group.getContext(), group, out, 0,
+ skipChildren, includeProperties);
+ }
+ out.write("DONE.");
+ out.newLine();
+ } catch (Exception e) {
+ android.util.Log.w("View", "Problem dumping the view:", e);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Dumps the view hierarchy starting from the given view.
+ * Rather than using reflection, it uses View's encode method to obtain all the properties.
+ * @hide
+ */
+ public static void dumpv2(@NonNull final View view, @NonNull ByteArrayOutputStream out)
+ throws InterruptedException {
+ final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(out);
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
+ encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
+ view.encode(encoder);
+ latch.countDown();
+ }
+ });
+
+ latch.await(2, TimeUnit.SECONDS);
+ encoder.endStream();
+ }
+
+ private static void dumpEncoded(@NonNull final View view, @NonNull OutputStream out)
+ throws IOException {
+ ByteArrayOutputStream baOut = new ByteArrayOutputStream();
+
+ final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(baOut);
+ encoder.setUserPropertiesEnabled(false);
+ encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
+ encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
+ view.encode(encoder);
+ encoder.endStream();
+ out.write(baOut.toByteArray());
+ }
+
+ /**
+ * Dumps the theme attributes from the given View.
+ * @hide
+ */
+ public static void dumpTheme(View view, OutputStream clientStream) throws IOException {
+ BufferedWriter out = null;
+ try {
+ out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
+ String[] attributes = getStyleAttributesDump(view.getContext().getResources(),
+ view.getContext().getTheme());
+ if (attributes != null) {
+ for (int i = 0; i < attributes.length; i += 2) {
+ if (attributes[i] != null) {
+ out.write(attributes[i] + "\n");
+ out.write(attributes[i + 1] + "\n");
+ }
+ }
+ }
+ out.write("DONE.");
+ out.newLine();
+ } catch (Exception e) {
+ android.util.Log.w("View", "Problem dumping View Theme:", e);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Gets the style attributes from the {@link Resources.Theme}. For debugging only.
+ *
+ * @param resources Resources to resolve attributes from.
+ * @param theme Theme to dump.
+ * @return a String array containing pairs of adjacent Theme attribute data: name followed by
+ * its value.
+ *
+ * @hide
+ */
+ private static String[] getStyleAttributesDump(Resources resources, Resources.Theme theme) {
+ TypedValue outValue = new TypedValue();
+ String nullString = "null";
+ int i = 0;
+ int[] attributes = theme.getAllAttributes();
+ String[] data = new String[attributes.length * 2];
+ for (int attributeId : attributes) {
+ try {
+ data[i] = resources.getResourceName(attributeId);
+ data[i + 1] = theme.resolveAttribute(attributeId, outValue, true) ?
+ outValue.coerceToString().toString() : nullString;
+ i += 2;
+
+ // attempt to replace reference data with its name
+ if (outValue.type == TypedValue.TYPE_REFERENCE) {
+ data[i - 1] = resources.getResourceName(outValue.resourceId);
+ }
+ } catch (Resources.NotFoundException e) {
+ // ignore resources we can't resolve
+ }
+ }
+ return data;
+ }
+
+ private static View findView(ViewGroup group, String className, int hashCode) {
+ if (isRequestedView(group, className, hashCode)) {
+ return group;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ final View found = findView((ViewGroup) view, className, hashCode);
+ if (found != null) {
+ return found;
+ }
+ } else if (isRequestedView(view, className, hashCode)) {
+ return view;
+ }
+ if (view.mOverlay != null) {
+ final View found = findView((ViewGroup) view.mOverlay.mOverlayViewGroup,
+ className, hashCode);
+ if (found != null) {
+ return found;
+ }
+ }
+ if (view instanceof HierarchyHandler) {
+ final View found = ((HierarchyHandler)view)
+ .findHierarchyView(className, hashCode);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean isRequestedView(View view, String className, int hashCode) {
+ if (view.hashCode() == hashCode) {
+ String viewClassName = view.getClass().getName();
+ if (className.equals("ViewOverlay")) {
+ return viewClassName.equals("android.view.ViewOverlay$OverlayViewGroup");
+ } else {
+ return className.equals(viewClassName);
+ }
+ }
+ return false;
+ }
+
+ private static void dumpViewHierarchy(Context context, ViewGroup group,
+ BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
+ cacheExportedProperties(group.getClass());
+ if (!skipChildren) {
+ cacheExportedPropertiesForChildren(group);
+ }
+ // Try to use the handler provided by the view
+ Handler handler = group.getHandler();
+ // Fall back on using the main thread
+ if (handler == null) {
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ if (handler.getLooper() == Looper.myLooper()) {
+ dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
+ includeProperties);
+ } else {
+ FutureTask task = new FutureTask(() ->
+ dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
+ includeProperties), null);
+ Message msg = Message.obtain(handler, task);
+ msg.setAsynchronous(true);
+ handler.sendMessage(msg);
+ while (true) {
+ try {
+ task.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
+ return;
+ } catch (InterruptedException e) {
+ // try again
+ } catch (ExecutionException | TimeoutException e) {
+ // Something unexpected happened.
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ private static void cacheExportedPropertiesForChildren(ViewGroup group) {
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ cacheExportedProperties(view.getClass());
+ if (view instanceof ViewGroup) {
+ cacheExportedPropertiesForChildren((ViewGroup) view);
+ }
+ }
+ }
+
+ private static void cacheExportedProperties(Class<?> klass) {
+ if (sExportProperties != null && sExportProperties.containsKey(klass)) {
+ return;
+ }
+ do {
+ for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
+ if (!info.returnType.isPrimitive() && info.property.deepExport()) {
+ cacheExportedProperties(info.returnType);
+ }
+ }
+ klass = klass.getSuperclass();
+ } while (klass != Object.class);
+ }
+
+
+ private static void dumpViewHierarchyOnUIThread(Context context, ViewGroup group,
+ BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
+ if (!dumpView(context, group, out, level, includeProperties)) {
+ return;
+ }
+
+ if (skipChildren) {
+ return;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ dumpViewHierarchyOnUIThread(context, (ViewGroup) view, out, level + 1,
+ skipChildren, includeProperties);
+ } else {
+ dumpView(context, view, out, level + 1, includeProperties);
+ }
+ if (view.mOverlay != null) {
+ ViewOverlay overlay = view.getOverlay();
+ ViewGroup overlayContainer = overlay.mOverlayViewGroup;
+ dumpViewHierarchyOnUIThread(context, overlayContainer, out, level + 2,
+ skipChildren, includeProperties);
+ }
+ }
+ if (group instanceof HierarchyHandler) {
+ ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1);
+ }
+ }
+
+ private static boolean dumpView(Context context, View view,
+ BufferedWriter out, int level, boolean includeProperties) {
+
+ try {
+ for (int i = 0; i < level; i++) {
+ out.write(' ');
+ }
+ String className = view.getClass().getName();
+ if (className.equals("android.view.ViewOverlay$OverlayViewGroup")) {
+ className = "ViewOverlay";
+ }
+ out.write(className);
+ out.write('@');
+ out.write(Integer.toHexString(view.hashCode()));
+ out.write(' ');
+ if (includeProperties) {
+ dumpViewProperties(context, view, out);
+ }
+ out.newLine();
+ } catch (IOException e) {
+ Log.w("View", "Error while dumping hierarchy tree");
+ return false;
+ }
+ return true;
+ }
+
+ private static <T extends Annotation> PropertyInfo<T, ?>[] convertToPropertyInfos(
+ Method[] methods, Field[] fields, Class<T> property) {
+ return Stream.of(Arrays.stream(methods).map(m -> PropertyInfo.forMethod(m, property)),
+ Arrays.stream(fields).map(f -> PropertyInfo.forField(f, property)))
+ .flatMap(Function.identity())
+ .filter(i -> i != null)
+ .toArray(PropertyInfo[]::new);
+ }
+
+ private static PropertyInfo<ExportedProperty, ?>[] getExportedProperties(Class<?> klass) {
+ if (sExportProperties == null) {
+ sExportProperties = new HashMap<>();
+ }
+ final HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> map = sExportProperties;
+ PropertyInfo<ExportedProperty, ?>[] properties = sExportProperties.get(klass);
+
+ if (properties == null) {
+ properties = convertToPropertyInfos(klass.getDeclaredMethods(),
+ klass.getDeclaredFields(), ExportedProperty.class);
+ map.put(klass, properties);
+ }
+ return properties;
+ }
+
+ private static void dumpViewProperties(Context context, Object view,
+ BufferedWriter out) throws IOException {
+
+ dumpViewProperties(context, view, out, "");
+ }
+
+ private static void dumpViewProperties(Context context, Object view,
+ BufferedWriter out, String prefix) throws IOException {
+
+ if (view == null) {
+ out.write(prefix + "=4,null ");
+ return;
+ }
+
+ Class<?> klass = view.getClass();
+ do {
+ writeExportedProperties(context, view, out, klass, prefix);
+ klass = klass.getSuperclass();
+ } while (klass != Object.class);
+ }
+
+ private static String formatIntToHexString(int value) {
+ return "0x" + Integer.toHexString(value).toUpperCase();
+ }
+
+ private static void writeExportedProperties(Context context, Object view, BufferedWriter out,
+ Class<?> klass, String prefix) throws IOException {
+ for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
+ //noinspection EmptyCatchBlock
+ Object value;
+ try {
+ value = info.invoke(view);
+ } catch (Exception e) {
+ // ignore
+ continue;
+ }
+
+ String categoryPrefix =
+ info.property.category().length() != 0 ? info.property.category() + ":" : "";
+
+ if (info.returnType == int.class || info.returnType == byte.class) {
+ if (info.property.resolveId() && context != null) {
+ final int id = (Integer) value;
+ value = resolveId(context, id);
+
+ } else if (info.property.formatToHexString()) {
+ if (info.returnType == int.class) {
+ value = formatIntToHexString((Integer) value);
+ } else if (info.returnType == byte.class) {
+ value = "0x"
+ + HexEncoding.encodeToString((Byte) value, true);
+ }
+ } else {
+ final ViewDebug.FlagToString[] flagsMapping = info.property.flagMapping();
+ if (flagsMapping.length > 0) {
+ final int intValue = (Integer) value;
+ final String valuePrefix =
+ categoryPrefix + prefix + info.name + '_';
+ exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
+ }
+
+ final ViewDebug.IntToString[] mapping = info.property.mapping();
+ if (mapping.length > 0) {
+ final int intValue = (Integer) value;
+ boolean mapped = false;
+ int mappingCount = mapping.length;
+ for (int j = 0; j < mappingCount; j++) {
+ final ViewDebug.IntToString mapper = mapping[j];
+ if (mapper.from() == intValue) {
+ value = mapper.to();
+ mapped = true;
+ break;
+ }
+ }
+
+ if (!mapped) {
+ value = intValue;
+ }
+ }
+ }
+ } else if (info.returnType == int[].class) {
+ final int[] array = (int[]) value;
+ final String valuePrefix = categoryPrefix + prefix + info.name + '_';
+ exportUnrolledArray(context, out, info.property, array, valuePrefix,
+ info.entrySuffix);
+
+ continue;
+ } else if (info.returnType == String[].class) {
+ final String[] array = (String[]) value;
+ if (info.property.hasAdjacentMapping() && array != null) {
+ for (int j = 0; j < array.length; j += 2) {
+ if (array[j] != null) {
+ writeEntry(out, categoryPrefix + prefix, array[j],
+ info.entrySuffix, array[j + 1] == null ? "null" : array[j + 1]);
+ }
+ }
+ }
+
+ continue;
+ } else if (!info.returnType.isPrimitive()) {
+ if (info.property.deepExport()) {
+ dumpViewProperties(context, value, out, prefix + info.property.prefix());
+ continue;
+ }
+ }
+
+ writeEntry(out, categoryPrefix + prefix, info.name, info.entrySuffix, value);
+ }
+ }
+
+ private static void writeEntry(BufferedWriter out, String prefix, String name,
+ String suffix, Object value) throws IOException {
+
+ out.write(prefix);
+ out.write(name);
+ out.write(suffix);
+ out.write("=");
+ writeValue(out, value);
+ out.write(' ');
+ }
+
+ private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
+ int intValue, String prefix) throws IOException {
+
+ 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 = intValue & flagMapping.mask();
+ final boolean test = maskResult == flagMapping.equals();
+ if ((test && ifTrue) || (!test && !ifTrue)) {
+ final String name = flagMapping.name();
+ final String value = formatIntToHexString(maskResult);
+ writeEntry(out, prefix, name, "", value);
+ }
+ }
+ }
+
+ /**
+ * 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 {
+
+ final IntToString[] indexMapping = property.indexMapping();
+ final boolean hasIndexMapping = indexMapping.length > 0;
+
+ final IntToString[] mapping = property.mapping();
+ final boolean hasMapping = mapping.length > 0;
+
+ final boolean resolveId = property.resolveId() && context != null;
+ final int valuesCount = array.length;
+
+ for (int j = 0; j < valuesCount; j++) {
+ String name;
+ String value = null;
+
+ final int intValue = array[j];
+
+ name = String.valueOf(j);
+ if (hasIndexMapping) {
+ int mappingCount = indexMapping.length;
+ for (int k = 0; k < mappingCount; k++) {
+ final IntToString mapped = indexMapping[k];
+ if (mapped.from() == j) {
+ name = mapped.to();
+ break;
+ }
+ }
+ }
+
+ if (hasMapping) {
+ int mappingCount = mapping.length;
+ for (int k = 0; k < mappingCount; k++) {
+ final IntToString mapped = mapping[k];
+ if (mapped.from() == intValue) {
+ value = mapped.to();
+ break;
+ }
+ }
+ }
+
+ if (resolveId) {
+ if (value == null) value = (String) resolveId(context, intValue);
+ } else {
+ value = String.valueOf(intValue);
+ }
+
+ writeEntry(out, prefix, name, suffix, value);
+ }
+ }
+
+ static Object resolveId(Context context, int id) {
+ Object fieldValue;
+ final Resources resources = context.getResources();
+ if (id >= 0) {
+ try {
+ fieldValue = resources.getResourceTypeName(id) + '/' +
+ resources.getResourceEntryName(id);
+ } catch (Resources.NotFoundException e) {
+ fieldValue = "id/" + formatIntToHexString(id);
+ }
+ } else {
+ fieldValue = "NO_ID";
+ }
+ return fieldValue;
+ }
+
+ private static void writeValue(BufferedWriter out, Object value) throws IOException {
+ if (value != null) {
+ String output = "[EXCEPTION]";
+ try {
+ output = value.toString().replace("\n", "\\n");
+ } finally {
+ out.write(String.valueOf(output.length()));
+ out.write(",");
+ out.write(output);
+ }
+ } else {
+ out.write("4,null");
+ }
+ }
+
+ private static PropertyInfo<CapturedViewProperty, ?>[] getCapturedViewProperties(
+ Class<?> klass) {
+ if (sCapturedViewProperties == null) {
+ sCapturedViewProperties = new HashMap<>();
+ }
+ final HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]> map =
+ sCapturedViewProperties;
+
+ PropertyInfo<CapturedViewProperty, ?>[] infos = map.get(klass);
+ if (infos == null) {
+ infos = convertToPropertyInfos(klass.getMethods(), klass.getFields(),
+ CapturedViewProperty.class);
+ map.put(klass, infos);
+ }
+ return infos;
+ }
+
+ private static String exportCapturedViewProperties(Object obj, Class<?> klass, String prefix) {
+ if (obj == null) {
+ return "null";
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ for (PropertyInfo<CapturedViewProperty, ?> pi : getCapturedViewProperties(klass)) {
+ try {
+ Object methodValue = pi.invoke(obj);
+
+ if (pi.property.retrieveReturn()) {
+ //we are interested in the second level data only
+ sb.append(exportCapturedViewProperties(methodValue, pi.returnType,
+ pi.name + "#"));
+ } else {
+ sb.append(prefix).append(pi.name).append(pi.entrySuffix).append("=");
+
+ if (methodValue != null) {
+ final String value = methodValue.toString().replace("\n", "\\n");
+ sb.append(value);
+ } else {
+ sb.append("null");
+ }
+ sb.append(pi.valueSuffix).append(" ");
+ }
+ } catch (Exception e) {
+ //It is OK here, we simply ignore this property
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Dump view info for id based instrument test generation
+ * (and possibly further data analysis). The results are dumped
+ * to the log.
+ * @param tag for log
+ * @param view for dump
+ */
+ public static void dumpCapturedView(String tag, Object view) {
+ Class<?> klass = view.getClass();
+ StringBuilder sb = new StringBuilder(klass.getName() + ": ");
+ sb.append(exportCapturedViewProperties(view, klass, ""));
+ Log.d(tag, sb.toString());
+ }
+
+ /**
+ * Invoke a particular method on given view.
+ * The given method is always invoked on the UI thread. The caller thread will stall until the
+ * method invocation is complete. Returns an object equal to the result of the method
+ * invocation, null if the method is declared to return void
+ * @throws Exception if the method invocation caused any exception
+ * @hide
+ */
+ public static Object invokeViewMethod(final View view, final Method method,
+ final Object[] args) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<Object> result = new AtomicReference<Object>();
+ final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
+
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ result.set(method.invoke(view, args));
+ } catch (InvocationTargetException e) {
+ exception.set(e.getCause());
+ } catch (Exception e) {
+ exception.set(e);
+ }
+
+ latch.countDown();
+ }
+ });
+
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ if (exception.get() != null) {
+ throw new RuntimeException(exception.get());
+ }
+
+ return result.get();
+ }
+
+ /**
+ * @hide
+ */
+ public static void setLayoutParameter(final View view, final String param, final int value)
+ throws NoSuchFieldException, IllegalAccessException {
+ final ViewGroup.LayoutParams p = view.getLayoutParams();
+ final Field f = p.getClass().getField(param);
+ if (f.getType() != int.class) {
+ throw new RuntimeException("Only integer layout parameters can be set. Field "
+ + param + " is of type " + f.getType().getSimpleName());
+ }
+
+ f.set(p, Integer.valueOf(value));
+
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ view.setLayoutParams(p);
+ }
+ });
+ }
+
+ /**
+ * @hide
+ */
+ public static class SoftwareCanvasProvider implements CanvasProvider {
+
+ private Canvas mCanvas;
+ private Bitmap mBitmap;
+ private boolean mEnabledHwBitmapsInSwMode;
+
+ @Override
+ public Canvas getCanvas(View view, int width, int height) {
+ mBitmap = Bitmap.createBitmap(view.getResources().getDisplayMetrics(),
+ width, height, Bitmap.Config.ARGB_8888);
+ if (mBitmap == null) {
+ throw new OutOfMemoryError();
+ }
+ mBitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi);
+
+ if (view.mAttachInfo != null) {
+ mCanvas = view.mAttachInfo.mCanvas;
+ }
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+ mEnabledHwBitmapsInSwMode = mCanvas.isHwBitmapsInSwModeEnabled();
+ mCanvas.setBitmap(mBitmap);
+ return mCanvas;
+ }
+
+ @Override
+ public Bitmap createBitmap() {
+ mCanvas.setBitmap(null);
+ mCanvas.setHwBitmapsInSwModeEnabled(mEnabledHwBitmapsInSwMode);
+ return mBitmap;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class HardwareCanvasProvider implements CanvasProvider {
+ private Picture mPicture;
+
+ @Override
+ public Canvas getCanvas(View view, int width, int height) {
+ mPicture = new Picture();
+ return mPicture.beginRecording(width, height);
+ }
+
+ @Override
+ public Bitmap createBitmap() {
+ mPicture.endRecording();
+ return Bitmap.createBitmap(mPicture);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public interface CanvasProvider {
+
+ /**
+ * Returns a canvas which can be used to draw {@param view}
+ */
+ Canvas getCanvas(View view, int width, int height);
+
+ /**
+ * Creates a bitmap from previously returned canvas
+ * @return
+ */
+ Bitmap createBitmap();
+ }
+}
diff --git a/android/view/ViewFrameInfo.java b/android/view/ViewFrameInfo.java
new file mode 100644
index 0000000..36bf532
--- /dev/null
+++ b/android/view/ViewFrameInfo.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.FrameInfo;
+import android.os.IInputConstants;
+
+/**
+ * The timing information of events taking place in ViewRootImpl
+ * @hide
+ */
+public class ViewFrameInfo {
+ public long drawStart;
+
+
+ // Various flags set to provide extra metadata about the current frame. See flag definitions
+ // inside FrameInfo.
+ // @see android.graphics.FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED
+ public long flags;
+
+ private int mInputEventId;
+
+ /**
+ * Populate the missing fields using the data from ViewFrameInfo
+ * @param frameInfo : the structure FrameInfo object to populate
+ */
+ public void populateFrameInfo(FrameInfo frameInfo) {
+ frameInfo.frameInfo[FrameInfo.FLAGS] |= flags;
+ frameInfo.frameInfo[FrameInfo.DRAW_START] = drawStart;
+ frameInfo.frameInfo[FrameInfo.INPUT_EVENT_ID] = mInputEventId;
+ }
+
+ /**
+ * Reset this data. Should typically be invoked after calling "populateFrameInfo".
+ */
+ public void reset() {
+ drawStart = 0;
+ mInputEventId = IInputConstants.INVALID_INPUT_EVENT_ID;
+ flags = 0;
+ }
+
+ /**
+ * Record the current time, and store it in 'drawStart'
+ */
+ public void markDrawStart() {
+ drawStart = System.nanoTime();
+ }
+
+ /**
+ * Assign the value for input event id
+ * @param eventId the id of the input event
+ */
+ public void setInputEvent(int eventId) {
+ mInputEventId = eventId;
+ }
+}
diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java
new file mode 100644
index 0000000..679da31
--- /dev/null
+++ b/android/view/ViewGroup.java
@@ -0,0 +1,9299 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE;
+import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
+
+import android.animation.LayoutTransition;
+import android.annotation.CallSuper;
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.annotation.UiThread;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.Pools;
+import android.util.Pools.SynchronizedPool;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.WindowInsetsAnimation.Bounds;
+import android.view.WindowInsetsAnimation.Callback.DispatchMode;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.LayoutAnimationController;
+import android.view.animation.Transformation;
+import android.view.autofill.AutofillId;
+import android.view.autofill.Helper;
+import android.view.inspector.InspectableProperty;
+import android.view.inspector.InspectableProperty.EnumEntry;
+import android.view.translation.TranslationCapability;
+import android.view.translation.TranslationSpec.DataFormat;
+import android.view.translation.ViewTranslationRequest;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * <p>
+ * A <code>ViewGroup</code> is a special view that can contain other views
+ * (called children.) The view group is the base class for layouts and views
+ * containers. This class also defines the
+ * {@link android.view.ViewGroup.LayoutParams} class which serves as the base
+ * class for layouts parameters.
+ * </p>
+ *
+ * <p>
+ * Also see {@link LayoutParams} for layout attributes.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating user interface layouts, read the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html">XML Layouts</a> developer
+ * guide.</p></div>
+ *
+ * <p>Here is a complete implementation of a custom ViewGroup that implements
+ * a simple {@link android.widget.FrameLayout} along with the ability to stack
+ * children in left and right gutters.</p>
+ *
+ * {@sample development/samples/ApiDemos/src/com/example/android/apis/view/CustomLayout.java
+ * Complete}
+ *
+ * <p>If you are implementing XML layout attributes as shown in the example, this is the
+ * corresponding definition for them that would go in <code>res/values/attrs.xml</code>:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/values/attrs.xml CustomLayout}
+ *
+ * <p>Finally the layout manager can be used in an XML layout like so:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/layout/custom_layout.xml Complete}
+ *
+ * @attr ref android.R.styleable#ViewGroup_clipChildren
+ * @attr ref android.R.styleable#ViewGroup_clipToPadding
+ * @attr ref android.R.styleable#ViewGroup_layoutAnimation
+ * @attr ref android.R.styleable#ViewGroup_animationCache
+ * @attr ref android.R.styleable#ViewGroup_persistentDrawingCache
+ * @attr ref android.R.styleable#ViewGroup_alwaysDrawnWithCache
+ * @attr ref android.R.styleable#ViewGroup_addStatesFromChildren
+ * @attr ref android.R.styleable#ViewGroup_descendantFocusability
+ * @attr ref android.R.styleable#ViewGroup_animateLayoutChanges
+ * @attr ref android.R.styleable#ViewGroup_splitMotionEvents
+ * @attr ref android.R.styleable#ViewGroup_layoutMode
+ */
+@UiThread
+public abstract class ViewGroup extends View implements ViewParent, ViewManager {
+ private static final String TAG = "ViewGroup";
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private static final boolean DBG = false;
+
+ /**
+ * Views which have been hidden or removed which need to be animated on
+ * their way out.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ protected ArrayList<View> mDisappearingChildren;
+
+ /**
+ * Listener used to propagate events indicating when children are added
+ * and/or removed from a view group.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768704)
+ protected OnHierarchyChangeListener mOnHierarchyChangeListener;
+
+ // The view contained within this ViewGroup that has or contains focus.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private View mFocused;
+ // The view contained within this ViewGroup (excluding nested keyboard navigation clusters)
+ // that is or contains a default-focus view.
+ private View mDefaultFocus;
+ // The last child of this ViewGroup which held focus within the current cluster
+ View mFocusedInCluster;
+
+ /**
+ * A Transformation used when drawing children, to
+ * apply on the child being drawn.
+ */
+ private Transformation mChildTransformation;
+
+ /**
+ * Used to track the current invalidation region.
+ */
+ RectF mInvalidateRegion;
+
+ /**
+ * A Transformation used to calculate a correct
+ * invalidation area when the application is autoscaled.
+ */
+ Transformation mInvalidationTransformation;
+
+ // Current frontmost child that can accept drag and lies under the drag location.
+ // Used only to generate ENTER/EXIT events for pre-Nougat aps.
+ private View mCurrentDragChild;
+
+ // Metadata about the ongoing drag
+ private DragEvent mCurrentDragStartEvent;
+ private boolean mIsInterestedInDrag;
+ private HashSet<View> mChildrenInterestedInDrag;
+
+ // Used during drag dispatch
+ private PointF mLocalPoint;
+
+ // Lazily-created holder for point computations.
+ private float[] mTempPosition;
+
+ // Lazily-created holder for point computations.
+ private Point mTempPoint;
+
+ // Lazily created Rect for dispatch to children
+ private Rect mTempRect;
+
+ // Lazily created int[2] for dispatch to children
+ private int[] mTempLocation;
+
+ // Layout animation
+ private LayoutAnimationController mLayoutAnimationController;
+ private Animation.AnimationListener mAnimationListener;
+
+ // First touch target in the linked list of touch targets.
+ @UnsupportedAppUsage
+ private TouchTarget mFirstTouchTarget;
+
+ // For debugging only. You can see these in hierarchyviewer.
+ @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
+ @ViewDebug.ExportedProperty(category = "events")
+ private long mLastTouchDownTime;
+ @ViewDebug.ExportedProperty(category = "events")
+ private int mLastTouchDownIndex = -1;
+ @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
+ @ViewDebug.ExportedProperty(category = "events")
+ private float mLastTouchDownX;
+ @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
+ @ViewDebug.ExportedProperty(category = "events")
+ private float mLastTouchDownY;
+
+ // First hover target in the linked list of hover targets.
+ // The hover targets are children which have received ACTION_HOVER_ENTER.
+ // They might not have actually handled the hover event, but we will
+ // continue sending hover events to them as long as the pointer remains over
+ // their bounds and the view group does not intercept hover.
+ private HoverTarget mFirstHoverTarget;
+
+ // True if the view group itself received a hover event.
+ // It might not have actually handled the hover event.
+ private boolean mHoveredSelf;
+
+ // The child capable of showing a tooltip and currently under the pointer.
+ private View mTooltipHoverTarget;
+
+ // True if the view group is capable of showing a tooltip and the pointer is directly
+ // over the view group but not one of its child views.
+ private boolean mTooltipHoveredSelf;
+
+ /**
+ * Internal flags.
+ *
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(mask = FLAG_CLIP_CHILDREN, equals = FLAG_CLIP_CHILDREN,
+ name = "CLIP_CHILDREN"),
+ @ViewDebug.FlagToString(mask = FLAG_CLIP_TO_PADDING, equals = FLAG_CLIP_TO_PADDING,
+ name = "CLIP_TO_PADDING"),
+ @ViewDebug.FlagToString(mask = FLAG_PADDING_NOT_NULL, equals = FLAG_PADDING_NOT_NULL,
+ name = "PADDING_NOT_NULL")
+ }, formatToHexString = true)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769411)
+ protected int mGroupFlags;
+
+ /**
+ * Either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}.
+ */
+ private int mLayoutMode = LAYOUT_MODE_UNDEFINED;
+
+ /**
+ * NOTE: If you change the flags below make sure to reflect the changes
+ * the DisplayList class
+ */
+
+ // When set, ViewGroup invalidates only the child's rectangle
+ // Set by default
+ static final int FLAG_CLIP_CHILDREN = 0x1;
+
+ // When set, ViewGroup excludes the padding area from the invalidate rectangle
+ // Set by default
+ private static final int FLAG_CLIP_TO_PADDING = 0x2;
+
+ // When set, dispatchDraw() will invoke invalidate(); this is set by drawChild() when
+ // a child needs to be invalidated and FLAG_OPTIMIZE_INVALIDATE is set
+ static final int FLAG_INVALIDATE_REQUIRED = 0x4;
+
+ // When set, dispatchDraw() will run the layout animation and unset the flag
+ private static final int FLAG_RUN_ANIMATION = 0x8;
+
+ // When set, there is either no layout animation on the ViewGroup or the layout
+ // animation is over
+ // Set by default
+ static final int FLAG_ANIMATION_DONE = 0x10;
+
+ // If set, this ViewGroup has padding; if unset there is no padding and we don't need
+ // to clip it, even if FLAG_CLIP_TO_PADDING is set
+ private static final int FLAG_PADDING_NOT_NULL = 0x20;
+
+ /** @deprecated - functionality removed */
+ @Deprecated
+ private static final int FLAG_ANIMATION_CACHE = 0x40;
+
+ // When set, this ViewGroup converts calls to invalidate(Rect) to invalidate() during a
+ // layout animation; this avoid clobbering the hierarchy
+ // Automatically set when the layout animation starts, depending on the animation's
+ // characteristics
+ static final int FLAG_OPTIMIZE_INVALIDATE = 0x80;
+
+ // When set, the next call to drawChild() will clear mChildTransformation's matrix
+ static final int FLAG_CLEAR_TRANSFORMATION = 0x100;
+
+ // When set, this ViewGroup invokes mAnimationListener.onAnimationEnd() and removes
+ // the children's Bitmap caches if necessary
+ // This flag is set when the layout animation is over (after FLAG_ANIMATION_DONE is set)
+ private static final int FLAG_NOTIFY_ANIMATION_LISTENER = 0x200;
+
+ /**
+ * When set, the drawing method will call {@link #getChildDrawingOrder(int, int)}
+ * to get the index of the child to draw for that iteration.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769377)
+ protected static final int FLAG_USE_CHILD_DRAWING_ORDER = 0x400;
+
+ /**
+ * When set, this ViewGroup supports static transformations on children; this causes
+ * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} to be
+ * invoked when a child is drawn.
+ *
+ * Any subclass overriding
+ * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} should
+ * set this flags in {@link #mGroupFlags}.
+ *
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769647)
+ protected static final int FLAG_SUPPORT_STATIC_TRANSFORMATIONS = 0x800;
+
+ // UNUSED FLAG VALUE: 0x1000;
+
+ /**
+ * When set, this ViewGroup's drawable states also include those
+ * of its children.
+ */
+ private static final int FLAG_ADD_STATES_FROM_CHILDREN = 0x2000;
+
+ /** @deprecated functionality removed */
+ @Deprecated
+ private static final int FLAG_ALWAYS_DRAWN_WITH_CACHE = 0x4000;
+
+ /** @deprecated functionality removed */
+ @Deprecated
+ private static final int FLAG_CHILDREN_DRAWN_WITH_CACHE = 0x8000;
+
+ /**
+ * When set, this group will go through its list of children to notify them of
+ * any drawable state change.
+ */
+ private static final int FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE = 0x10000;
+
+ private static final int FLAG_MASK_FOCUSABILITY = 0x60000;
+
+ /**
+ * This view will get focus before any of its descendants.
+ */
+ public static final int FOCUS_BEFORE_DESCENDANTS = 0x20000;
+
+ /**
+ * This view will get focus only if none of its descendants want it.
+ */
+ public static final int FOCUS_AFTER_DESCENDANTS = 0x40000;
+
+ /**
+ * This view will block any of its descendants from getting focus, even
+ * if they are focusable.
+ */
+ public static final int FOCUS_BLOCK_DESCENDANTS = 0x60000;
+
+ /**
+ * Used to map between enum in attrubutes and flag values.
+ */
+ private static final int[] DESCENDANT_FOCUSABILITY_FLAGS =
+ {FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS,
+ FOCUS_BLOCK_DESCENDANTS};
+
+ /**
+ * When set, this ViewGroup should not intercept touch events.
+ * {@hide}
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123983692)
+ protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
+
+ /**
+ * When set, this ViewGroup will split MotionEvents to multiple child Views when appropriate.
+ */
+ private static final int FLAG_SPLIT_MOTION_EVENTS = 0x200000;
+
+ /**
+ * When set, this ViewGroup will not dispatch onAttachedToWindow calls
+ * to children when adding new views. This is used to prevent multiple
+ * onAttached calls when a ViewGroup adds children in its own onAttached method.
+ */
+ private static final int FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW = 0x400000;
+
+ /**
+ * When true, indicates that a layoutMode has been explicitly set, either with
+ * an explicit call to {@link #setLayoutMode(int)} in code or from an XML resource.
+ * This distinguishes the situation in which a layout mode was inherited from
+ * one of the ViewGroup's ancestors and cached locally.
+ */
+ private static final int FLAG_LAYOUT_MODE_WAS_EXPLICITLY_SET = 0x800000;
+
+ static final int FLAG_IS_TRANSITION_GROUP = 0x1000000;
+
+ static final int FLAG_IS_TRANSITION_GROUP_SET = 0x2000000;
+
+ /**
+ * When set, focus will not be permitted to enter this group if a touchscreen is present.
+ */
+ static final int FLAG_TOUCHSCREEN_BLOCKS_FOCUS = 0x4000000;
+
+ /**
+ * When true, indicates that a call to startActionModeForChild was made with the type parameter
+ * and should not be ignored. This helps in backwards compatibility with the existing method
+ * without a type.
+ *
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback)
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int)
+ */
+ private static final int FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED = 0x8000000;
+
+ /**
+ * When true, indicates that a call to startActionModeForChild was made without the type
+ * parameter. This helps in backwards compatibility with the existing method
+ * without a type.
+ *
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback)
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int)
+ */
+ private static final int FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED = 0x10000000;
+
+ /**
+ * When set, indicates that a call to showContextMenuForChild was made with explicit
+ * coordinates within the initiating child view.
+ */
+ private static final int FLAG_SHOW_CONTEXT_MENU_WITH_COORDS = 0x20000000;
+
+ /**
+ * Indicates which types of drawing caches are to be kept in memory.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ protected int mPersistentDrawingCache;
+
+ /**
+ * Used to indicate that no drawing cache should be kept in memory.
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int PERSISTENT_NO_CACHE = 0x0;
+
+ /**
+ * Used to indicate that the animation drawing cache should be kept in memory.
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int PERSISTENT_ANIMATION_CACHE = 0x1;
+
+ /**
+ * Used to indicate that the scrolling drawing cache should be kept in memory.
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int PERSISTENT_SCROLLING_CACHE = 0x2;
+
+ /**
+ * Used to indicate that all drawing caches should be kept in memory.
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public static final int PERSISTENT_ALL_CACHES = 0x3;
+
+ // Layout Modes
+
+ private static final int LAYOUT_MODE_UNDEFINED = -1;
+
+ /**
+ * This constant is a {@link #setLayoutMode(int) layoutMode}.
+ * Clip bounds are the raw values of {@link #getLeft() left}, {@link #getTop() top},
+ * {@link #getRight() right} and {@link #getBottom() bottom}.
+ */
+ public static final int LAYOUT_MODE_CLIP_BOUNDS = 0;
+
+ /**
+ * This constant is a {@link #setLayoutMode(int) layoutMode}.
+ * Optical bounds describe where a widget appears to be. They sit inside the clip
+ * bounds which need to cover a larger area to allow other effects,
+ * such as shadows and glows, to be drawn.
+ */
+ public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1;
+
+ /** @hide */
+ public static int LAYOUT_MODE_DEFAULT = LAYOUT_MODE_CLIP_BOUNDS;
+
+ /**
+ * We clip to padding when FLAG_CLIP_TO_PADDING and FLAG_PADDING_NOT_NULL
+ * are set at the same time.
+ */
+ protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;
+
+ // Index of the child's left position in the mLocation array
+ private static final int CHILD_LEFT_INDEX = 0;
+ // Index of the child's top position in the mLocation array
+ private static final int CHILD_TOP_INDEX = 1;
+
+ // Child views of this ViewGroup
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private View[] mChildren;
+ // Number of valid children in the mChildren array, the rest should be null or not
+ // considered as children
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ private int mChildrenCount;
+
+ // Whether layout calls are currently being suppressed, controlled by calls to
+ // suppressLayout()
+ boolean mSuppressLayout = false;
+
+ // Whether any layout calls have actually been suppressed while mSuppressLayout
+ // has been true. This tracks whether we need to issue a requestLayout() when
+ // layout is later re-enabled.
+ private boolean mLayoutCalledWhileSuppressed = false;
+
+ private static final int ARRAY_INITIAL_CAPACITY = 12;
+ private static final int ARRAY_CAPACITY_INCREMENT = 12;
+
+ private static float[] sDebugLines;
+
+ // Used to draw cached views
+ Paint mCachePaint;
+
+ // Used to animate add/remove changes in layout
+ private LayoutTransition mTransition;
+
+ // The set of views that are currently being transitioned. This list is used to track views
+ // being removed that should not actually be removed from the parent yet because they are
+ // being animated.
+ private ArrayList<View> mTransitioningViews;
+
+ // List of children changing visibility. This is used to potentially keep rendering
+ // views during a transition when they otherwise would have become gone/invisible
+ private ArrayList<View> mVisibilityChangingChildren;
+
+ // Temporary holder of presorted children, only used for
+ // input/software draw dispatch for correctly Z ordering.
+ private ArrayList<View> mPreSortedChildren;
+
+ // Indicates how many of this container's child subtrees contain transient state
+ @ViewDebug.ExportedProperty(category = "layout")
+ private int mChildCountWithTransientState = 0;
+
+ /**
+ * Currently registered axes for nested scrolling. Flag set consisting of
+ * {@link #SCROLL_AXIS_HORIZONTAL} {@link #SCROLL_AXIS_VERTICAL} or {@link #SCROLL_AXIS_NONE}
+ * for null.
+ */
+ private int mNestedScrollAxes;
+
+ // Used to manage the list of transient views, added by addTransientView()
+ private IntArray mTransientIndices = null;
+ private List<View> mTransientViews = null;
+
+ /**
+ * Keeps track of how many child views have UnhandledKeyEventListeners. This should only be
+ * updated on the UI thread so shouldn't require explicit synchronization.
+ */
+ int mChildUnhandledKeyListeners = 0;
+
+ /**
+ * Current dispatch mode of animation events
+ *
+ * @see WindowInsetsAnimation.Callback#getDispatchMode()
+ */
+ private @DispatchMode int mInsetsAnimationDispatchMode = DISPATCH_MODE_CONTINUE_ON_SUBTREE;
+
+ /**
+ * Empty ActionMode used as a sentinel in recursive entries to startActionModeForChild.
+ *
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback)
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int)
+ */
+ private static final ActionMode SENTINEL_ACTION_MODE = new ActionMode() {
+ @Override
+ public void setTitle(CharSequence title) {}
+
+ @Override
+ public void setTitle(int resId) {}
+
+ @Override
+ public void setSubtitle(CharSequence subtitle) {}
+
+ @Override
+ public void setSubtitle(int resId) {}
+
+ @Override
+ public void setCustomView(View view) {}
+
+ @Override
+ public void invalidate() {}
+
+ @Override
+ public void finish() {}
+
+ @Override
+ public Menu getMenu() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getSubtitle() {
+ return null;
+ }
+
+ @Override
+ public View getCustomView() {
+ return null;
+ }
+
+ @Override
+ public MenuInflater getMenuInflater() {
+ return null;
+ }
+ };
+
+ public ViewGroup(Context context) {
+ this(context, null);
+ }
+
+ public ViewGroup(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ initViewGroup();
+ initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private void initViewGroup() {
+ // ViewGroup doesn't draw by default
+ if (!isShowingLayoutBounds()) {
+ setFlags(WILL_NOT_DRAW, DRAW_MASK);
+ }
+ mGroupFlags |= FLAG_CLIP_CHILDREN;
+ mGroupFlags |= FLAG_CLIP_TO_PADDING;
+ mGroupFlags |= FLAG_ANIMATION_DONE;
+ mGroupFlags |= FLAG_ANIMATION_CACHE;
+ mGroupFlags |= FLAG_ALWAYS_DRAWN_WITH_CACHE;
+
+ if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
+ mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
+ }
+
+ setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
+
+ mChildren = new View[ARRAY_INITIAL_CAPACITY];
+ mChildrenCount = 0;
+
+ mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE;
+ }
+
+ private void initFromAttributes(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup,
+ defStyleAttr, defStyleRes);
+ saveAttributeDataForStyleable(context, R.styleable.ViewGroup, attrs, a, defStyleAttr,
+ defStyleRes);
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.ViewGroup_clipChildren:
+ setClipChildren(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_clipToPadding:
+ setClipToPadding(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_animationCache:
+ setAnimationCacheEnabled(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_persistentDrawingCache:
+ setPersistentDrawingCache(a.getInt(attr, PERSISTENT_SCROLLING_CACHE));
+ break;
+ case R.styleable.ViewGroup_addStatesFromChildren:
+ setAddStatesFromChildren(a.getBoolean(attr, false));
+ break;
+ case R.styleable.ViewGroup_alwaysDrawnWithCache:
+ setAlwaysDrawnWithCacheEnabled(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_layoutAnimation:
+ int id = a.getResourceId(attr, -1);
+ if (id > 0) {
+ setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, id));
+ }
+ break;
+ case R.styleable.ViewGroup_descendantFocusability:
+ setDescendantFocusability(DESCENDANT_FOCUSABILITY_FLAGS[a.getInt(attr, 0)]);
+ break;
+ case R.styleable.ViewGroup_splitMotionEvents:
+ setMotionEventSplittingEnabled(a.getBoolean(attr, false));
+ break;
+ case R.styleable.ViewGroup_animateLayoutChanges:
+ boolean animateLayoutChanges = a.getBoolean(attr, false);
+ if (animateLayoutChanges) {
+ setLayoutTransition(new LayoutTransition());
+ }
+ break;
+ case R.styleable.ViewGroup_layoutMode:
+ setLayoutMode(a.getInt(attr, LAYOUT_MODE_UNDEFINED));
+ break;
+ case R.styleable.ViewGroup_transitionGroup:
+ setTransitionGroup(a.getBoolean(attr, false));
+ break;
+ case R.styleable.ViewGroup_touchscreenBlocksFocus:
+ setTouchscreenBlocksFocus(a.getBoolean(attr, false));
+ break;
+ }
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Gets the descendant focusability of this view group. The descendant
+ * focusability defines the relationship between this view group and its
+ * descendants when looking for a view to take focus in
+ * {@link #requestFocus(int, android.graphics.Rect)}.
+ *
+ * @return one of {@link #FOCUS_BEFORE_DESCENDANTS}, {@link #FOCUS_AFTER_DESCENDANTS},
+ * {@link #FOCUS_BLOCK_DESCENDANTS}.
+ */
+ @ViewDebug.ExportedProperty(category = "focus", mapping = {
+ @ViewDebug.IntToString(from = FOCUS_BEFORE_DESCENDANTS, to = "FOCUS_BEFORE_DESCENDANTS"),
+ @ViewDebug.IntToString(from = FOCUS_AFTER_DESCENDANTS, to = "FOCUS_AFTER_DESCENDANTS"),
+ @ViewDebug.IntToString(from = FOCUS_BLOCK_DESCENDANTS, to = "FOCUS_BLOCK_DESCENDANTS")
+ })
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = FOCUS_BEFORE_DESCENDANTS, name = "beforeDescendants"),
+ @EnumEntry(value = FOCUS_AFTER_DESCENDANTS, name = "afterDescendants"),
+ @EnumEntry(value = FOCUS_BLOCK_DESCENDANTS, name = "blocksDescendants")
+ })
+ public int getDescendantFocusability() {
+ return mGroupFlags & FLAG_MASK_FOCUSABILITY;
+ }
+
+ /**
+ * Set the descendant focusability of this view group. This defines the relationship
+ * between this view group and its descendants when looking for a view to
+ * take focus in {@link #requestFocus(int, android.graphics.Rect)}.
+ *
+ * @param focusability one of {@link #FOCUS_BEFORE_DESCENDANTS}, {@link #FOCUS_AFTER_DESCENDANTS},
+ * {@link #FOCUS_BLOCK_DESCENDANTS}.
+ */
+ public void setDescendantFocusability(int focusability) {
+ switch (focusability) {
+ case FOCUS_BEFORE_DESCENDANTS:
+ case FOCUS_AFTER_DESCENDANTS:
+ case FOCUS_BLOCK_DESCENDANTS:
+ break;
+ default:
+ throw new IllegalArgumentException("must be one of FOCUS_BEFORE_DESCENDANTS, "
+ + "FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS");
+ }
+ mGroupFlags &= ~FLAG_MASK_FOCUSABILITY;
+ mGroupFlags |= (focusability & FLAG_MASK_FOCUSABILITY);
+ }
+
+ @Override
+ void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
+ if (mFocused != null) {
+ mFocused.unFocus(this);
+ mFocused = null;
+ mFocusedInCluster = null;
+ }
+ super.handleFocusGainInternal(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (DBG) {
+ System.out.println(this + " requestChildFocus()");
+ }
+ if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
+ return;
+ }
+
+ // Unfocus us, if necessary
+ super.unFocus(focused);
+
+ // We had a previous notion of who had focus. Clear it.
+ if (mFocused != child) {
+ if (mFocused != null) {
+ mFocused.unFocus(focused);
+ }
+
+ mFocused = child;
+ }
+ if (mParent != null) {
+ mParent.requestChildFocus(this, focused);
+ }
+ }
+
+ void setDefaultFocus(View child) {
+ // Stop at any higher view which is explicitly focused-by-default
+ if (mDefaultFocus != null && mDefaultFocus.isFocusedByDefault()) {
+ return;
+ }
+
+ mDefaultFocus = child;
+
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).setDefaultFocus(this);
+ }
+ }
+
+ /**
+ * Clears the default-focus chain from {@param child} up to the first parent which has another
+ * default-focusable branch below it or until there is no default-focus chain.
+ *
+ * @param child
+ */
+ void clearDefaultFocus(View child) {
+ // Stop at any higher view which is explicitly focused-by-default
+ if (mDefaultFocus != child && mDefaultFocus != null
+ && mDefaultFocus.isFocusedByDefault()) {
+ return;
+ }
+
+ mDefaultFocus = null;
+
+ // Search child siblings for default focusables.
+ for (int i = 0; i < mChildrenCount; ++i) {
+ View sibling = mChildren[i];
+ if (sibling.isFocusedByDefault()) {
+ mDefaultFocus = sibling;
+ return;
+ } else if (mDefaultFocus == null && sibling.hasDefaultFocus()) {
+ mDefaultFocus = sibling;
+ }
+ }
+
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearDefaultFocus(this);
+ }
+ }
+
+ @Override
+ boolean hasDefaultFocus() {
+ return mDefaultFocus != null || super.hasDefaultFocus();
+ }
+
+ /**
+ * Removes {@code child} (and associated focusedInCluster chain) from the cluster containing
+ * it.
+ * <br>
+ * This is intended to be run on {@code child}'s immediate parent. This is necessary because
+ * the chain is sometimes cleared after {@code child} has been detached.
+ */
+ void clearFocusedInCluster(View child) {
+ if (mFocusedInCluster != child) {
+ return;
+ }
+ clearFocusedInCluster();
+ }
+
+ /**
+ * Removes the focusedInCluster chain from this up to the cluster containing it.
+ */
+ void clearFocusedInCluster() {
+ View top = findKeyboardNavigationCluster();
+ ViewParent parent = this;
+ do {
+ ((ViewGroup) parent).mFocusedInCluster = null;
+ if (parent == top) {
+ break;
+ }
+ parent = parent.getParent();
+ } while (parent instanceof ViewGroup);
+ }
+
+ @Override
+ public void focusableViewAvailable(View v) {
+ if (mParent != null
+ // shortcut: don't report a new focusable view if we block our descendants from
+ // getting focus or if we're not visible
+ && (getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS)
+ && ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
+ && (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())
+ // shortcut: don't report a new focusable view if we already are focused
+ // (and we don't prefer our descendants)
+ //
+ // note: knowing that mFocused is non-null is not a good enough reason
+ // to break the traversal since in that case we'd actually have to find
+ // the focused view and make sure it wasn't FOCUS_AFTER_DESCENDANTS and
+ // an ancestor of v; this will get checked for at ViewAncestor
+ && !(isFocused() && getDescendantFocusability() != FOCUS_AFTER_DESCENDANTS)) {
+ mParent.focusableViewAvailable(v);
+ }
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+ if (isShowingContextMenuWithCoords()) {
+ // We're being called for compatibility. Return false and let the version
+ // with coordinates recurse up.
+ return false;
+ }
+ return mParent != null && mParent.showContextMenuForChild(originalView);
+ }
+
+ /**
+ * @hide used internally for compatibility with existing app code only
+ */
+ public final boolean isShowingContextMenuWithCoords() {
+ return (mGroupFlags & FLAG_SHOW_CONTEXT_MENU_WITH_COORDS) != 0;
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView, float x, float y) {
+ try {
+ mGroupFlags |= FLAG_SHOW_CONTEXT_MENU_WITH_COORDS;
+ if (showContextMenuForChild(originalView)) {
+ return true;
+ }
+ } finally {
+ mGroupFlags &= ~FLAG_SHOW_CONTEXT_MENU_WITH_COORDS;
+ }
+ return mParent != null && mParent.showContextMenuForChild(originalView, x, y);
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback) {
+ if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED) == 0) {
+ // This is the original call.
+ try {
+ mGroupFlags |= FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED;
+ return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY);
+ } finally {
+ mGroupFlags &= ~FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED;
+ }
+ } else {
+ // We are being called from the new method with type.
+ return SENTINEL_ACTION_MODE;
+ }
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(
+ View originalView, ActionMode.Callback callback, int type) {
+ if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED) == 0
+ && type == ActionMode.TYPE_PRIMARY) {
+ ActionMode mode;
+ try {
+ mGroupFlags |= FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED;
+ mode = startActionModeForChild(originalView, callback);
+ } finally {
+ mGroupFlags &= ~FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED;
+ }
+ if (mode != SENTINEL_ACTION_MODE) {
+ return mode;
+ }
+ }
+ if (mParent != null) {
+ try {
+ return mParent.startActionModeForChild(originalView, callback, type);
+ } catch (AbstractMethodError ame) {
+ // Custom view parents might not implement this method.
+ return mParent.startActionModeForChild(originalView, callback);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean dispatchActivityResult(
+ String who, int requestCode, int resultCode, Intent data) {
+ if (super.dispatchActivityResult(who, requestCode, resultCode, data)) {
+ return true;
+ }
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child.dispatchActivityResult(who, requestCode, resultCode, data)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Find the nearest view in the specified direction that wants to take
+ * focus.
+ *
+ * @param focused The view that currently has focus
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
+ * FOCUS_RIGHT, or 0 for not applicable.
+ */
+ @Override
+ public View focusSearch(View focused, int direction) {
+ if (isRootNamespace()) {
+ // root namespace means we should consider ourselves the top of the
+ // tree for focus searching; otherwise we could be focus searching
+ // into other tabs. see LocalActivityManager and TabHost for more info.
+ return FocusFinder.getInstance().findNextFocus(this, focused, direction);
+ } else if (mParent != null) {
+ return mParent.focusSearch(focused, direction);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
+ return false;
+ }
+
+ @Override
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ ViewParent parent = mParent;
+ if (parent == null) {
+ return false;
+ }
+ final boolean propagate = onRequestSendAccessibilityEvent(child, event);
+ if (!propagate) {
+ return false;
+ }
+ return parent.requestSendAccessibilityEvent(this, event);
+ }
+
+ /**
+ * Called when a child has requested sending an {@link AccessibilityEvent} and
+ * gives an opportunity to its parent to augment the event.
+ * <p>
+ * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling
+ * {@link android.view.View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its
+ * {@link android.view.View.AccessibilityDelegate#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)}
+ * is responsible for handling this call.
+ * </p>
+ *
+ * @param child The child which requests sending the event.
+ * @param event The event to be sent.
+ * @return True if the event should be sent.
+ *
+ * @see #requestSendAccessibilityEvent(View, AccessibilityEvent)
+ */
+ public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ if (mAccessibilityDelegate != null) {
+ return mAccessibilityDelegate.onRequestSendAccessibilityEvent(this, child, event);
+ } else {
+ return onRequestSendAccessibilityEventInternal(child, event);
+ }
+ }
+
+ /**
+ * @see #onRequestSendAccessibilityEvent(View, AccessibilityEvent)
+ *
+ * Note: Called from the default {@link View.AccessibilityDelegate}.
+ *
+ * @hide
+ */
+ public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+ return true;
+ }
+
+ /**
+ * Called when a child view has changed whether or not it is tracking transient state.
+ */
+ @Override
+ public void childHasTransientStateChanged(View child, boolean childHasTransientState) {
+ final boolean oldHasTransientState = hasTransientState();
+ if (childHasTransientState) {
+ mChildCountWithTransientState++;
+ } else {
+ mChildCountWithTransientState--;
+ }
+
+ final boolean newHasTransientState = hasTransientState();
+ if (mParent != null && oldHasTransientState != newHasTransientState) {
+ try {
+ mParent.childHasTransientStateChanged(this, newHasTransientState);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasTransientState() {
+ return mChildCountWithTransientState > 0 || super.hasTransientState();
+ }
+
+ @Override
+ public boolean dispatchUnhandledMove(View focused, int direction) {
+ return mFocused != null &&
+ mFocused.dispatchUnhandledMove(focused, direction);
+ }
+
+ @Override
+ public void clearChildFocus(View child) {
+ if (DBG) {
+ System.out.println(this + " clearChildFocus()");
+ }
+
+ mFocused = null;
+ if (mParent != null) {
+ mParent.clearChildFocus(this);
+ }
+ }
+
+ @Override
+ public void clearFocus() {
+ if (DBG) {
+ System.out.println(this + " clearFocus()");
+ }
+ if (mFocused == null) {
+ super.clearFocus();
+ } else {
+ View focused = mFocused;
+ mFocused = null;
+ focused.clearFocus();
+ }
+ }
+
+ @Override
+ void unFocus(View focused) {
+ if (DBG) {
+ System.out.println(this + " unFocus()");
+ }
+ if (mFocused == null) {
+ super.unFocus(focused);
+ } else {
+ mFocused.unFocus(focused);
+ mFocused = null;
+ }
+ }
+
+ /**
+ * Returns the focused child of this view, if any. The child may have focus
+ * or contain focus.
+ *
+ * @return the focused child or null.
+ */
+ public View getFocusedChild() {
+ return mFocused;
+ }
+
+ View getDeepestFocusedChild() {
+ View v = this;
+ while (v != null) {
+ if (v.isFocused()) {
+ return v;
+ }
+ v = v instanceof ViewGroup ? ((ViewGroup) v).getFocusedChild() : null;
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if this view has or contains focus
+ *
+ * @return true if this view has or contains focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View#findFocus()
+ */
+ @Override
+ public View findFocus() {
+ if (DBG) {
+ System.out.println("Find focus in " + this + ": flags="
+ + isFocused() + ", child=" + mFocused);
+ }
+
+ if (isFocused()) {
+ return this;
+ }
+
+ if (mFocused != null) {
+ return mFocused.findFocus();
+ }
+ return null;
+ }
+
+ @Override
+ boolean hasFocusable(boolean allowAutoFocus, boolean dispatchExplicit) {
+ // This should probably be super.hasFocusable, but that would change
+ // behavior. Historically, we have not checked the ancestor views for
+ // shouldBlockFocusForTouchscreen() in ViewGroup.hasFocusable.
+
+ // Invisible and gone views are never focusable.
+ if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+
+ // Only use effective focusable value when allowed.
+ if ((allowAutoFocus || getFocusable() != FOCUSABLE_AUTO) && isFocusable()) {
+ return true;
+ }
+
+ // Determine whether we have a focused descendant.
+ final int descendantFocusability = getDescendantFocusability();
+ if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+ return hasFocusableChild(dispatchExplicit);
+ }
+
+ return false;
+ }
+
+ boolean hasFocusableChild(boolean dispatchExplicit) {
+ // Determine whether we have a focusable descendant.
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+
+ // In case the subclass has overridden has[Explicit]Focusable, dispatch
+ // to the expected one for each child even though we share logic here.
+ if ((dispatchExplicit && child.hasExplicitFocusable())
+ || (!dispatchExplicit && child.hasFocusable())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ final int focusableCount = views.size();
+
+ final int descendantFocusability = getDescendantFocusability();
+ final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
+ final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);
+
+ if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {
+ if (focusSelf) {
+ super.addFocusables(views, direction, focusableMode);
+ }
+ return;
+ }
+
+ if (blockFocusForTouchscreen) {
+ focusableMode |= FOCUSABLES_TOUCH_MODE;
+ }
+
+ if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
+ super.addFocusables(views, direction, focusableMode);
+ }
+
+ int count = 0;
+ final View[] children = new View[mChildrenCount];
+ for (int i = 0; i < mChildrenCount; ++i) {
+ View child = mChildren[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ children[count++] = child;
+ }
+ }
+ FocusFinder.sort(children, 0, count, this, isLayoutRtl());
+ for (int i = 0; i < count; ++i) {
+ children[i].addFocusables(views, direction, focusableMode);
+ }
+
+ // When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
+ // there aren't any focusable descendants. this is
+ // to avoid the focus search finding layouts when a more precise search
+ // among the focusable children would be more interesting.
+ if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
+ && focusableCount == views.size()) {
+ super.addFocusables(views, direction, focusableMode);
+ }
+ }
+
+ @Override
+ public void addKeyboardNavigationClusters(Collection<View> views, int direction) {
+ final int focusableCount = views.size();
+
+ if (isKeyboardNavigationCluster()) {
+ // Cluster-navigation can enter a touchscreenBlocksFocus cluster, so temporarily
+ // disable touchscreenBlocksFocus to evaluate whether it contains focusables.
+ final boolean blockedFocus = getTouchscreenBlocksFocus();
+ try {
+ setTouchscreenBlocksFocusNoRefocus(false);
+ super.addKeyboardNavigationClusters(views, direction);
+ } finally {
+ setTouchscreenBlocksFocusNoRefocus(blockedFocus);
+ }
+ } else {
+ super.addKeyboardNavigationClusters(views, direction);
+ }
+
+ if (focusableCount != views.size()) {
+ // No need to look for groups inside a group.
+ return;
+ }
+
+ if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
+ return;
+ }
+
+ int count = 0;
+ final View[] visibleChildren = new View[mChildrenCount];
+ for (int i = 0; i < mChildrenCount; ++i) {
+ final View child = mChildren[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ visibleChildren[count++] = child;
+ }
+ }
+ FocusFinder.sort(visibleChildren, 0, count, this, isLayoutRtl());
+ for (int i = 0; i < count; ++i) {
+ visibleChildren[i].addKeyboardNavigationClusters(views, direction);
+ }
+ }
+
+ /**
+ * Set whether this ViewGroup should ignore focus requests for itself and its children.
+ * If this option is enabled and the ViewGroup or a descendant currently has focus, focus
+ * will proceed forward.
+ *
+ * @param touchscreenBlocksFocus true to enable blocking focus in the presence of a touchscreen
+ */
+ public void setTouchscreenBlocksFocus(boolean touchscreenBlocksFocus) {
+ if (touchscreenBlocksFocus) {
+ mGroupFlags |= FLAG_TOUCHSCREEN_BLOCKS_FOCUS;
+ if (hasFocus() && !isKeyboardNavigationCluster()) {
+ final View focusedChild = getDeepestFocusedChild();
+ if (!focusedChild.isFocusableInTouchMode()) {
+ final View newFocus = focusSearch(FOCUS_FORWARD);
+ if (newFocus != null) {
+ newFocus.requestFocus();
+ }
+ }
+ }
+ } else {
+ mGroupFlags &= ~FLAG_TOUCHSCREEN_BLOCKS_FOCUS;
+ }
+ }
+
+ private void setTouchscreenBlocksFocusNoRefocus(boolean touchscreenBlocksFocus) {
+ if (touchscreenBlocksFocus) {
+ mGroupFlags |= FLAG_TOUCHSCREEN_BLOCKS_FOCUS;
+ } else {
+ mGroupFlags &= ~FLAG_TOUCHSCREEN_BLOCKS_FOCUS;
+ }
+ }
+
+ /**
+ * Check whether this ViewGroup should ignore focus requests for itself and its children.
+ */
+ @ViewDebug.ExportedProperty(category = "focus")
+ @InspectableProperty
+ public boolean getTouchscreenBlocksFocus() {
+ return (mGroupFlags & FLAG_TOUCHSCREEN_BLOCKS_FOCUS) != 0;
+ }
+
+ boolean shouldBlockFocusForTouchscreen() {
+ // There is a special case for keyboard-navigation clusters. We allow cluster navigation
+ // to jump into blockFocusForTouchscreen ViewGroups which are clusters. Once in the
+ // cluster, focus is free to move around within it.
+ return getTouchscreenBlocksFocus() &&
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
+ && !(isKeyboardNavigationCluster()
+ && (hasFocus() || (findKeyboardNavigationCluster() != this)));
+ }
+
+ @Override
+ public void findViewsWithText(ArrayList<View> outViews, CharSequence text, int flags) {
+ super.findViewsWithText(outViews, text, flags);
+ final int childrenCount = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < childrenCount; i++) {
+ View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
+ && (child.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
+ child.findViewsWithText(outViews, text, flags);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public View findViewByAccessibilityIdTraversal(int accessibilityId) {
+ View foundView = super.findViewByAccessibilityIdTraversal(accessibilityId);
+ if (foundView != null) {
+ return foundView;
+ }
+
+ if (getAccessibilityNodeProvider() != null) {
+ return null;
+ }
+
+ final int childrenCount = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < childrenCount; i++) {
+ View child = children[i];
+ foundView = child.findViewByAccessibilityIdTraversal(accessibilityId);
+ if (foundView != null) {
+ return foundView;
+ }
+ }
+
+ return null;
+ }
+
+ /** @hide */
+ @Override
+ public View findViewByAutofillIdTraversal(int autofillId) {
+ View foundView = super.findViewByAutofillIdTraversal(autofillId);
+ if (foundView != null) {
+ return foundView;
+ }
+
+ final int childrenCount = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < childrenCount; i++) {
+ View child = children[i];
+ foundView = child.findViewByAutofillIdTraversal(autofillId);
+ if (foundView != null) {
+ return foundView;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void dispatchWindowFocusChanged(boolean hasFocus) {
+ super.dispatchWindowFocusChanged(hasFocus);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchWindowFocusChanged(hasFocus);
+ }
+ }
+
+ @Override
+ public void addTouchables(ArrayList<View> views) {
+ super.addTouchables(views);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ child.addTouchables(views);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @UnsupportedAppUsage
+ public void makeOptionalFitsSystemWindows() {
+ super.makeOptionalFitsSystemWindows();
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].makeOptionalFitsSystemWindows();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void makeFrameworkOptionalFitsSystemWindows() {
+ super.makeFrameworkOptionalFitsSystemWindows();
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].makeFrameworkOptionalFitsSystemWindows();
+ }
+ }
+
+ @Override
+ public void dispatchDisplayHint(int hint) {
+ super.dispatchDisplayHint(hint);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchDisplayHint(hint);
+ }
+ }
+
+ /**
+ * Called when a view's visibility has changed. Notify the parent to take any appropriate
+ * action.
+ *
+ * @param child The view whose visibility has changed
+ * @param oldVisibility The previous visibility value (GONE, INVISIBLE, or VISIBLE).
+ * @param newVisibility The new visibility value (GONE, INVISIBLE, or VISIBLE).
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
+ if (mTransition != null) {
+ if (newVisibility == VISIBLE) {
+ mTransition.showChild(this, child, oldVisibility);
+ } else {
+ mTransition.hideChild(this, child, newVisibility);
+ if (mTransitioningViews != null && mTransitioningViews.contains(child)) {
+ // Only track this on disappearing views - appearing views are already visible
+ // and don't need special handling during drawChild()
+ if (mVisibilityChangingChildren == null) {
+ mVisibilityChangingChildren = new ArrayList<View>();
+ }
+ mVisibilityChangingChildren.add(child);
+ addDisappearingView(child);
+ }
+ }
+ }
+
+ // in all cases, for drags
+ if (newVisibility == VISIBLE && mCurrentDragStartEvent != null) {
+ if (!mChildrenInterestedInDrag.contains(child)) {
+ notifyChildOfDragStart(child);
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchVisibilityChanged(View changedView, int visibility) {
+ super.dispatchVisibilityChanged(changedView, visibility);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchVisibilityChanged(changedView, visibility);
+ }
+ }
+
+ @Override
+ public void dispatchWindowVisibilityChanged(int visibility) {
+ super.dispatchWindowVisibilityChanged(visibility);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchWindowVisibilityChanged(visibility);
+ }
+ }
+
+ @Override
+ boolean dispatchVisibilityAggregated(boolean isVisible) {
+ isVisible = super.dispatchVisibilityAggregated(isVisible);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ // Only dispatch to visible children. Not visible children and their subtrees already
+ // know that they aren't visible and that's not going to change as a result of
+ // whatever triggered this dispatch.
+ if (children[i].getVisibility() == VISIBLE) {
+ children[i].dispatchVisibilityAggregated(isVisible);
+ }
+ }
+ return isVisible;
+ }
+
+ @Override
+ public void dispatchConfigurationChanged(Configuration newConfig) {
+ super.dispatchConfigurationChanged(newConfig);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchConfigurationChanged(newConfig);
+ }
+ }
+
+ @Override
+ public void recomputeViewAttributes(View child) {
+ if (mAttachInfo != null && !mAttachInfo.mRecomputeGlobalAttributes) {
+ ViewParent parent = mParent;
+ if (parent != null) parent.recomputeViewAttributes(this);
+ }
+ }
+
+ @Override
+ void dispatchCollectViewAttributes(AttachInfo attachInfo, int visibility) {
+ if ((visibility & VISIBILITY_MASK) == VISIBLE) {
+ super.dispatchCollectViewAttributes(attachInfo, visibility);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ child.dispatchCollectViewAttributes(attachInfo,
+ visibility | (child.mViewFlags&VISIBILITY_MASK));
+ }
+ }
+ }
+
+ @Override
+ public void bringChildToFront(View child) {
+ final int index = indexOfChild(child);
+ if (index >= 0) {
+ removeFromArray(index);
+ addInArray(child, mChildrenCount);
+ child.mParent = this;
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ private PointF getLocalPoint() {
+ if (mLocalPoint == null) mLocalPoint = new PointF();
+ return mLocalPoint;
+ }
+
+ @Override
+ boolean dispatchDragEnterExitInPreN(DragEvent event) {
+ if (event.mAction == DragEvent.ACTION_DRAG_EXITED && mCurrentDragChild != null) {
+ // The drag exited a sub-tree of views; notify of the exit all descendants that are in
+ // entered state.
+ // We don't need this recursive delivery for ENTERED events because they get generated
+ // from the recursive delivery of LOCATION/DROP events, and hence, don't need their own
+ // recursion.
+ mCurrentDragChild.dispatchDragEnterExitInPreN(event);
+ mCurrentDragChild = null;
+ }
+ return mIsInterestedInDrag && super.dispatchDragEnterExitInPreN(event);
+ }
+
+ // TODO: Write real docs
+ @Override
+ public boolean dispatchDragEvent(DragEvent event) {
+ boolean retval = false;
+ final float tx = event.mX;
+ final float ty = event.mY;
+ final ClipData td = event.mClipData;
+
+ // Dispatch down the view hierarchy
+ final PointF localPoint = getLocalPoint();
+
+ switch (event.mAction) {
+ case DragEvent.ACTION_DRAG_STARTED: {
+ // Clear the state to recalculate which views we drag over.
+ mCurrentDragChild = null;
+
+ // Set up our tracking of drag-started notifications
+ mCurrentDragStartEvent = DragEvent.obtain(event);
+ if (mChildrenInterestedInDrag == null) {
+ mChildrenInterestedInDrag = new HashSet<View>();
+ } else {
+ mChildrenInterestedInDrag.clear();
+ }
+
+ // Now dispatch down to our children, caching the responses
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ child.mPrivateFlags2 &= ~View.DRAG_MASK;
+ if (child.getVisibility() == VISIBLE) {
+ if (notifyChildOfDragStart(children[i])) {
+ retval = true;
+ }
+ }
+ }
+
+ // Notify itself of the drag start.
+ mIsInterestedInDrag = super.dispatchDragEvent(event);
+ if (mIsInterestedInDrag) {
+ retval = true;
+ }
+
+ if (!retval) {
+ // Neither us nor any of our children are interested in this drag, so stop tracking
+ // the current drag event.
+ mCurrentDragStartEvent.recycle();
+ mCurrentDragStartEvent = null;
+ }
+ } break;
+
+ case DragEvent.ACTION_DRAG_ENDED: {
+ // Release the bookkeeping now that the drag lifecycle has ended
+ final HashSet<View> childrenInterestedInDrag = mChildrenInterestedInDrag;
+ if (childrenInterestedInDrag != null) {
+ for (View child : childrenInterestedInDrag) {
+ // If a child was interested in the ongoing drag, it's told that it's over
+ if (child.dispatchDragEvent(event)) {
+ retval = true;
+ }
+ }
+ childrenInterestedInDrag.clear();
+ }
+ if (mCurrentDragStartEvent != null) {
+ mCurrentDragStartEvent.recycle();
+ mCurrentDragStartEvent = null;
+ }
+
+ if (mIsInterestedInDrag) {
+ if (super.dispatchDragEvent(event)) {
+ retval = true;
+ }
+ mIsInterestedInDrag = false;
+ }
+ } break;
+
+ case DragEvent.ACTION_DRAG_LOCATION:
+ case DragEvent.ACTION_DROP: {
+ // Find the [possibly new] drag target
+ View target = findFrontmostDroppableChildAt(event.mX, event.mY, localPoint);
+
+ if (target != mCurrentDragChild) {
+ if (sCascadedDragDrop) {
+ // For pre-Nougat apps, make sure that the whole hierarchy of views that contain
+ // the drag location is kept in the state between ENTERED and EXITED events.
+ // (Starting with N, only the innermost view will be in that state).
+
+ final int action = event.mAction;
+ // Position should not be available for ACTION_DRAG_ENTERED and
+ // ACTION_DRAG_EXITED.
+ event.mX = 0;
+ event.mY = 0;
+ event.mClipData = null;
+
+ if (mCurrentDragChild != null) {
+ event.mAction = DragEvent.ACTION_DRAG_EXITED;
+ mCurrentDragChild.dispatchDragEnterExitInPreN(event);
+ }
+
+ if (target != null) {
+ event.mAction = DragEvent.ACTION_DRAG_ENTERED;
+ target.dispatchDragEnterExitInPreN(event);
+ }
+
+ event.mAction = action;
+ event.mX = tx;
+ event.mY = ty;
+ event.mClipData = td;
+ }
+ mCurrentDragChild = target;
+ }
+
+ if (target == null && mIsInterestedInDrag) {
+ target = this;
+ }
+
+ // Dispatch the actual drag notice, localized into the target coordinates.
+ if (target != null) {
+ if (target != this) {
+ event.mX = localPoint.x;
+ event.mY = localPoint.y;
+
+ retval = target.dispatchDragEvent(event);
+
+ event.mX = tx;
+ event.mY = ty;
+
+ if (mIsInterestedInDrag) {
+ final boolean eventWasConsumed;
+ if (sCascadedDragDrop) {
+ eventWasConsumed = retval;
+ } else {
+ eventWasConsumed = event.mEventHandlerWasCalled;
+ }
+
+ if (!eventWasConsumed) {
+ retval = super.dispatchDragEvent(event);
+ }
+ }
+ } else {
+ retval = super.dispatchDragEvent(event);
+ }
+ }
+ } break;
+ }
+
+ return retval;
+ }
+
+ // Find the frontmost child view that lies under the given point, and calculate
+ // the position within its own local coordinate system.
+ View findFrontmostDroppableChildAt(float x, float y, PointF outLocalPoint) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = children[i];
+ if (!child.canAcceptDrag()) {
+ continue;
+ }
+
+ if (isTransformedTouchPointInView(x, y, child, outLocalPoint)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ boolean notifyChildOfDragStart(View child) {
+ // The caller guarantees that the child is not in mChildrenInterestedInDrag yet.
+
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(View.VIEW_LOG_TAG, "Sending drag-started to view: " + child);
+ }
+
+ final float tx = mCurrentDragStartEvent.mX;
+ final float ty = mCurrentDragStartEvent.mY;
+
+ final float[] point = getTempLocationF();
+ point[0] = tx;
+ point[1] = ty;
+ transformPointToViewLocal(point, child);
+
+ mCurrentDragStartEvent.mX = point[0];
+ mCurrentDragStartEvent.mY = point[1];
+ final boolean canAccept = child.dispatchDragEvent(mCurrentDragStartEvent);
+ mCurrentDragStartEvent.mX = tx;
+ mCurrentDragStartEvent.mY = ty;
+ mCurrentDragStartEvent.mEventHandlerWasCalled = false;
+ if (canAccept) {
+ mChildrenInterestedInDrag.add(child);
+ if (!child.canAcceptDrag()) {
+ child.mPrivateFlags2 |= View.PFLAG2_DRAG_CAN_ACCEPT;
+ child.refreshDrawableState();
+ }
+ }
+ return canAccept;
+ }
+
+ @Override
+ @Deprecated
+ public void dispatchWindowSystemUiVisiblityChanged(int visible) {
+ super.dispatchWindowSystemUiVisiblityChanged(visible);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i=0; i <count; i++) {
+ final View child = children[i];
+ child.dispatchWindowSystemUiVisiblityChanged(visible);
+ }
+ }
+
+ @Override
+ @Deprecated
+ public void dispatchSystemUiVisibilityChanged(int visible) {
+ super.dispatchSystemUiVisibilityChanged(visible);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i=0; i <count; i++) {
+ final View child = children[i];
+ child.dispatchSystemUiVisibilityChanged(visible);
+ }
+ }
+
+ @Override
+ boolean updateLocalSystemUiVisibility(int localValue, int localChanges) {
+ boolean changed = super.updateLocalSystemUiVisibility(localValue, localChanges);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i=0; i <count; i++) {
+ final View child = children[i];
+ changed |= child.updateLocalSystemUiVisibility(localValue, localChanges);
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
+ == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
+ return super.dispatchKeyEventPreIme(event);
+ } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
+ == PFLAG_HAS_BOUNDS) {
+ return mFocused.dispatchKeyEventPreIme(event);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onKeyEvent(event, 1);
+ }
+
+ if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
+ == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
+ if (super.dispatchKeyEvent(event)) {
+ return true;
+ }
+ } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
+ == PFLAG_HAS_BOUNDS) {
+ if (mFocused.dispatchKeyEvent(event)) {
+ return true;
+ }
+ }
+
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
+ == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
+ return super.dispatchKeyShortcutEvent(event);
+ } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
+ == PFLAG_HAS_BOUNDS) {
+ return mFocused.dispatchKeyShortcutEvent(event);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTrackballEvent(event, 1);
+ }
+
+ if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
+ == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
+ if (super.dispatchTrackballEvent(event)) {
+ return true;
+ }
+ } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
+ == PFLAG_HAS_BOUNDS) {
+ if (mFocused.dispatchTrackballEvent(event)) {
+ return true;
+ }
+ }
+
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchCapturedPointerEvent(MotionEvent event) {
+ if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
+ == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
+ if (super.dispatchCapturedPointerEvent(event)) {
+ return true;
+ }
+ } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
+ == PFLAG_HAS_BOUNDS) {
+ if (mFocused.dispatchCapturedPointerEvent(event)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void dispatchPointerCaptureChanged(boolean hasCapture) {
+ exitHoverTargets();
+
+ super.dispatchPointerCaptureChanged(hasCapture);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchPointerCaptureChanged(hasCapture);
+ }
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ if (isOnScrollbarThumb(x, y) || isDraggingScrollBar()) {
+ return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_ARROW);
+ }
+ // Check what the child under the pointer says about the pointer.
+ final int childrenCount = mChildrenCount;
+ if (childrenCount != 0) {
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
+
+ if (!child.canReceivePointerEvents()
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ continue;
+ }
+ final PointerIcon pointerIcon =
+ dispatchResolvePointerIcon(event, pointerIndex, child);
+ if (pointerIcon != null) {
+ if (preorderedList != null) preorderedList.clear();
+ return pointerIcon;
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+
+ // The pointer is not a child or the child has no preferences, returning the default
+ // implementation.
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ private PointerIcon dispatchResolvePointerIcon(MotionEvent event, int pointerIndex,
+ View child) {
+ final PointerIcon pointerIcon;
+ if (!child.hasIdentityMatrix()) {
+ MotionEvent transformedEvent = getTransformedMotionEvent(event, child);
+ pointerIcon = child.onResolvePointerIcon(transformedEvent, pointerIndex);
+ transformedEvent.recycle();
+ } else {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ event.offsetLocation(offsetX, offsetY);
+ pointerIcon = child.onResolvePointerIcon(event, pointerIndex);
+ event.offsetLocation(-offsetX, -offsetY);
+ }
+ return pointerIcon;
+ }
+
+ private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
+ final int childIndex;
+ if (customOrder) {
+ final int childIndex1 = getChildDrawingOrder(childrenCount, i);
+ if (childIndex1 >= childrenCount) {
+ throw new IndexOutOfBoundsException("getChildDrawingOrder() "
+ + "returned invalid index " + childIndex1
+ + " (child count is " + childrenCount + ")");
+ }
+ childIndex = childIndex1;
+ } else {
+ childIndex = i;
+ }
+ return childIndex;
+ }
+
+ @SuppressWarnings({"ConstantConditions"})
+ @Override
+ protected boolean dispatchHoverEvent(MotionEvent event) {
+ final int action = event.getAction();
+
+ // First check whether the view group wants to intercept the hover event.
+ final boolean interceptHover = onInterceptHoverEvent(event);
+ event.setAction(action); // restore action in case it was changed
+
+ MotionEvent eventNoHistory = event;
+ boolean handled = false;
+
+ // Send events to the hovered children and build a new list of hover targets until
+ // one is found that handles the event.
+ HoverTarget firstOldHoverTarget = mFirstHoverTarget;
+ mFirstHoverTarget = null;
+ if (!interceptHover && action != MotionEvent.ACTION_HOVER_EXIT) {
+ final float x = event.getX();
+ final float y = event.getY();
+ final int childrenCount = mChildrenCount;
+ if (childrenCount != 0) {
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ HoverTarget lastHoverTarget = null;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = getAndVerifyPreorderedIndex(
+ childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(
+ preorderedList, children, childIndex);
+ if (!child.canReceivePointerEvents()
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ continue;
+ }
+
+ // Obtain a hover target for this child. Dequeue it from the
+ // old hover target list if the child was previously hovered.
+ HoverTarget hoverTarget = firstOldHoverTarget;
+ final boolean wasHovered;
+ for (HoverTarget predecessor = null; ;) {
+ if (hoverTarget == null) {
+ hoverTarget = HoverTarget.obtain(child);
+ wasHovered = false;
+ break;
+ }
+
+ if (hoverTarget.child == child) {
+ if (predecessor != null) {
+ predecessor.next = hoverTarget.next;
+ } else {
+ firstOldHoverTarget = hoverTarget.next;
+ }
+ hoverTarget.next = null;
+ wasHovered = true;
+ break;
+ }
+
+ predecessor = hoverTarget;
+ hoverTarget = hoverTarget.next;
+ }
+
+ // Enqueue the hover target onto the new hover target list.
+ if (lastHoverTarget != null) {
+ lastHoverTarget.next = hoverTarget;
+ } else {
+ mFirstHoverTarget = hoverTarget;
+ }
+ lastHoverTarget = hoverTarget;
+
+ // Dispatch the event to the child.
+ if (action == MotionEvent.ACTION_HOVER_ENTER) {
+ if (!wasHovered) {
+ // Send the enter as is.
+ handled |= dispatchTransformedGenericPointerEvent(
+ event, child); // enter
+ }
+ } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
+ if (!wasHovered) {
+ // Synthesize an enter from a move.
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+ handled |= dispatchTransformedGenericPointerEvent(
+ eventNoHistory, child); // enter
+ eventNoHistory.setAction(action);
+
+ handled |= dispatchTransformedGenericPointerEvent(
+ eventNoHistory, child); // move
+ } else {
+ // Send the move as is.
+ handled |= dispatchTransformedGenericPointerEvent(event, child);
+ }
+ }
+ if (handled) {
+ break;
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+ }
+
+ // Send exit events to all previously hovered children that are no longer hovered.
+ while (firstOldHoverTarget != null) {
+ final View child = firstOldHoverTarget.child;
+
+ // Exit the old hovered child.
+ if (action == MotionEvent.ACTION_HOVER_EXIT) {
+ // Send the exit as is.
+ handled |= dispatchTransformedGenericPointerEvent(
+ event, child); // exit
+ } else {
+ // Synthesize an exit from a move or enter.
+ // Ignore the result because hover focus has moved to a different view.
+ if (action == MotionEvent.ACTION_HOVER_MOVE) {
+ final boolean hoverExitPending = event.isHoverExitPending();
+ event.setHoverExitPending(true);
+ dispatchTransformedGenericPointerEvent(
+ event, child); // move
+ event.setHoverExitPending(hoverExitPending);
+ }
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ dispatchTransformedGenericPointerEvent(
+ eventNoHistory, child); // exit
+ eventNoHistory.setAction(action);
+ }
+
+ final HoverTarget nextOldHoverTarget = firstOldHoverTarget.next;
+ firstOldHoverTarget.recycle();
+ firstOldHoverTarget = nextOldHoverTarget;
+ }
+
+ // Send events to the view group itself if no children have handled it and the view group
+ // itself is not currently being hover-exited.
+ boolean newHoveredSelf = !handled &&
+ (action != MotionEvent.ACTION_HOVER_EXIT) && !event.isHoverExitPending();
+ if (newHoveredSelf == mHoveredSelf) {
+ if (newHoveredSelf) {
+ // Send event to the view group as before.
+ handled |= super.dispatchHoverEvent(event);
+ }
+ } else {
+ if (mHoveredSelf) {
+ // Exit the view group.
+ if (action == MotionEvent.ACTION_HOVER_EXIT) {
+ // Send the exit as is.
+ handled |= super.dispatchHoverEvent(event); // exit
+ } else {
+ // Synthesize an exit from a move or enter.
+ // Ignore the result because hover focus is moving to a different view.
+ if (action == MotionEvent.ACTION_HOVER_MOVE) {
+ super.dispatchHoverEvent(event); // move
+ }
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ super.dispatchHoverEvent(eventNoHistory); // exit
+ eventNoHistory.setAction(action);
+ }
+ mHoveredSelf = false;
+ }
+
+ if (newHoveredSelf) {
+ // Enter the view group.
+ if (action == MotionEvent.ACTION_HOVER_ENTER) {
+ // Send the enter as is.
+ handled |= super.dispatchHoverEvent(event); // enter
+ mHoveredSelf = true;
+ } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
+ // Synthesize an enter from a move.
+ eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+ eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+ handled |= super.dispatchHoverEvent(eventNoHistory); // enter
+ eventNoHistory.setAction(action);
+
+ handled |= super.dispatchHoverEvent(eventNoHistory); // move
+ mHoveredSelf = true;
+ }
+ }
+ }
+
+ // Recycle the copy of the event that we made.
+ if (eventNoHistory != event) {
+ eventNoHistory.recycle();
+ }
+
+ // Done.
+ return handled;
+ }
+
+ private void exitHoverTargets() {
+ if (mHoveredSelf || mFirstHoverTarget != null) {
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_HOVER_EXIT, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ dispatchHoverEvent(event);
+ event.recycle();
+ }
+ }
+
+ private void cancelHoverTarget(View view) {
+ HoverTarget predecessor = null;
+ HoverTarget target = mFirstHoverTarget;
+ while (target != null) {
+ final HoverTarget next = target.next;
+ if (target.child == view) {
+ if (predecessor == null) {
+ mFirstHoverTarget = next;
+ } else {
+ predecessor.next = next;
+ }
+ target.recycle();
+
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_HOVER_EXIT, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ view.dispatchHoverEvent(event);
+ event.recycle();
+ return;
+ }
+ predecessor = target;
+ target = next;
+ }
+ }
+
+ @Override
+ boolean dispatchTooltipHoverEvent(MotionEvent event) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ break;
+
+ case MotionEvent.ACTION_HOVER_MOVE:
+ View newTarget = null;
+
+ // Check what the child under the pointer says about the tooltip.
+ final int childrenCount = mChildrenCount;
+ if (childrenCount != 0) {
+ final float x = event.getX();
+ final float y = event.getY();
+
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex =
+ getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child =
+ getAndVerifyPreorderedView(preorderedList, children, childIndex);
+ if (!child.canReceivePointerEvents()
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ continue;
+ }
+ if (dispatchTooltipHoverEvent(event, child)) {
+ newTarget = child;
+ break;
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+
+ if (mTooltipHoverTarget != newTarget) {
+ if (mTooltipHoverTarget != null) {
+ event.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ mTooltipHoverTarget.dispatchTooltipHoverEvent(event);
+ event.setAction(action);
+ }
+ mTooltipHoverTarget = newTarget;
+ }
+
+ if (mTooltipHoverTarget != null) {
+ if (mTooltipHoveredSelf) {
+ mTooltipHoveredSelf = false;
+ event.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ super.dispatchTooltipHoverEvent(event);
+ event.setAction(action);
+ }
+ return true;
+ }
+
+ mTooltipHoveredSelf = super.dispatchTooltipHoverEvent(event);
+ return mTooltipHoveredSelf;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mTooltipHoverTarget != null) {
+ mTooltipHoverTarget.dispatchTooltipHoverEvent(event);
+ mTooltipHoverTarget = null;
+ } else if (mTooltipHoveredSelf) {
+ super.dispatchTooltipHoverEvent(event);
+ mTooltipHoveredSelf = false;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private boolean dispatchTooltipHoverEvent(MotionEvent event, View child) {
+ final boolean result;
+ if (!child.hasIdentityMatrix()) {
+ MotionEvent transformedEvent = getTransformedMotionEvent(event, child);
+ result = child.dispatchTooltipHoverEvent(transformedEvent);
+ transformedEvent.recycle();
+ } else {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ event.offsetLocation(offsetX, offsetY);
+ result = child.dispatchTooltipHoverEvent(event);
+ event.offsetLocation(-offsetX, -offsetY);
+ }
+ return result;
+ }
+
+ private void exitTooltipHoverTargets() {
+ if (mTooltipHoveredSelf || mTooltipHoverTarget != null) {
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_HOVER_EXIT, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ dispatchTooltipHoverEvent(event);
+ event.recycle();
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected boolean hasHoveredChild() {
+ return mFirstHoverTarget != null;
+ }
+
+ /** @hide */
+ @Override
+ protected boolean pointInHoveredChild(MotionEvent event) {
+ if (mFirstHoverTarget != null) {
+ return isTransformedTouchPointInView(event.getX(), event.getY(),
+ mFirstHoverTarget.child, null);
+ }
+ return false;
+ }
+
+ @Override
+ public void addChildrenForAccessibility(ArrayList<View> outChildren) {
+ if (getAccessibilityNodeProvider() != null) {
+ return;
+ }
+ ChildListForAccessibility children = ChildListForAccessibility.obtain(this, true);
+ try {
+ final int childrenCount = children.getChildCount();
+ for (int i = 0; i < childrenCount; i++) {
+ View child = children.getChildAt(i);
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ if (child.includeForAccessibility()) {
+ outChildren.add(child);
+ } else {
+ child.addChildrenForAccessibility(outChildren);
+ }
+ }
+ }
+ } finally {
+ children.recycle();
+ }
+ }
+
+ /**
+ * Implement this method to intercept hover events before they are handled
+ * by child views.
+ * <p>
+ * This method is called before dispatching a hover event to a child of
+ * the view group or to the view group's own {@link #onHoverEvent} to allow
+ * the view group a chance to intercept the hover event.
+ * This method can also be used to watch all pointer motions that occur within
+ * the bounds of the view group even when the pointer is hovering over
+ * a child of the view group rather than over the view group itself.
+ * </p><p>
+ * The view group can prevent its children from receiving hover events by
+ * implementing this method and returning <code>true</code> to indicate
+ * that it would like to intercept hover events. The view group must
+ * continuously return <code>true</code> from {@link #onInterceptHoverEvent}
+ * for as long as it wishes to continue intercepting hover events from
+ * its children.
+ * </p><p>
+ * Interception preserves the invariant that at most one view can be
+ * hovered at a time by transferring hover focus from the currently hovered
+ * child to the view group or vice-versa as needed.
+ * </p><p>
+ * If this method returns <code>true</code> and a child is already hovered, then the
+ * child view will first receive a hover exit event and then the view group
+ * itself will receive a hover enter event in {@link #onHoverEvent}.
+ * Likewise, if this method had previously returned <code>true</code> to intercept hover
+ * events and instead returns <code>false</code> while the pointer is hovering
+ * within the bounds of one of a child, then the view group will first receive a
+ * hover exit event in {@link #onHoverEvent} and then the hovered child will
+ * receive a hover enter event.
+ * </p><p>
+ * The default implementation handles mouse hover on the scroll bars.
+ * </p>
+ *
+ * @param event The motion event that describes the hover.
+ * @return True if the view group would like to intercept the hover event
+ * and prevent its children from receiving it.
+ */
+ public boolean onInterceptHoverEvent(MotionEvent event) {
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ final int action = event.getAction();
+ final float x = event.getX();
+ final float y = event.getY();
+ if ((action == MotionEvent.ACTION_HOVER_MOVE
+ || action == MotionEvent.ACTION_HOVER_ENTER) && isOnScrollbar(x, y)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static MotionEvent obtainMotionEventNoHistoryOrSelf(MotionEvent event) {
+ if (event.getHistorySize() == 0) {
+ return event;
+ }
+ return MotionEvent.obtainNoHistory(event);
+ }
+
+ @Override
+ protected boolean dispatchGenericPointerEvent(MotionEvent event) {
+ // Send the event to the child under the pointer.
+ final int childrenCount = mChildrenCount;
+ if (childrenCount != 0) {
+ final float x = event.getX();
+ final float y = event.getY();
+
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
+ if (!child.canReceivePointerEvents()
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ continue;
+ }
+
+ if (dispatchTransformedGenericPointerEvent(event, child)) {
+ if (preorderedList != null) preorderedList.clear();
+ return true;
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+
+ // No child handled the event. Send it to this view group.
+ return super.dispatchGenericPointerEvent(event);
+ }
+
+ @Override
+ protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
+ // Send the event to the focused child or to this view group if it has focus.
+ if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
+ == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
+ return super.dispatchGenericFocusedEvent(event);
+ } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
+ == PFLAG_HAS_BOUNDS) {
+ return mFocused.dispatchGenericMotionEvent(event);
+ }
+ return false;
+ }
+
+ /**
+ * Dispatches a generic pointer event to a child, taking into account
+ * transformations that apply to the child.
+ *
+ * @param event The event to send.
+ * @param child The view to send the event to.
+ * @return {@code true} if the child handled the event.
+ */
+ private boolean dispatchTransformedGenericPointerEvent(MotionEvent event, View child) {
+ boolean handled;
+ if (!child.hasIdentityMatrix()) {
+ MotionEvent transformedEvent = getTransformedMotionEvent(event, child);
+ handled = child.dispatchGenericMotionEvent(transformedEvent);
+ transformedEvent.recycle();
+ } else {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ event.offsetLocation(offsetX, offsetY);
+ handled = child.dispatchGenericMotionEvent(event);
+ event.offsetLocation(-offsetX, -offsetY);
+ }
+ return handled;
+ }
+
+ /**
+ * Returns a MotionEvent that's been transformed into the child's local coordinates.
+ *
+ * It's the responsibility of the caller to recycle it once they're finished with it.
+ * @param event The event to transform.
+ * @param child The view whose coordinate space is to be used.
+ * @return A copy of the the given MotionEvent, transformed into the given View's coordinate
+ * space.
+ */
+ private MotionEvent getTransformedMotionEvent(MotionEvent event, View child) {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ final MotionEvent transformedEvent = MotionEvent.obtain(event);
+ transformedEvent.offsetLocation(offsetX, offsetY);
+ if (!child.hasIdentityMatrix()) {
+ transformedEvent.transform(child.getInverseMatrix());
+ }
+ return transformedEvent;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
+ }
+
+ // If the event targets the accessibility focused view and this is it, start
+ // normal event dispatch. Maybe a descendant is what will handle the click.
+ if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
+ ev.setTargetAccessibilityFocus(false);
+ }
+
+ boolean handled = false;
+ if (onFilterTouchEventForSecurity(ev)) {
+ final int action = ev.getAction();
+ final int actionMasked = action & MotionEvent.ACTION_MASK;
+
+ // Handle an initial down.
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ // Throw away all previous state when starting a new touch gesture.
+ // The framework may have dropped the up or cancel event for the previous gesture
+ // due to an app switch, ANR, or some other state change.
+ cancelAndClearTouchTargets(ev);
+ resetTouchState();
+ }
+
+ // Check for interception.
+ final boolean intercepted;
+ if (actionMasked == MotionEvent.ACTION_DOWN
+ || mFirstTouchTarget != null) {
+ final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
+ if (!disallowIntercept) {
+ intercepted = onInterceptTouchEvent(ev);
+ ev.setAction(action); // restore action in case it was changed
+ } else {
+ intercepted = false;
+ }
+ } else {
+ // There are no touch targets and this action is not an initial down
+ // so this view group continues to intercept touches.
+ intercepted = true;
+ }
+
+ // If intercepted, start normal event dispatch. Also if there is already
+ // a view that is handling the gesture, do normal event dispatch.
+ if (intercepted || mFirstTouchTarget != null) {
+ ev.setTargetAccessibilityFocus(false);
+ }
+
+ // Check for cancelation.
+ final boolean canceled = resetCancelNextUpFlag(this)
+ || actionMasked == MotionEvent.ACTION_CANCEL;
+
+ // Update list of touch targets for pointer down, if needed.
+ final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
+ final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
+ && !isMouseEvent;
+ TouchTarget newTouchTarget = null;
+ boolean alreadyDispatchedToNewTouchTarget = false;
+ if (!canceled && !intercepted) {
+ // If the event is targeting accessibility focus we give it to the
+ // view that has accessibility focus and if it does not handle it
+ // we clear the flag and dispatch the event to all children as usual.
+ // We are looking up the accessibility focused host to avoid keeping
+ // state since these events are very rare.
+ View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
+ ? findChildWithAccessibilityFocus() : null;
+
+ if (actionMasked == MotionEvent.ACTION_DOWN
+ || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
+ || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
+ final int actionIndex = ev.getActionIndex(); // always 0 for down
+ final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
+ : TouchTarget.ALL_POINTER_IDS;
+
+ // Clean up earlier touch targets for this pointer id in case they
+ // have become out of sync.
+ removePointersFromTouchTargets(idBitsToAssign);
+
+ final int childrenCount = mChildrenCount;
+ if (newTouchTarget == null && childrenCount != 0) {
+ final float x =
+ isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
+ final float y =
+ isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
+ // Find a child that can receive the event.
+ // Scan children from front to back.
+ final ArrayList<View> preorderedList = buildTouchDispatchChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = getAndVerifyPreorderedIndex(
+ childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(
+ preorderedList, children, childIndex);
+
+ // If there is a view that has accessibility focus we want it
+ // to get the event first and if not handled we will perform a
+ // normal dispatch. We may do a double iteration but this is
+ // safer given the timeframe.
+ if (childWithAccessibilityFocus != null) {
+ if (childWithAccessibilityFocus != child) {
+ continue;
+ }
+ childWithAccessibilityFocus = null;
+ i = childrenCount - 1;
+ }
+
+ if (!child.canReceivePointerEvents()
+ || !isTransformedTouchPointInView(x, y, child, null)) {
+ ev.setTargetAccessibilityFocus(false);
+ continue;
+ }
+
+ newTouchTarget = getTouchTarget(child);
+ if (newTouchTarget != null) {
+ // Child is already receiving touch within its bounds.
+ // Give it the new pointer in addition to the ones it is handling.
+ newTouchTarget.pointerIdBits |= idBitsToAssign;
+ break;
+ }
+
+ resetCancelNextUpFlag(child);
+ if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
+ // Child wants to receive touch within its bounds.
+ mLastTouchDownTime = ev.getDownTime();
+ if (preorderedList != null) {
+ // childIndex points into presorted list, find original index
+ for (int j = 0; j < childrenCount; j++) {
+ if (children[childIndex] == mChildren[j]) {
+ mLastTouchDownIndex = j;
+ break;
+ }
+ }
+ } else {
+ mLastTouchDownIndex = childIndex;
+ }
+ mLastTouchDownX = ev.getX();
+ mLastTouchDownY = ev.getY();
+ newTouchTarget = addTouchTarget(child, idBitsToAssign);
+ alreadyDispatchedToNewTouchTarget = true;
+ break;
+ }
+
+ // The accessibility focus didn't handle the event, so clear
+ // the flag and do a normal dispatch to all children.
+ ev.setTargetAccessibilityFocus(false);
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+
+ if (newTouchTarget == null && mFirstTouchTarget != null) {
+ // Did not find a child to receive the event.
+ // Assign the pointer to the least recently added target.
+ newTouchTarget = mFirstTouchTarget;
+ while (newTouchTarget.next != null) {
+ newTouchTarget = newTouchTarget.next;
+ }
+ newTouchTarget.pointerIdBits |= idBitsToAssign;
+ }
+ }
+ }
+
+ // Dispatch to touch targets.
+ if (mFirstTouchTarget == null) {
+ // No touch targets so treat this as an ordinary view.
+ handled = dispatchTransformedTouchEvent(ev, canceled, null,
+ TouchTarget.ALL_POINTER_IDS);
+ } else {
+ // Dispatch to touch targets, excluding the new touch target if we already
+ // dispatched to it. Cancel touch targets if necessary.
+ TouchTarget predecessor = null;
+ TouchTarget target = mFirstTouchTarget;
+ while (target != null) {
+ final TouchTarget next = target.next;
+ if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
+ handled = true;
+ } else {
+ final boolean cancelChild = resetCancelNextUpFlag(target.child)
+ || intercepted;
+ if (dispatchTransformedTouchEvent(ev, cancelChild,
+ target.child, target.pointerIdBits)) {
+ handled = true;
+ }
+ if (cancelChild) {
+ if (predecessor == null) {
+ mFirstTouchTarget = next;
+ } else {
+ predecessor.next = next;
+ }
+ target.recycle();
+ target = next;
+ continue;
+ }
+ }
+ predecessor = target;
+ target = next;
+ }
+ }
+
+ // Update list of touch targets for pointer up or cancel, if needed.
+ if (canceled
+ || actionMasked == MotionEvent.ACTION_UP
+ || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
+ resetTouchState();
+ } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
+ final int actionIndex = ev.getActionIndex();
+ final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
+ removePointersFromTouchTargets(idBitsToRemove);
+ }
+ }
+
+ if (!handled && mInputEventConsistencyVerifier != null) {
+ mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
+ }
+ return handled;
+ }
+
+ /**
+ * Provide custom ordering of views in which the touch will be dispatched.
+ *
+ * This is called within a tight loop, so you are not allowed to allocate objects, including
+ * the return array. Instead, you should return a pre-allocated list that will be cleared
+ * after the dispatch is finished.
+ * @hide
+ */
+ public ArrayList<View> buildTouchDispatchChildList() {
+ return buildOrderedChildList();
+ }
+
+ /**
+ * Finds the child which has accessibility focus.
+ *
+ * @return The child that has focus.
+ */
+ private View findChildWithAccessibilityFocus() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot == null) {
+ return null;
+ }
+
+ View current = viewRoot.getAccessibilityFocusedHost();
+ if (current == null) {
+ return null;
+ }
+
+ ViewParent parent = current.getParent();
+ while (parent instanceof View) {
+ if (parent == this) {
+ return current;
+ }
+ current = (View) parent;
+ parent = current.getParent();
+ }
+
+ return null;
+ }
+
+ /**
+ * Resets all touch state in preparation for a new cycle.
+ */
+ private void resetTouchState() {
+ clearTouchTargets();
+ resetCancelNextUpFlag(this);
+ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
+ mNestedScrollAxes = SCROLL_AXIS_NONE;
+ }
+
+ /**
+ * Resets the cancel next up flag.
+ * Returns true if the flag was previously set.
+ */
+ private static boolean resetCancelNextUpFlag(@NonNull View view) {
+ if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
+ view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Clears all touch targets.
+ */
+ private void clearTouchTargets() {
+ TouchTarget target = mFirstTouchTarget;
+ if (target != null) {
+ do {
+ TouchTarget next = target.next;
+ target.recycle();
+ target = next;
+ } while (target != null);
+ mFirstTouchTarget = null;
+ }
+ }
+
+ /**
+ * Cancels and clears all touch targets.
+ */
+ private void cancelAndClearTouchTargets(MotionEvent event) {
+ if (mFirstTouchTarget != null) {
+ boolean syntheticEvent = false;
+ if (event == null) {
+ final long now = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ syntheticEvent = true;
+ }
+
+ for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
+ resetCancelNextUpFlag(target.child);
+ dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
+ }
+ clearTouchTargets();
+
+ if (syntheticEvent) {
+ event.recycle();
+ }
+ }
+ }
+
+ /**
+ * Gets the touch target for specified child view.
+ * Returns null if not found.
+ */
+ private TouchTarget getTouchTarget(@NonNull View child) {
+ for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
+ if (target.child == child) {
+ return target;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a touch target for specified child to the beginning of the list.
+ * Assumes the target child is not already present.
+ */
+ private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
+ final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
+ target.next = mFirstTouchTarget;
+ mFirstTouchTarget = target;
+ return target;
+ }
+
+ /**
+ * Removes the pointer ids from consideration.
+ */
+ private void removePointersFromTouchTargets(int pointerIdBits) {
+ TouchTarget predecessor = null;
+ TouchTarget target = mFirstTouchTarget;
+ while (target != null) {
+ final TouchTarget next = target.next;
+ if ((target.pointerIdBits & pointerIdBits) != 0) {
+ target.pointerIdBits &= ~pointerIdBits;
+ if (target.pointerIdBits == 0) {
+ if (predecessor == null) {
+ mFirstTouchTarget = next;
+ } else {
+ predecessor.next = next;
+ }
+ target.recycle();
+ target = next;
+ continue;
+ }
+ }
+ predecessor = target;
+ target = next;
+ }
+ }
+
+ @UnsupportedAppUsage
+ private void cancelTouchTarget(View view) {
+ TouchTarget predecessor = null;
+ TouchTarget target = mFirstTouchTarget;
+ while (target != null) {
+ final TouchTarget next = target.next;
+ if (target.child == view) {
+ if (predecessor == null) {
+ mFirstTouchTarget = next;
+ } else {
+ predecessor.next = next;
+ }
+ target.recycle();
+
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ view.dispatchTouchEvent(event);
+ event.recycle();
+ return;
+ }
+ predecessor = target;
+ target = next;
+ }
+ }
+
+ private Rect getTempRect() {
+ if (mTempRect == null) {
+ mTempRect = new Rect();
+ }
+ return mTempRect;
+ }
+
+ private float[] getTempLocationF() {
+ if (mTempPosition == null) {
+ mTempPosition = new float[2];
+ }
+ return mTempPosition;
+ }
+
+ private Point getTempPoint() {
+ if (mTempPoint == null) {
+ mTempPoint = new Point();
+ }
+ return mTempPoint;
+ }
+
+ /**
+ * Returns true if a child view contains the specified point when transformed
+ * into its coordinate space.
+ * Child must not be null.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected boolean isTransformedTouchPointInView(float x, float y, View child,
+ PointF outLocalPoint) {
+ final float[] point = getTempLocationF();
+ point[0] = x;
+ point[1] = y;
+ transformPointToViewLocal(point, child);
+ final boolean isInView = child.pointInView(point[0], point[1]);
+ if (isInView && outLocalPoint != null) {
+ outLocalPoint.set(point[0], point[1]);
+ }
+ return isInView;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void transformPointToViewLocal(float[] point, View child) {
+ point[0] += mScrollX - child.mLeft;
+ point[1] += mScrollY - child.mTop;
+
+ if (!child.hasIdentityMatrix()) {
+ child.getInverseMatrix().mapPoints(point);
+ }
+ }
+
+ /**
+ * Transforms a motion event into the coordinate space of a particular child view,
+ * filters out irrelevant pointer ids, and overrides its action if necessary.
+ * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
+ */
+ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
+ View child, int desiredPointerIdBits) {
+ final boolean handled;
+
+ // Canceling motions is a special case. We don't need to perform any transformations
+ // or filtering. The important part is the action, not the contents.
+ final int oldAction = event.getAction();
+ if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
+ event.setAction(MotionEvent.ACTION_CANCEL);
+ if (child == null) {
+ handled = super.dispatchTouchEvent(event);
+ } else {
+ handled = child.dispatchTouchEvent(event);
+ }
+ event.setAction(oldAction);
+ return handled;
+ }
+
+ // Calculate the number of pointers to deliver.
+ final int oldPointerIdBits = event.getPointerIdBits();
+ final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
+
+ // If for some reason we ended up in an inconsistent state where it looks like we
+ // might produce a motion event with no pointers in it, then drop the event.
+ if (newPointerIdBits == 0) {
+ return false;
+ }
+
+ // If the number of pointers is the same and we don't need to perform any fancy
+ // irreversible transformations, then we can reuse the motion event for this
+ // dispatch as long as we are careful to revert any changes we make.
+ // Otherwise we need to make a copy.
+ final MotionEvent transformedEvent;
+ if (newPointerIdBits == oldPointerIdBits) {
+ if (child == null || child.hasIdentityMatrix()) {
+ if (child == null) {
+ handled = super.dispatchTouchEvent(event);
+ } else {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ event.offsetLocation(offsetX, offsetY);
+
+ handled = child.dispatchTouchEvent(event);
+
+ event.offsetLocation(-offsetX, -offsetY);
+ }
+ return handled;
+ }
+ transformedEvent = MotionEvent.obtain(event);
+ } else {
+ transformedEvent = event.split(newPointerIdBits);
+ }
+
+ // Perform any necessary transformations and dispatch.
+ if (child == null) {
+ handled = super.dispatchTouchEvent(transformedEvent);
+ } else {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ transformedEvent.offsetLocation(offsetX, offsetY);
+ if (! child.hasIdentityMatrix()) {
+ transformedEvent.transform(child.getInverseMatrix());
+ }
+
+ handled = child.dispatchTouchEvent(transformedEvent);
+ }
+
+ // Done.
+ transformedEvent.recycle();
+ return handled;
+ }
+
+ /**
+ * Enable or disable the splitting of MotionEvents to multiple children during touch event
+ * dispatch. This behavior is enabled by default for applications that target an
+ * SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer.
+ *
+ * <p>When this option is enabled MotionEvents may be split and dispatched to different child
+ * views depending on where each pointer initially went down. This allows for user interactions
+ * such as scrolling two panes of content independently, chording of buttons, and performing
+ * independent gestures on different pieces of content.
+ *
+ * @param split <code>true</code> to allow MotionEvents to be split and dispatched to multiple
+ * child views. <code>false</code> to only allow one child view to be the target of
+ * any MotionEvent received by this ViewGroup.
+ * @attr ref android.R.styleable#ViewGroup_splitMotionEvents
+ */
+ public void setMotionEventSplittingEnabled(boolean split) {
+ // TODO Applications really shouldn't change this setting mid-touch event,
+ // but perhaps this should handle that case and send ACTION_CANCELs to any child views
+ // with gestures in progress when this is changed.
+ if (split) {
+ mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
+ } else {
+ mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
+ }
+ }
+
+ /**
+ * Returns true if MotionEvents dispatched to this ViewGroup can be split to multiple children.
+ * @return true if MotionEvents dispatched to this ViewGroup can be split to multiple children.
+ */
+ @InspectableProperty(name = "splitMotionEvents")
+ public boolean isMotionEventSplittingEnabled() {
+ return (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) == FLAG_SPLIT_MOTION_EVENTS;
+ }
+
+ /**
+ * Returns true if this ViewGroup should be considered as a single entity for removal
+ * when executing an Activity transition. If this is false, child elements will move
+ * individually during the transition.
+ *
+ * @return True if the ViewGroup should be acted on together during an Activity transition.
+ * The default value is true when there is a non-null background or if
+ * {@link #getTransitionName()} is not null or if a
+ * non-null {@link android.view.ViewOutlineProvider} other than
+ * {@link android.view.ViewOutlineProvider#BACKGROUND} was given to
+ * {@link #setOutlineProvider(ViewOutlineProvider)} and false otherwise.
+ */
+ @InspectableProperty
+ public boolean isTransitionGroup() {
+ if ((mGroupFlags & FLAG_IS_TRANSITION_GROUP_SET) != 0) {
+ return ((mGroupFlags & FLAG_IS_TRANSITION_GROUP) != 0);
+ } else {
+ final ViewOutlineProvider outlineProvider = getOutlineProvider();
+ return getBackground() != null || getTransitionName() != null ||
+ (outlineProvider != null && outlineProvider != ViewOutlineProvider.BACKGROUND);
+ }
+ }
+
+ /**
+ * Changes whether or not this ViewGroup should be treated as a single entity during
+ * Activity Transitions.
+ * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit
+ * in Activity transitions. If false, the ViewGroup won't transition,
+ * only its children. If true, the entire ViewGroup will transition
+ * together.
+ * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.app.Activity,
+ * android.util.Pair[])
+ */
+ public void setTransitionGroup(boolean isTransitionGroup) {
+ mGroupFlags |= FLAG_IS_TRANSITION_GROUP_SET;
+ if (isTransitionGroup) {
+ mGroupFlags |= FLAG_IS_TRANSITION_GROUP;
+ } else {
+ mGroupFlags &= ~FLAG_IS_TRANSITION_GROUP;
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+
+ if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
+ // We're already in this state, assume our ancestors are too
+ return;
+ }
+
+ if (disallowIntercept) {
+ mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
+ } else {
+ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
+ }
+
+ // Pass it up to our parent
+ if (mParent != null) {
+ mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+
+ /**
+ * Implement this method to intercept all touch screen motion events. This
+ * allows you to watch events as they are dispatched to your children, and
+ * take ownership of the current gesture at any point.
+ *
+ * <p>Using this function takes some care, as it has a fairly complicated
+ * interaction with {@link View#onTouchEvent(MotionEvent)
+ * View.onTouchEvent(MotionEvent)}, and using it requires implementing
+ * that method as well as this one in the correct way. Events will be
+ * received in the following order:
+ *
+ * <ol>
+ * <li> You will receive the down event here.
+ * <li> The down event will be handled either by a child of this view
+ * group, or given to your own onTouchEvent() method to handle; this means
+ * you should implement onTouchEvent() to return true, so you will
+ * continue to see the rest of the gesture (instead of looking for
+ * a parent view to handle it). Also, by returning true from
+ * onTouchEvent(), you will not receive any following
+ * events in onInterceptTouchEvent() and all touch processing must
+ * happen in onTouchEvent() like normal.
+ * <li> For as long as you return false from this function, each following
+ * event (up to and including the final up) will be delivered first here
+ * and then to the target's onTouchEvent().
+ * <li> If you return true from here, you will not receive any
+ * following events: the target view will receive the same event but
+ * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
+ * events will be delivered to your onTouchEvent() method and no longer
+ * appear here.
+ * </ol>
+ *
+ * @param ev The motion event being dispatched down the hierarchy.
+ * @return Return true to steal motion events from the children and have
+ * them dispatched to this ViewGroup through onTouchEvent().
+ * The current target will receive an ACTION_CANCEL event, and no further
+ * messages will be delivered here.
+ */
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
+ && ev.getAction() == MotionEvent.ACTION_DOWN
+ && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
+ && isOnScrollbarThumb(ev.getX(), ev.getY())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Looks for a view to give focus to respecting the setting specified by
+ * {@link #getDescendantFocusability()}.
+ *
+ * Uses {@link #onRequestFocusInDescendants(int, android.graphics.Rect)} to
+ * find focus within the children of this group when appropriate.
+ *
+ * @see #FOCUS_BEFORE_DESCENDANTS
+ * @see #FOCUS_AFTER_DESCENDANTS
+ * @see #FOCUS_BLOCK_DESCENDANTS
+ * @see #onRequestFocusInDescendants(int, android.graphics.Rect)
+ */
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ if (DBG) {
+ System.out.println(this + " ViewGroup.requestFocus direction="
+ + direction);
+ }
+ int descendantFocusability = getDescendantFocusability();
+
+ boolean result;
+ switch (descendantFocusability) {
+ case FOCUS_BLOCK_DESCENDANTS:
+ result = super.requestFocus(direction, previouslyFocusedRect);
+ break;
+ case FOCUS_BEFORE_DESCENDANTS: {
+ final boolean took = super.requestFocus(direction, previouslyFocusedRect);
+ result = took ? took : onRequestFocusInDescendants(direction,
+ previouslyFocusedRect);
+ break;
+ }
+ case FOCUS_AFTER_DESCENDANTS: {
+ final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
+ break;
+ }
+ default:
+ throw new IllegalStateException("descendant focusability must be "
+ + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ + "but is " + descendantFocusability);
+ }
+ if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
+ mPrivateFlags |= PFLAG_WANTS_FOCUS;
+ }
+ return result;
+ }
+
+ /**
+ * Look for a descendant to call {@link View#requestFocus} on.
+ * Called by {@link ViewGroup#requestFocus(int, android.graphics.Rect)}
+ * when it wants to request focus within its children. Override this to
+ * customize how your {@link ViewGroup} requests focus within its children.
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @param previouslyFocusedRect The rectangle (in this View's coordinate system)
+ * to give a finer grained hint about where focus is coming from. May be null
+ * if there is no hint.
+ * @return Whether focus was taken.
+ */
+ @SuppressWarnings({"ConstantConditions"})
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+ int index;
+ int increment;
+ int end;
+ int count = mChildrenCount;
+ if ((direction & FOCUS_FORWARD) != 0) {
+ index = 0;
+ increment = 1;
+ end = count;
+ } else {
+ index = count - 1;
+ increment = -1;
+ end = -1;
+ }
+ final View[] children = mChildren;
+ for (int i = index; i != end; i += increment) {
+ View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ if (child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean restoreDefaultFocus() {
+ if (mDefaultFocus != null
+ && getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS
+ && (mDefaultFocus.mViewFlags & VISIBILITY_MASK) == VISIBLE
+ && mDefaultFocus.restoreDefaultFocus()) {
+ return true;
+ }
+ return super.restoreDefaultFocus();
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ @Override
+ public boolean restoreFocusInCluster(@FocusRealDirection int direction) {
+ // Allow cluster-navigation to enter touchscreenBlocksFocus ViewGroups.
+ if (isKeyboardNavigationCluster()) {
+ final boolean blockedFocus = getTouchscreenBlocksFocus();
+ try {
+ setTouchscreenBlocksFocusNoRefocus(false);
+ return restoreFocusInClusterInternal(direction);
+ } finally {
+ setTouchscreenBlocksFocusNoRefocus(blockedFocus);
+ }
+ } else {
+ return restoreFocusInClusterInternal(direction);
+ }
+ }
+
+ private boolean restoreFocusInClusterInternal(@FocusRealDirection int direction) {
+ if (mFocusedInCluster != null && getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS
+ && (mFocusedInCluster.mViewFlags & VISIBILITY_MASK) == VISIBLE
+ && mFocusedInCluster.restoreFocusInCluster(direction)) {
+ return true;
+ }
+ return super.restoreFocusInCluster(direction);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean restoreFocusNotInCluster() {
+ if (mFocusedInCluster != null) {
+ // since clusters don't nest; we can assume that a non-null mFocusedInCluster
+ // will refer to a view not-in a cluster.
+ return restoreFocusInCluster(View.FOCUS_DOWN);
+ }
+ if (isKeyboardNavigationCluster() || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+ int descendentFocusability = getDescendantFocusability();
+ if (descendentFocusability == FOCUS_BLOCK_DESCENDANTS) {
+ return super.requestFocus(FOCUS_DOWN, null);
+ }
+ if (descendentFocusability == FOCUS_BEFORE_DESCENDANTS
+ && super.requestFocus(FOCUS_DOWN, null)) {
+ return true;
+ }
+ for (int i = 0; i < mChildrenCount; ++i) {
+ View child = mChildren[i];
+ if (!child.isKeyboardNavigationCluster()
+ && child.restoreFocusNotInCluster()) {
+ return true;
+ }
+ }
+ if (descendentFocusability == FOCUS_AFTER_DESCENDANTS && !hasFocusableChild(false)) {
+ return super.requestFocus(FOCUS_DOWN, null);
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ public void dispatchStartTemporaryDetach() {
+ super.dispatchStartTemporaryDetach();
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchStartTemporaryDetach();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ public void dispatchFinishTemporaryDetach() {
+ super.dispatchFinishTemporaryDetach();
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchFinishTemporaryDetach();
+ }
+ }
+
+ @Override
+ @UnsupportedAppUsage
+ void dispatchAttachedToWindow(AttachInfo info, int visibility) {
+ mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
+ super.dispatchAttachedToWindow(info, visibility);
+ mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ child.dispatchAttachedToWindow(info,
+ combineVisibility(visibility, child.getVisibility()));
+ }
+ final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
+ for (int i = 0; i < transientCount; ++i) {
+ View view = mTransientViews.get(i);
+ view.dispatchAttachedToWindow(info,
+ combineVisibility(visibility, view.getVisibility()));
+ }
+ }
+
+ @Override
+ void dispatchScreenStateChanged(int screenState) {
+ super.dispatchScreenStateChanged(screenState);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchScreenStateChanged(screenState);
+ }
+ }
+
+ @Override
+ void dispatchMovedToDisplay(Display display, Configuration config) {
+ super.dispatchMovedToDisplay(display, config);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchMovedToDisplay(display, config);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ boolean handled = false;
+ if (includeForAccessibility()) {
+ handled = super.dispatchPopulateAccessibilityEventInternal(event);
+ if (handled) {
+ return handled;
+ }
+ }
+ // Let our children have a shot in populating the event.
+ ChildListForAccessibility children = ChildListForAccessibility.obtain(this, true);
+ try {
+ final int childCount = children.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = children.getChildAt(i);
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ handled = child.dispatchPopulateAccessibilityEvent(event);
+ if (handled) {
+ return handled;
+ }
+ }
+ }
+ } finally {
+ children.recycle();
+ }
+ return false;
+ }
+
+ /**
+ * Dispatch creation of {@link ViewStructure} down the hierarchy. This implementation
+ * adds in all child views of the view group, in addition to calling the default View
+ * implementation.
+ */
+ @Override
+ public void dispatchProvideStructure(ViewStructure structure) {
+ super.dispatchProvideStructure(structure);
+ if (isAssistBlocked() || structure.getChildCount() != 0) {
+ return;
+ }
+ final int childrenCount = mChildrenCount;
+ if (childrenCount <= 0) {
+ return;
+ }
+
+ if (!isLaidOut()) {
+ if (Helper.sVerbose) {
+ Log.v(VIEW_LOG_TAG, "dispatchProvideStructure(): not laid out, ignoring "
+ + childrenCount + " children of " + getAccessibilityViewId());
+ }
+ return;
+ }
+
+ structure.setChildCount(childrenCount);
+ ArrayList<View> tempPreorderedList = buildOrderedChildList();
+ ArrayList<View> preorderedList =
+ tempPreorderedList != null ? new ArrayList<>(tempPreorderedList) : null;
+ boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ for (int i = 0; i < childrenCount; i++) {
+ int childIndex;
+ try {
+ childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ } catch (IndexOutOfBoundsException e) {
+ childIndex = i;
+ if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M) {
+ Log.w(TAG, "Bad getChildDrawingOrder while collecting assist @ "
+ + i + " of " + childrenCount, e);
+ // At least one app is failing when we call getChildDrawingOrder
+ // at this point, so deal semi-gracefully with it by falling back
+ // on the basic order.
+ customOrder = false;
+ if (i > 0) {
+ // If we failed at the first index, there really isn't
+ // anything to do -- we will just proceed with the simple
+ // sequence order.
+ // Otherwise, we failed in the middle, so need to come up
+ // with an order for the remaining indices and use that.
+ // Failed at the first one, easy peasy.
+ int[] permutation = new int[childrenCount];
+ SparseBooleanArray usedIndices = new SparseBooleanArray();
+ // Go back and collected the indices we have done so far.
+ for (int j = 0; j < i; j++) {
+ permutation[j] = getChildDrawingOrder(childrenCount, j);
+ usedIndices.put(permutation[j], true);
+ }
+ // Fill in the remaining indices with indices that have not
+ // yet been used.
+ int nextIndex = 0;
+ for (int j = i; j < childrenCount; j++) {
+ while (usedIndices.get(nextIndex, false)) {
+ nextIndex++;
+ }
+ permutation[j] = nextIndex;
+ nextIndex++;
+ }
+ // Build the final view list.
+ preorderedList = new ArrayList<>(childrenCount);
+ for (int j = 0; j < childrenCount; j++) {
+ final int index = permutation[j];
+ final View child = mChildren[index];
+ preorderedList.add(child);
+ }
+ }
+ } else {
+ throw e;
+ }
+ }
+ final View child = getAndVerifyPreorderedView(preorderedList, mChildren,
+ childIndex);
+ final ViewStructure cstructure = structure.newChild(i);
+ child.dispatchProvideStructure(cstructure);
+ }
+ if (preorderedList != null) {
+ preorderedList.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation adds in all child views of the view group, in addition to calling the
+ * default {@link View} implementation.
+ */
+ @Override
+ public void dispatchProvideAutofillStructure(ViewStructure structure,
+ @AutofillFlags int flags) {
+ super.dispatchProvideAutofillStructure(structure, flags);
+
+ if (structure.getChildCount() != 0) {
+ return;
+ }
+
+ if (!isLaidOut()) {
+ if (Helper.sVerbose) {
+ Log.v(VIEW_LOG_TAG, "dispatchProvideAutofillStructure(): not laid out, ignoring "
+ + mChildrenCount + " children of " + getAutofillId());
+ }
+ return;
+ }
+
+ final ChildListForAutoFillOrContentCapture children = getChildrenForAutofill(flags);
+ final int childrenCount = children.size();
+ structure.setChildCount(childrenCount);
+ for (int i = 0; i < childrenCount; i++) {
+ final View child = children.get(i);
+ final ViewStructure cstructure = structure.newChild(i);
+ child.dispatchProvideAutofillStructure(cstructure, flags);
+ }
+ children.recycle();
+ }
+
+ /** @hide */
+ @Override
+ public void dispatchProvideContentCaptureStructure() {
+ super.dispatchProvideContentCaptureStructure();
+
+ if (!isLaidOut()) return;
+
+ final ChildListForAutoFillOrContentCapture children = getChildrenForContentCapture();
+ final int childrenCount = children.size();
+ for (int i = 0; i < childrenCount; i++) {
+ final View child = children.get(i);
+ child.dispatchProvideContentCaptureStructure();
+ }
+ children.recycle();
+ }
+
+ /**
+ * Gets the children for autofill. Children for autofill are the first
+ * level descendants that are important for autofill. The returned
+ * child list object is pooled and the caller must recycle it once done.
+ * @hide */
+ private @NonNull ChildListForAutoFillOrContentCapture getChildrenForAutofill(
+ @AutofillFlags int flags) {
+ final ChildListForAutoFillOrContentCapture children = ChildListForAutoFillOrContentCapture
+ .obtain();
+ populateChildrenForAutofill(children, flags);
+ return children;
+ }
+
+ /** @hide */
+ private void populateChildrenForAutofill(ArrayList<View> list, @AutofillFlags int flags) {
+ final int childrenCount = mChildrenCount;
+ if (childrenCount <= 0) {
+ return;
+ }
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ for (int i = 0; i < childrenCount; i++) {
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = (preorderedList == null)
+ ? mChildren[childIndex] : preorderedList.get(childIndex);
+ if ((flags & AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0
+ || child.isImportantForAutofill()) {
+ list.add(child);
+ } else if (child instanceof ViewGroup) {
+ ((ViewGroup) child).populateChildrenForAutofill(list, flags);
+ }
+ }
+ }
+
+ private @NonNull ChildListForAutoFillOrContentCapture getChildrenForContentCapture() {
+ final ChildListForAutoFillOrContentCapture children = ChildListForAutoFillOrContentCapture
+ .obtain();
+ populateChildrenForContentCapture(children);
+ return children;
+ }
+
+ /** @hide */
+ private void populateChildrenForContentCapture(ArrayList<View> list) {
+ final int childrenCount = mChildrenCount;
+ if (childrenCount <= 0) {
+ return;
+ }
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ for (int i = 0; i < childrenCount; i++) {
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = (preorderedList == null)
+ ? mChildren[childIndex] : preorderedList.get(childIndex);
+ if (child.isImportantForContentCapture()) {
+ list.add(child);
+ } else if (child instanceof ViewGroup) {
+ ((ViewGroup) child).populateChildrenForContentCapture(list);
+ }
+ }
+ }
+
+ private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
+ int childIndex) {
+ final View child;
+ if (preorderedList != null) {
+ child = preorderedList.get(childIndex);
+ if (child == null) {
+ throw new RuntimeException("Invalid preorderedList contained null child at index "
+ + childIndex);
+ }
+ } else {
+ child = children[childIndex];
+ }
+ return child;
+ }
+
+ /** @hide */
+ @Override
+ public void resetSubtreeAutofillIds() {
+ super.resetSubtreeAutofillIds();
+ View[] children = mChildren;
+ final int childCount = mChildrenCount;
+ for (int i = 0; i < childCount; i++) {
+ children[i].resetSubtreeAutofillIds();
+ }
+ }
+
+ /** @hide */
+ @Override
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (getAccessibilityNodeProvider() != null) {
+ return;
+ }
+ if (mAttachInfo != null) {
+ final ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList;
+ childrenForAccessibility.clear();
+ addChildrenForAccessibility(childrenForAccessibility);
+ final int childrenForAccessibilityCount = childrenForAccessibility.size();
+ for (int i = 0; i < childrenForAccessibilityCount; i++) {
+ final View child = childrenForAccessibility.get(i);
+ info.addChildUnchecked(child);
+ }
+ childrenForAccessibility.clear();
+ }
+ info.setAvailableExtraData(Collections.singletonList(
+ AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY));
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param info The info to which to add the extra data. Never {@code null}.
+ * @param extraDataKey A key specifying the type of extra data to add to the info. The
+ * extra data should be added to the {@link Bundle} returned by
+ * the info's {@link AccessibilityNodeInfo#getExtras} method. Never
+ * {@code null}.
+ * @param arguments A {@link Bundle} holding any arguments relevant for this request. May be
+ * {@code null} if the service provided no arguments.
+ *
+ */
+ @Override
+ public void addExtraDataToAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info,
+ @NonNull String extraDataKey, @Nullable Bundle arguments) {
+ if (extraDataKey.equals(AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY)) {
+ final AccessibilityNodeInfo.ExtraRenderingInfo extraRenderingInfo =
+ AccessibilityNodeInfo.ExtraRenderingInfo.obtain();
+ extraRenderingInfo.setLayoutSize(getLayoutParams().width, getLayoutParams().height);
+ info.setExtraRenderingInfo(extraRenderingInfo);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ViewGroup.class.getName();
+ }
+
+ @Override
+ public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+ // If this is a live region, we should send a subtree change event
+ // from this view. Otherwise, we can let it propagate up.
+ if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ } else if (mParent != null) {
+ try {
+ mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
+ " does not fully implement ViewParent", e);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
+ return;
+ }
+ // If something important for a11y is happening in this subtree, make sure it's dispatched
+ // from a view that is important for a11y so it doesn't get lost.
+ if ((getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
+ && !isImportantForAccessibility() && (getChildCount() > 0)) {
+ ViewParent a11yParent = getParentForAccessibility();
+ if (a11yParent instanceof View) {
+ ((View) a11yParent).notifySubtreeAccessibilityStateChangedIfNeeded();
+ return;
+ }
+ }
+ super.notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+
+ @Override
+ void resetSubtreeAccessibilityStateChanged() {
+ super.resetSubtreeAccessibilityStateChanged();
+ View[] children = mChildren;
+ final int childCount = mChildrenCount;
+ for (int i = 0; i < childCount; i++) {
+ children[i].resetSubtreeAccessibilityStateChanged();
+ }
+ }
+
+ /**
+ * Counts the number of children of this View that will be sent to an accessibility service.
+ *
+ * @return The number of children an {@code AccessibilityNodeInfo} rooted at this View
+ * would have.
+ */
+ int getNumChildrenForAccessibility() {
+ int numChildrenForAccessibility = 0;
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child.includeForAccessibility()) {
+ numChildrenForAccessibility++;
+ } else if (child instanceof ViewGroup) {
+ numChildrenForAccessibility += ((ViewGroup) child)
+ .getNumChildrenForAccessibility();
+ }
+ }
+ return numChildrenForAccessibility;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Subclasses should always call <code>super.onNestedPrePerformAccessibilityAction</code></p>
+ *
+ * @param target The target view dispatching this action
+ * @param action Action being performed; see
+ * {@link android.view.accessibility.AccessibilityNodeInfo}
+ * @param args Optional action arguments
+ * @return false by default. Subclasses should return true if they handle the event.
+ */
+ @Override
+ public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+ return false;
+ }
+
+ @Override
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ void dispatchDetachedFromWindow() {
+ // If we still have a touch target, we are still in the process of
+ // dispatching motion events to a child; we need to get rid of that
+ // child to avoid dispatching events to it after the window is torn
+ // down. To make sure we keep the child in a consistent state, we
+ // first send it an ACTION_CANCEL motion event.
+ cancelAndClearTouchTargets(null);
+
+ // Similarly, set ACTION_EXIT to all hover targets and clear them.
+ exitHoverTargets();
+ exitTooltipHoverTargets();
+
+ // In case view is detached while transition is running
+ mLayoutCalledWhileSuppressed = false;
+
+ // Tear down our drag tracking
+ mChildrenInterestedInDrag = null;
+ mIsInterestedInDrag = false;
+ if (mCurrentDragStartEvent != null) {
+ mCurrentDragStartEvent.recycle();
+ mCurrentDragStartEvent = null;
+ }
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchDetachedFromWindow();
+ }
+ clearDisappearingChildren();
+ final int transientCount = mTransientViews == null ? 0 : mTransientIndices.size();
+ for (int i = 0; i < transientCount; ++i) {
+ View view = mTransientViews.get(i);
+ view.dispatchDetachedFromWindow();
+ }
+ super.dispatchDetachedFromWindow();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void internalSetPadding(int left, int top, int right, int bottom) {
+ super.internalSetPadding(left, top, right, bottom);
+
+ if ((mPaddingLeft | mPaddingTop | mPaddingRight | mPaddingBottom) != 0) {
+ mGroupFlags |= FLAG_PADDING_NOT_NULL;
+ } else {
+ mGroupFlags &= ~FLAG_PADDING_NOT_NULL;
+ }
+ }
+
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ View c = children[i];
+ if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
+ c.dispatchSaveInstanceState(container);
+ }
+ }
+ }
+
+ /**
+ * Perform dispatching of a {@link #saveHierarchyState(android.util.SparseArray)} freeze()}
+ * to only this view, not to its children. For use when overriding
+ * {@link #dispatchSaveInstanceState(android.util.SparseArray)} dispatchFreeze()} to allow
+ * subclasses to freeze their own state but not the state of their children.
+ *
+ * @param container the container
+ */
+ protected void dispatchFreezeSelfOnly(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ }
+
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchRestoreInstanceState(container);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ View c = children[i];
+ if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
+ c.dispatchRestoreInstanceState(container);
+ }
+ }
+ }
+
+ /**
+ * Perform dispatching of a {@link #restoreHierarchyState(android.util.SparseArray)}
+ * to only this view, not to its children. For use when overriding
+ * {@link #dispatchRestoreInstanceState(android.util.SparseArray)} to allow
+ * subclasses to thaw their own state but not the state of their children.
+ *
+ * @param container the container
+ */
+ protected void dispatchThawSelfOnly(SparseArray<Parcelable> container) {
+ super.dispatchRestoreInstanceState(container);
+ }
+
+ /**
+ * Enables or disables the drawing cache for each child of this view group.
+ *
+ * @param enabled true to enable the cache, false to dispose of it
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ protected void setChildrenDrawingCacheEnabled(boolean enabled) {
+ if (enabled || (mPersistentDrawingCache & PERSISTENT_ALL_CACHES) != PERSISTENT_ALL_CACHES) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].setDrawingCacheEnabled(enabled);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public Bitmap createSnapshot(ViewDebug.CanvasProvider canvasProvider, boolean skipChildren) {
+ int count = mChildrenCount;
+ int[] visibilities = null;
+
+ if (skipChildren) {
+ visibilities = new int[count];
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ visibilities[i] = child.getVisibility();
+ if (visibilities[i] == View.VISIBLE) {
+ child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
+ | (View.INVISIBLE & View.VISIBILITY_MASK);
+ }
+ }
+ }
+
+ try {
+ return super.createSnapshot(canvasProvider, skipChildren);
+ } finally {
+ if (skipChildren) {
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
+ | (visibilities[i] & View.VISIBILITY_MASK);
+ }
+ }
+ }
+ }
+
+ /** Return true if this ViewGroup is laying out using optical bounds. */
+ boolean isLayoutModeOptical() {
+ return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
+ }
+
+ @Override
+ Insets computeOpticalInsets() {
+ if (isLayoutModeOptical()) {
+ int left = 0;
+ int top = 0;
+ int right = 0;
+ int bottom = 0;
+ for (int i = 0; i < mChildrenCount; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ Insets insets = child.getOpticalInsets();
+ left = Math.max(left, insets.left);
+ top = Math.max(top, insets.top);
+ right = Math.max(right, insets.right);
+ bottom = Math.max(bottom, insets.bottom);
+ }
+ }
+ return Insets.of(left, top, right, bottom);
+ } else {
+ return Insets.NONE;
+ }
+ }
+
+ private static void fillRect(Canvas canvas, Paint paint, int x1, int y1, int x2, int y2) {
+ if (x1 != x2 && y1 != y2) {
+ if (x1 > x2) {
+ int tmp = x1; x1 = x2; x2 = tmp;
+ }
+ if (y1 > y2) {
+ int tmp = y1; y1 = y2; y2 = tmp;
+ }
+ canvas.drawRect(x1, y1, x2, y2, paint);
+ }
+ }
+
+ private static int sign(int x) {
+ return (x >= 0) ? 1 : -1;
+ }
+
+ private static void drawCorner(Canvas c, Paint paint, int x1, int y1, int dx, int dy, int lw) {
+ fillRect(c, paint, x1, y1, x1 + dx, y1 + lw * sign(dy));
+ fillRect(c, paint, x1, y1, x1 + lw * sign(dx), y1 + dy);
+ }
+
+ private static void drawRectCorners(Canvas canvas, int x1, int y1, int x2, int y2, Paint paint,
+ int lineLength, int lineWidth) {
+ drawCorner(canvas, paint, x1, y1, lineLength, lineLength, lineWidth);
+ drawCorner(canvas, paint, x1, y2, lineLength, -lineLength, lineWidth);
+ drawCorner(canvas, paint, x2, y1, -lineLength, lineLength, lineWidth);
+ drawCorner(canvas, paint, x2, y2, -lineLength, -lineLength, lineWidth);
+ }
+
+ private static void fillDifference(Canvas canvas,
+ int x2, int y2, int x3, int y3,
+ int dx1, int dy1, int dx2, int dy2, Paint paint) {
+ int x1 = x2 - dx1;
+ int y1 = y2 - dy1;
+
+ int x4 = x3 + dx2;
+ int y4 = y3 + dy2;
+
+ fillRect(canvas, paint, x1, y1, x4, y2);
+ fillRect(canvas, paint, x1, y2, x2, y3);
+ fillRect(canvas, paint, x3, y2, x4, y3);
+ fillRect(canvas, paint, x1, y3, x4, y4);
+ }
+
+ /**
+ * @hide
+ */
+ protected void onDebugDrawMargins(Canvas canvas, Paint paint) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View c = getChildAt(i);
+ c.getLayoutParams().onDebugDraw(c, canvas, paint);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected void onDebugDraw(Canvas canvas) {
+ Paint paint = getDebugPaint();
+
+ // Draw optical bounds
+ {
+ paint.setColor(Color.RED);
+ paint.setStyle(Paint.Style.STROKE);
+
+ for (int i = 0; i < getChildCount(); i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() != View.GONE) {
+ Insets insets = c.getOpticalInsets();
+
+ drawRect(canvas, paint,
+ c.getLeft() + insets.left,
+ c.getTop() + insets.top,
+ c.getRight() - insets.right - 1,
+ c.getBottom() - insets.bottom - 1);
+ }
+ }
+ }
+
+ // Draw margins
+ {
+ paint.setColor(Color.argb(63, 255, 0, 255));
+ paint.setStyle(Paint.Style.FILL);
+
+ onDebugDrawMargins(canvas, paint);
+ }
+
+ // Draw clip bounds
+ {
+ paint.setColor(DEBUG_CORNERS_COLOR);
+ paint.setStyle(Paint.Style.FILL);
+
+ int lineLength = dipsToPixels(DEBUG_CORNERS_SIZE_DIP);
+ int lineWidth = dipsToPixels(1);
+ for (int i = 0; i < getChildCount(); i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() != View.GONE) {
+ drawRectCorners(canvas, c.getLeft(), c.getTop(), c.getRight(), c.getBottom(),
+ paint, lineLength, lineWidth);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ final int childrenCount = mChildrenCount;
+ final View[] children = mChildren;
+ int flags = mGroupFlags;
+
+ if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
+ for (int i = 0; i < childrenCount; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ final LayoutParams params = child.getLayoutParams();
+ attachLayoutAnimationParameters(child, params, i, childrenCount);
+ bindLayoutAnimation(child);
+ }
+ }
+
+ final LayoutAnimationController controller = mLayoutAnimationController;
+ if (controller.willOverlap()) {
+ mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
+ }
+
+ controller.start();
+
+ mGroupFlags &= ~FLAG_RUN_ANIMATION;
+ mGroupFlags &= ~FLAG_ANIMATION_DONE;
+
+ if (mAnimationListener != null) {
+ mAnimationListener.onAnimationStart(controller.getAnimation());
+ }
+ }
+
+ int clipSaveCount = 0;
+ final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+ if (clipToPadding) {
+ clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
+ mScrollX + mRight - mLeft - mPaddingRight,
+ mScrollY + mBottom - mTop - mPaddingBottom);
+ }
+
+ // We will draw our child's animation, let's reset the flag
+ mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
+ mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
+
+ boolean more = false;
+ final long drawingTime = getDrawingTime();
+
+ canvas.enableZ();
+ final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
+ int transientIndex = transientCount != 0 ? 0 : -1;
+ // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
+ // draw reordering internally
+ final ArrayList<View> preorderedList = isHardwareAccelerated()
+ ? null : buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ for (int i = 0; i < childrenCount; i++) {
+ while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
+ final View transientChild = mTransientViews.get(transientIndex);
+ if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
+ transientChild.getAnimation() != null) {
+ more |= drawChild(canvas, transientChild, drawingTime);
+ }
+ transientIndex++;
+ if (transientIndex >= transientCount) {
+ transientIndex = -1;
+ }
+ }
+
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
+ more |= drawChild(canvas, child, drawingTime);
+ }
+ }
+ while (transientIndex >= 0) {
+ // there may be additional transient views after the normal views
+ final View transientChild = mTransientViews.get(transientIndex);
+ if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
+ transientChild.getAnimation() != null) {
+ more |= drawChild(canvas, transientChild, drawingTime);
+ }
+ transientIndex++;
+ if (transientIndex >= transientCount) {
+ break;
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+
+ // Draw any disappearing views that have animations
+ if (mDisappearingChildren != null) {
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ final int disappearingCount = disappearingChildren.size() - 1;
+ // Go backwards -- we may delete as animations finish
+ for (int i = disappearingCount; i >= 0; i--) {
+ final View child = disappearingChildren.get(i);
+ more |= drawChild(canvas, child, drawingTime);
+ }
+ }
+ canvas.disableZ();
+
+ if (isShowingLayoutBounds()) {
+ onDebugDraw(canvas);
+ }
+
+ if (clipToPadding) {
+ canvas.restoreToCount(clipSaveCount);
+ }
+
+ // mGroupFlags might have been updated by drawChild()
+ flags = mGroupFlags;
+
+ if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
+ invalidate(true);
+ }
+
+ if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
+ mLayoutAnimationController.isDone() && !more) {
+ // We want to erase the drawing cache and notify the listener after the
+ // next frame is drawn because one extra invalidate() is caused by
+ // drawChild() after the animation is over
+ mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
+ final Runnable end = new Runnable() {
+ @Override
+ public void run() {
+ notifyAnimationListener();
+ }
+ };
+ post(end);
+ }
+ }
+
+ /**
+ * Returns the ViewGroupOverlay for this view group, creating it if it does
+ * not yet exist. In addition to {@link ViewOverlay}'s support for drawables,
+ * {@link ViewGroupOverlay} allows views to be added to the overlay. These
+ * views, like overlay drawables, are visual-only; they do not receive input
+ * events and should not be used as anything other than a temporary
+ * representation of a view in a parent container, such as might be used
+ * by an animation effect.
+ *
+ * <p>Note: Overlays do not currently work correctly with {@link
+ * SurfaceView} or {@link TextureView}; contents in overlays for these
+ * types of views may not display correctly.</p>
+ *
+ * @return The ViewGroupOverlay object for this view.
+ * @see ViewGroupOverlay
+ */
+ @Override
+ public ViewGroupOverlay getOverlay() {
+ if (mOverlay == null) {
+ mOverlay = new ViewGroupOverlay(mContext, this);
+ }
+ return (ViewGroupOverlay) mOverlay;
+ }
+
+ /**
+ * Converts drawing order position to container position. Override this
+ * if you want to change the drawing order of children. By default, it
+ * returns drawingPosition.
+ * <p>
+ * NOTE: In order for this method to be called, you must enable child ordering
+ * first by calling {@link #setChildrenDrawingOrderEnabled(boolean)}.
+ *
+ * @param drawingPosition the drawing order position.
+ * @return the container position of a child for this drawing order position.
+ *
+ * @see #setChildrenDrawingOrderEnabled(boolean)
+ * @see #isChildrenDrawingOrderEnabled()
+ */
+ protected int getChildDrawingOrder(int childCount, int drawingPosition) {
+ return drawingPosition;
+ }
+
+ /**
+ * Converts drawing order position to container position.
+ * <p>
+ * Children are not necessarily drawn in the order in which they appear in the container.
+ * ViewGroups can enable a custom ordering via {@link #setChildrenDrawingOrderEnabled(boolean)}.
+ * This method returns the container position of a child that appears in the given position
+ * in the current drawing order.
+ *
+ * @param drawingPosition the drawing order position.
+ * @return the container position of a child for this drawing order position.
+ *
+ * @see #getChildDrawingOrder(int, int)}
+ */
+ public final int getChildDrawingOrder(int drawingPosition) {
+ return getChildDrawingOrder(getChildCount(), drawingPosition);
+ }
+
+ private boolean hasChildWithZ() {
+ for (int i = 0; i < mChildrenCount; i++) {
+ if (mChildren[i].getZ() != 0) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children,
+ * sorted first by Z, then by child drawing order (if applicable). This list must be cleared
+ * after use to avoid leaking child Views.
+ *
+ * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated
+ * children.
+ */
+ ArrayList<View> buildOrderedChildList() {
+ final int childrenCount = mChildrenCount;
+ if (childrenCount <= 1 || !hasChildWithZ()) return null;
+
+ if (mPreSortedChildren == null) {
+ mPreSortedChildren = new ArrayList<>(childrenCount);
+ } else {
+ // callers should clear, so clear shouldn't be necessary, but for safety...
+ mPreSortedChildren.clear();
+ mPreSortedChildren.ensureCapacity(childrenCount);
+ }
+
+ final boolean customOrder = isChildrenDrawingOrderEnabled();
+ for (int i = 0; i < childrenCount; i++) {
+ // add next child (in child order) to end of list
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View nextChild = mChildren[childIndex];
+ final float currentZ = nextChild.getZ();
+
+ // insert ahead of any Views with greater Z
+ int insertIndex = i;
+ while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
+ insertIndex--;
+ }
+ mPreSortedChildren.add(insertIndex, nextChild);
+ }
+ return mPreSortedChildren;
+ }
+
+ private void notifyAnimationListener() {
+ mGroupFlags &= ~FLAG_NOTIFY_ANIMATION_LISTENER;
+ mGroupFlags |= FLAG_ANIMATION_DONE;
+
+ if (mAnimationListener != null) {
+ final Runnable end = new Runnable() {
+ @Override
+ public void run() {
+ mAnimationListener.onAnimationEnd(mLayoutAnimationController.getAnimation());
+ }
+ };
+ post(end);
+ }
+
+ invalidate(true);
+ }
+
+ /**
+ * This method is used to cause children of this ViewGroup to restore or recreate their
+ * display lists. It is called by getDisplayList() when the parent ViewGroup does not need
+ * to recreate its own display list, which would happen if it went through the normal
+ * draw/dispatchDraw mechanisms.
+ *
+ * @hide
+ */
+ @Override
+ @UnsupportedAppUsage
+ protected void dispatchGetDisplayList() {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
+ recreateChildDisplayList(child);
+ }
+ }
+ final int transientCount = mTransientViews == null ? 0 : mTransientIndices.size();
+ for (int i = 0; i < transientCount; ++i) {
+ View child = mTransientViews.get(i);
+ if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
+ recreateChildDisplayList(child);
+ }
+ }
+ if (mOverlay != null) {
+ View overlayView = mOverlay.getOverlayView();
+ recreateChildDisplayList(overlayView);
+ }
+ if (mDisappearingChildren != null) {
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ final int disappearingCount = disappearingChildren.size();
+ for (int i = 0; i < disappearingCount; ++i) {
+ final View child = disappearingChildren.get(i);
+ recreateChildDisplayList(child);
+ }
+ }
+ }
+
+ private void recreateChildDisplayList(View child) {
+ child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
+ child.mPrivateFlags &= ~PFLAG_INVALIDATED;
+ child.updateDisplayListIfDirty();
+ child.mRecreateDisplayList = false;
+ }
+
+ /**
+ * Draw one child of this View Group. This method is responsible for getting
+ * the canvas in the right state. This includes clipping, translating so
+ * that the child's scrolled origin is at 0, 0, and applying any animation
+ * transformations.
+ *
+ * @param canvas The canvas on which to draw the child
+ * @param child Who to draw
+ * @param drawingTime The time at which draw is occurring
+ * @return True if an invalidate() was issued
+ */
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ return child.draw(canvas, this, drawingTime);
+ }
+
+ @Override
+ void getScrollIndicatorBounds(@NonNull Rect out) {
+ super.getScrollIndicatorBounds(out);
+
+ // If we have padding and we're supposed to clip children to that
+ // padding, offset the scroll indicators to match our clip bounds.
+ final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+ if (clipToPadding) {
+ out.left += mPaddingLeft;
+ out.right -= mPaddingRight;
+ out.top += mPaddingTop;
+ out.bottom -= mPaddingBottom;
+ }
+ }
+
+ /**
+ * Returns whether this group's children are clipped to their bounds before drawing.
+ * The default value is true.
+ * @see #setClipChildren(boolean)
+ *
+ * @return True if the group's children will be clipped to their bounds,
+ * false otherwise.
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public boolean getClipChildren() {
+ return ((mGroupFlags & FLAG_CLIP_CHILDREN) != 0);
+ }
+
+ /**
+ * By default, children are clipped to their bounds before drawing. This
+ * allows view groups to override this behavior for animations, etc.
+ *
+ * @param clipChildren true to clip children to their bounds,
+ * false otherwise
+ * @attr ref android.R.styleable#ViewGroup_clipChildren
+ */
+ public void setClipChildren(boolean clipChildren) {
+ boolean previousValue = (mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN;
+ if (clipChildren != previousValue) {
+ setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren);
+ for (int i = 0; i < mChildrenCount; ++i) {
+ View child = getChildAt(i);
+ if (child.mRenderNode != null) {
+ child.mRenderNode.setClipToBounds(clipChildren);
+ }
+ }
+ invalidate(true);
+ }
+ }
+
+ /**
+ * Sets whether this ViewGroup will clip its children to its padding and resize (but not
+ * clip) any EdgeEffect to the padded region, if padding is present.
+ * <p>
+ * By default, children are clipped to the padding of their parent
+ * ViewGroup. This clipping behavior is only enabled if padding is non-zero.
+ *
+ * @param clipToPadding true to clip children to the padding of the group, and resize (but
+ * not clip) any EdgeEffect to the padded region. False otherwise.
+ * @attr ref android.R.styleable#ViewGroup_clipToPadding
+ */
+ public void setClipToPadding(boolean clipToPadding) {
+ if (hasBooleanFlag(FLAG_CLIP_TO_PADDING) != clipToPadding) {
+ setBooleanFlag(FLAG_CLIP_TO_PADDING, clipToPadding);
+ invalidate(true);
+ }
+ }
+
+ /**
+ * Returns whether this ViewGroup will clip its children to its padding, and resize (but
+ * not clip) any EdgeEffect to the padded region, if padding is present.
+ * <p>
+ * By default, children are clipped to the padding of their parent
+ * Viewgroup. This clipping behavior is only enabled if padding is non-zero.
+ *
+ * @return true if this ViewGroup clips children to its padding and resizes (but doesn't
+ * clip) any EdgeEffect to the padded region, false otherwise.
+ *
+ * @attr ref android.R.styleable#ViewGroup_clipToPadding
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @InspectableProperty
+ public boolean getClipToPadding() {
+ return hasBooleanFlag(FLAG_CLIP_TO_PADDING);
+ }
+
+ @Override
+ public void dispatchSetSelected(boolean selected) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].setSelected(selected);
+ }
+ }
+
+ @Override
+ public void dispatchSetActivated(boolean activated) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].setActivated(activated);
+ }
+ }
+
+ @Override
+ protected void dispatchSetPressed(boolean pressed) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ // Children that are clickable on their own should not
+ // show a pressed state when their parent view does.
+ // Clearing a pressed state always propagates.
+ if (!pressed || (!child.isClickable() && !child.isLongClickable())) {
+ child.setPressed(pressed);
+ }
+ }
+ }
+
+ /**
+ * Dispatches drawable hotspot changes to child views that meet at least
+ * one of the following criteria:
+ * <ul>
+ * <li>Returns {@code false} from both {@link View#isClickable()} and
+ * {@link View#isLongClickable()}</li>
+ * <li>Requests duplication of parent state via
+ * {@link View#setDuplicateParentStateEnabled(boolean)}</li>
+ * </ul>
+ *
+ * @param x hotspot x coordinate
+ * @param y hotspot y coordinate
+ * @see #drawableHotspotChanged(float, float)
+ */
+ @Override
+ public void dispatchDrawableHotspotChanged(float x, float y) {
+ final int count = mChildrenCount;
+ if (count == 0) {
+ return;
+ }
+
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ // Children that are clickable on their own should not
+ // receive hotspots when their parent view does.
+ final boolean nonActionable = !child.isClickable() && !child.isLongClickable();
+ final boolean duplicatesState = (child.mViewFlags & DUPLICATE_PARENT_STATE) != 0;
+ if (nonActionable || duplicatesState) {
+ final float[] point = getTempLocationF();
+ point[0] = x;
+ point[1] = y;
+ transformPointToViewLocal(point, child);
+ child.drawableHotspotChanged(point[0], point[1]);
+ }
+ }
+ }
+
+ @Override
+ void dispatchCancelPendingInputEvents() {
+ super.dispatchCancelPendingInputEvents();
+
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchCancelPendingInputEvents();
+ }
+ }
+
+ /**
+ * When this property is set to true, this ViewGroup supports static transformations on
+ * children; this causes
+ * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} to be
+ * invoked when a child is drawn.
+ *
+ * Any subclass overriding
+ * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} should
+ * set this property to true.
+ *
+ * @param enabled True to enable static transformations on children, false otherwise.
+ *
+ * @see #getChildStaticTransformation(View, android.view.animation.Transformation)
+ */
+ protected void setStaticTransformationsEnabled(boolean enabled) {
+ setBooleanFlag(FLAG_SUPPORT_STATIC_TRANSFORMATIONS, enabled);
+ }
+
+ /**
+ * Sets <code>t</code> to be the static transformation of the child, if set, returning a
+ * boolean to indicate whether a static transform was set. The default implementation
+ * simply returns <code>false</code>; subclasses may override this method for different
+ * behavior. {@link #setStaticTransformationsEnabled(boolean)} must be set to true
+ * for this method to be called.
+ *
+ * @param child The child view whose static transform is being requested
+ * @param t The Transformation which will hold the result
+ * @return true if the transformation was set, false otherwise
+ * @see #setStaticTransformationsEnabled(boolean)
+ */
+ protected boolean getChildStaticTransformation(View child, Transformation t) {
+ return false;
+ }
+
+ Transformation getChildTransformation() {
+ if (mChildTransformation == null) {
+ mChildTransformation = new Transformation();
+ }
+ return mChildTransformation;
+ }
+
+ /**
+ * {@hide}
+ */
+ @Override
+ protected <T extends View> T findViewTraversal(@IdRes int id) {
+ if (id == mID) {
+ return (T) this;
+ }
+
+ final View[] where = mChildren;
+ final int len = mChildrenCount;
+
+ for (int i = 0; i < len; i++) {
+ View v = where[i];
+
+ if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
+ v = v.findViewById(id);
+
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * {@hide}
+ */
+ @Override
+ protected <T extends View> T findViewWithTagTraversal(Object tag) {
+ if (tag != null && tag.equals(mTag)) {
+ return (T) this;
+ }
+
+ final View[] where = mChildren;
+ final int len = mChildrenCount;
+
+ for (int i = 0; i < len; i++) {
+ View v = where[i];
+
+ if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
+ v = v.findViewWithTag(tag);
+
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * {@hide}
+ */
+ @Override
+ protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
+ View childToSkip) {
+ if (predicate.test(this)) {
+ return (T) this;
+ }
+
+ final View[] where = mChildren;
+ final int len = mChildrenCount;
+
+ for (int i = 0; i < len; i++) {
+ View v = where[i];
+
+ if (v != childToSkip && (v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
+ v = v.findViewByPredicate(predicate);
+
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This method adds a view to this container at the specified index purely for the
+ * purposes of allowing that view to draw even though it is not a normal child of
+ * the container. That is, the view does not participate in layout, focus, accessibility,
+ * input, or other normal view operations; it is purely an item to be drawn during the normal
+ * rendering operation of this container. The index that it is added at is the order
+ * in which it will be drawn, with respect to the other views in the container.
+ * For example, a transient view added at index 0 will be drawn before all other views
+ * in the container because it will be drawn first (including before any real view
+ * at index 0). There can be more than one transient view at any particular index;
+ * these views will be drawn in the order in which they were added to the list of
+ * transient views. The index of transient views can also be greater than the number
+ * of normal views in the container; that just means that they will be drawn after all
+ * other views are drawn.
+ *
+ * <p>Note that since transient views do not participate in layout, they must be sized
+ * manually or, more typically, they should just use the size that they had before they
+ * were removed from their container.</p>
+ *
+ * <p>Transient views are useful for handling animations of views that have been removed
+ * from the container, but which should be animated out after the removal. Adding these
+ * views as transient views allows them to participate in drawing without side-effecting
+ * the layout of the container.</p>
+ *
+ * <p>Transient views must always be explicitly {@link #removeTransientView(View) removed}
+ * from the container when they are no longer needed. For example, a transient view
+ * which is added in order to fade it out in its old location should be removed
+ * once the animation is complete.</p>
+ *
+ * @param view The view to be added. The view must not have a parent.
+ * @param index The index at which this view should be drawn, must be >= 0.
+ * This value is relative to the {@link #getChildAt(int) index} values in the normal
+ * child list of this container, where any transient view at a particular index will
+ * be drawn before any normal child at that same index.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void addTransientView(View view, int index) {
+ if (index < 0 || view == null) {
+ return;
+ }
+ if (view.mParent != null) {
+ throw new IllegalStateException("The specified view already has a parent "
+ + view.mParent);
+ }
+
+ if (mTransientIndices == null) {
+ mTransientIndices = new IntArray();
+ mTransientViews = new ArrayList<View>();
+ }
+ final int oldSize = mTransientIndices.size();
+ if (oldSize > 0) {
+ int insertionIndex;
+ for (insertionIndex = 0; insertionIndex < oldSize; ++insertionIndex) {
+ if (index < mTransientIndices.get(insertionIndex)) {
+ break;
+ }
+ }
+ mTransientIndices.add(insertionIndex, index);
+ mTransientViews.add(insertionIndex, view);
+ } else {
+ mTransientIndices.add(index);
+ mTransientViews.add(view);
+ }
+ view.mParent = this;
+ if (mAttachInfo != null) {
+ view.dispatchAttachedToWindow(mAttachInfo, (mViewFlags & VISIBILITY_MASK));
+ }
+ invalidate(true);
+ }
+
+ /**
+ * Removes a view from the list of transient views in this container. If there is no
+ * such transient view, this method does nothing.
+ *
+ * @param view The transient view to be removed
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void removeTransientView(View view) {
+ if (mTransientViews == null) {
+ return;
+ }
+ final int size = mTransientViews.size();
+ for (int i = 0; i < size; ++i) {
+ if (view == mTransientViews.get(i)) {
+ mTransientViews.remove(i);
+ mTransientIndices.remove(i);
+ view.mParent = null;
+ if (view.mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+ invalidate(true);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Returns the number of transient views in this container. Specific transient
+ * views and the index at which they were added can be retrieved via
+ * {@link #getTransientView(int)} and {@link #getTransientViewIndex(int)}.
+ *
+ * @see #addTransientView(View, int)
+ * @return The number of transient views in this container
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getTransientViewCount() {
+ return mTransientIndices == null ? 0 : mTransientIndices.size();
+ }
+
+ /**
+ * Given a valid position within the list of transient views, returns the index of
+ * the transient view at that position.
+ *
+ * @param position The position of the index being queried. Must be at least 0
+ * and less than the value returned by {@link #getTransientViewCount()}.
+ * @return The index of the transient view stored in the given position if the
+ * position is valid, otherwise -1
+ *
+ * @hide
+ */
+ public int getTransientViewIndex(int position) {
+ if (position < 0 || mTransientIndices == null || position >= mTransientIndices.size()) {
+ return -1;
+ }
+ return mTransientIndices.get(position);
+ }
+
+ /**
+ * Given a valid position within the list of transient views, returns the
+ * transient view at that position.
+ *
+ * @param position The position of the view being queried. Must be at least 0
+ * and less than the value returned by {@link #getTransientViewCount()}.
+ * @return The transient view stored in the given position if the
+ * position is valid, otherwise null
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public View getTransientView(int position) {
+ if (mTransientViews == null || position >= mTransientViews.size()) {
+ return null;
+ }
+ return mTransientViews.get(position);
+ }
+
+ /**
+ * <p>Adds a child view. If no layout parameters are already set on the child, the
+ * default parameters for this ViewGroup are set on the child.</p>
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param child the child view to add
+ *
+ * @see #generateDefaultLayoutParams()
+ */
+ public void addView(View child) {
+ addView(child, -1);
+ }
+
+ /**
+ * Adds a child view. If no layout parameters are already set on the child, the
+ * default parameters for this ViewGroup are set on the child.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param child the child view to add
+ * @param index the position at which to add the child
+ *
+ * @see #generateDefaultLayoutParams()
+ */
+ public void addView(View child, int index) {
+ if (child == null) {
+ throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
+ }
+ LayoutParams params = child.getLayoutParams();
+ if (params == null) {
+ params = generateDefaultLayoutParams();
+ if (params == null) {
+ throw new IllegalArgumentException(
+ "generateDefaultLayoutParams() cannot return null ");
+ }
+ }
+ addView(child, index, params);
+ }
+
+ /**
+ * Adds a child view with this ViewGroup's default layout parameters and the
+ * specified width and height.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param child the child view to add
+ */
+ public void addView(View child, int width, int height) {
+ final LayoutParams params = generateDefaultLayoutParams();
+ params.width = width;
+ params.height = height;
+ addView(child, -1, params);
+ }
+
+ /**
+ * Adds a child view with the specified layout parameters.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param child the child view to add
+ * @param params the layout parameters to set on the child
+ */
+ @Override
+ public void addView(View child, LayoutParams params) {
+ addView(child, -1, params);
+ }
+
+ /**
+ * Adds a child view with the specified layout parameters.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param child the child view to add
+ * @param index the position at which to add the child or -1 to add last
+ * @param params the layout parameters to set on the child
+ */
+ public void addView(View child, int index, LayoutParams params) {
+ if (DBG) {
+ System.out.println(this + " addView");
+ }
+
+ if (child == null) {
+ throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
+ }
+
+ // addViewInner() will call child.requestLayout() when setting the new LayoutParams
+ // therefore, we call requestLayout() on ourselves before, so that the child's request
+ // will be blocked at our level
+ requestLayout();
+ invalidate(true);
+ addViewInner(child, index, params, false);
+ }
+
+ @Override
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
+ if (!checkLayoutParams(params)) {
+ throw new IllegalArgumentException("Invalid LayoutParams supplied to " + this);
+ }
+ if (view.mParent != this) {
+ throw new IllegalArgumentException("Given view not a child of " + this);
+ }
+ view.setLayoutParams(params);
+ }
+
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p != null;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the hierarchy
+ * within this view changed. The hierarchy changes whenever a child is added
+ * to or removed from this view.
+ */
+ public interface OnHierarchyChangeListener {
+ /**
+ * Called when a new child is added to a parent view.
+ *
+ * @param parent the view in which a child was added
+ * @param child the new child view added in the hierarchy
+ */
+ void onChildViewAdded(View parent, View child);
+
+ /**
+ * Called when a child is removed from a parent view.
+ *
+ * @param parent the view from which the child was removed
+ * @param child the child removed from the hierarchy
+ */
+ void onChildViewRemoved(View parent, View child);
+ }
+
+ /**
+ * Register a callback to be invoked when a child is added to or removed
+ * from this view.
+ *
+ * @param listener the callback to invoke on hierarchy change
+ */
+ public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+ mOnHierarchyChangeListener = listener;
+ }
+
+ @UnsupportedAppUsage
+ void dispatchViewAdded(View child) {
+ onViewAdded(child);
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewAdded(this, child);
+ }
+ }
+
+ /**
+ * Called when a new child is added to this ViewGroup. Overrides should always
+ * call super.onViewAdded.
+ *
+ * @param child the added child view
+ */
+ public void onViewAdded(View child) {
+ }
+
+ @UnsupportedAppUsage
+ void dispatchViewRemoved(View child) {
+ onViewRemoved(child);
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewRemoved(this, child);
+ }
+ }
+
+ /**
+ * Called when a child view is removed from this ViewGroup. Overrides should always
+ * call super.onViewRemoved.
+ *
+ * @param child the removed child view
+ */
+ public void onViewRemoved(View child) {
+ }
+
+ private void clearCachedLayoutMode() {
+ if (!hasBooleanFlag(FLAG_LAYOUT_MODE_WAS_EXPLICITLY_SET)) {
+ mLayoutMode = LAYOUT_MODE_UNDEFINED;
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ clearCachedLayoutMode();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ clearCachedLayoutMode();
+ }
+
+ /** @hide */
+ @Override
+ protected void destroyHardwareResources() {
+ super.destroyHardwareResources();
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).destroyHardwareResources();
+ }
+ }
+
+ /**
+ * Adds a view during layout. This is useful if in your onLayout() method,
+ * you need to add more views (as does the list view for example).
+ *
+ * If index is negative, it means put it at the end of the list.
+ *
+ * @param child the view to add to the group
+ * @param index the index at which the child must be added or -1 to add last
+ * @param params the layout parameters to associate with the child
+ * @return true if the child was added, false otherwise
+ */
+ protected boolean addViewInLayout(View child, int index, LayoutParams params) {
+ return addViewInLayout(child, index, params, false);
+ }
+
+ /**
+ * Adds a view during layout. This is useful if in your onLayout() method,
+ * you need to add more views (as does the list view for example).
+ *
+ * If index is negative, it means put it at the end of the list.
+ *
+ * @param child the view to add to the group
+ * @param index the index at which the child must be added or -1 to add last
+ * @param params the layout parameters to associate with the child
+ * @param preventRequestLayout if true, calling this method will not trigger a
+ * layout request on child
+ * @return true if the child was added, false otherwise
+ */
+ protected boolean addViewInLayout(View child, int index, LayoutParams params,
+ boolean preventRequestLayout) {
+ if (child == null) {
+ throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
+ }
+ child.mParent = null;
+ addViewInner(child, index, params, preventRequestLayout);
+ child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
+ return true;
+ }
+
+ /**
+ * Prevents the specified child to be laid out during the next layout pass.
+ *
+ * @param child the child on which to perform the cleanup
+ */
+ protected void cleanupLayoutState(View child) {
+ child.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT;
+ }
+
+ private void addViewInner(View child, int index, LayoutParams params,
+ boolean preventRequestLayout) {
+
+ if (mTransition != null) {
+ // Don't prevent other add transitions from completing, but cancel remove
+ // transitions to let them complete the process before we add to the container
+ mTransition.cancel(LayoutTransition.DISAPPEARING);
+ }
+
+ if (child.getParent() != null) {
+ throw new IllegalStateException("The specified child already has a parent. " +
+ "You must call removeView() on the child's parent first.");
+ }
+
+ if (mTransition != null) {
+ mTransition.addChild(this, child);
+ }
+
+ if (!checkLayoutParams(params)) {
+ params = generateLayoutParams(params);
+ }
+
+ if (preventRequestLayout) {
+ child.mLayoutParams = params;
+ } else {
+ child.setLayoutParams(params);
+ }
+
+ if (index < 0) {
+ index = mChildrenCount;
+ }
+
+ addInArray(child, index);
+
+ // tell our children
+ if (preventRequestLayout) {
+ child.assignParent(this);
+ } else {
+ child.mParent = this;
+ }
+ if (child.hasUnhandledKeyListener()) {
+ incrementChildUnhandledKeyListeners();
+ }
+
+ final boolean childHasFocus = child.hasFocus();
+ if (childHasFocus) {
+ requestChildFocus(child, child.findFocus());
+ }
+
+ AttachInfo ai = mAttachInfo;
+ if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
+ boolean lastKeepOn = ai.mKeepScreenOn;
+ ai.mKeepScreenOn = false;
+ child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
+ if (ai.mKeepScreenOn) {
+ needGlobalAttributesUpdate(true);
+ }
+ ai.mKeepScreenOn = lastKeepOn;
+ }
+
+ if (child.isLayoutDirectionInherited()) {
+ child.resetRtlProperties();
+ }
+
+ dispatchViewAdded(child);
+
+ if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {
+ mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;
+ }
+
+ if (child.hasTransientState()) {
+ childHasTransientStateChanged(child, true);
+ }
+
+ if (child.getVisibility() != View.GONE) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+
+ if (mTransientIndices != null) {
+ final int transientCount = mTransientIndices.size();
+ for (int i = 0; i < transientCount; ++i) {
+ final int oldIndex = mTransientIndices.get(i);
+ if (index <= oldIndex) {
+ mTransientIndices.set(i, oldIndex + 1);
+ }
+ }
+ }
+
+ if (mCurrentDragStartEvent != null && child.getVisibility() == VISIBLE) {
+ notifyChildOfDragStart(child);
+ }
+
+ if (child.hasDefaultFocus()) {
+ // When adding a child that contains default focus, either during inflation or while
+ // manually assembling the hierarchy, update the ancestor default-focus chain.
+ setDefaultFocus(child);
+ }
+
+ touchAccessibilityNodeProviderIfNeeded(child);
+ }
+
+ /**
+ * We may need to touch the provider to bring up the a11y layer. In a11y mode
+ * clients inspect the screen or the user touches it which triggers bringing up
+ * of the a11y infrastructure while in autofill mode we want the infra up and
+ * running from the beginning since we watch for a11y events to drive autofill.
+ */
+ private void touchAccessibilityNodeProviderIfNeeded(View child) {
+ if (mContext.isAutofillCompatibilityEnabled()) {
+ child.getAccessibilityNodeProvider();
+ }
+ }
+
+ private void addInArray(View child, int index) {
+ View[] children = mChildren;
+ final int count = mChildrenCount;
+ final int size = children.length;
+ if (index == count) {
+ if (size == count) {
+ mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
+ System.arraycopy(children, 0, mChildren, 0, size);
+ children = mChildren;
+ }
+ children[mChildrenCount++] = child;
+ } else if (index < count) {
+ if (size == count) {
+ mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
+ System.arraycopy(children, 0, mChildren, 0, index);
+ System.arraycopy(children, index, mChildren, index + 1, count - index);
+ children = mChildren;
+ } else {
+ System.arraycopy(children, index, children, index + 1, count - index);
+ }
+ children[index] = child;
+ mChildrenCount++;
+ if (mLastTouchDownIndex >= index) {
+ mLastTouchDownIndex++;
+ }
+ } else {
+ throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
+ }
+ }
+
+ // This method also sets the child's mParent to null
+ private void removeFromArray(int index) {
+ final View[] children = mChildren;
+ if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
+ children[index].mParent = null;
+ }
+ final int count = mChildrenCount;
+ if (index == count - 1) {
+ children[--mChildrenCount] = null;
+ } else if (index >= 0 && index < count) {
+ System.arraycopy(children, index + 1, children, index, count - index - 1);
+ children[--mChildrenCount] = null;
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ if (mLastTouchDownIndex == index) {
+ mLastTouchDownTime = 0;
+ mLastTouchDownIndex = -1;
+ } else if (mLastTouchDownIndex > index) {
+ mLastTouchDownIndex--;
+ }
+ }
+
+ // This method also sets the children's mParent to null
+ private void removeFromArray(int start, int count) {
+ final View[] children = mChildren;
+ final int childrenCount = mChildrenCount;
+
+ start = Math.max(0, start);
+ final int end = Math.min(childrenCount, start + count);
+
+ if (start == end) {
+ return;
+ }
+
+ if (end == childrenCount) {
+ for (int i = start; i < end; i++) {
+ children[i].mParent = null;
+ children[i] = null;
+ }
+ } else {
+ for (int i = start; i < end; i++) {
+ children[i].mParent = null;
+ }
+
+ // Since we're looping above, we might as well do the copy, but is arraycopy()
+ // faster than the extra 2 bounds checks we would do in the loop?
+ System.arraycopy(children, end, children, start, childrenCount - end);
+
+ for (int i = childrenCount - (end - start); i < childrenCount; i++) {
+ children[i] = null;
+ }
+ }
+
+ mChildrenCount -= (end - start);
+ }
+
+ private void bindLayoutAnimation(View child) {
+ Animation a = mLayoutAnimationController.getAnimationForView(child);
+ child.setAnimation(a);
+ }
+
+ /**
+ * Subclasses should override this method to set layout animation
+ * parameters on the supplied child.
+ *
+ * @param child the child to associate with animation parameters
+ * @param params the child's layout parameters which hold the animation
+ * parameters
+ * @param index the index of the child in the view group
+ * @param count the number of children in the view group
+ */
+ protected void attachLayoutAnimationParameters(View child,
+ LayoutParams params, int index, int count) {
+ LayoutAnimationController.AnimationParameters animationParams =
+ params.layoutAnimationParameters;
+ if (animationParams == null) {
+ animationParams = new LayoutAnimationController.AnimationParameters();
+ params.layoutAnimationParameters = animationParams;
+ }
+
+ animationParams.count = count;
+ animationParams.index = index;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ */
+ @Override
+ public void removeView(View view) {
+ if (removeViewInternal(view)) {
+ requestLayout();
+ invalidate(true);
+ }
+ }
+
+ /**
+ * Removes a view during layout. This is useful if in your onLayout() method,
+ * you need to remove more views.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param view the view to remove from the group
+ */
+ public void removeViewInLayout(View view) {
+ removeViewInternal(view);
+ }
+
+ /**
+ * Removes a range of views during layout. This is useful if in your onLayout() method,
+ * you need to remove more views.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param start the index of the first view to remove from the group
+ * @param count the number of views to remove from the group
+ */
+ public void removeViewsInLayout(int start, int count) {
+ removeViewsInternal(start, count);
+ }
+
+ /**
+ * Removes the view at the specified position in the group.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param index the position in the group of the view to remove
+ */
+ public void removeViewAt(int index) {
+ removeViewInternal(index, getChildAt(index));
+ requestLayout();
+ invalidate(true);
+ }
+
+ /**
+ * Removes the specified range of views from the group.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ *
+ * @param start the first position in the group of the range of views to remove
+ * @param count the number of views to remove
+ */
+ public void removeViews(int start, int count) {
+ removeViewsInternal(start, count);
+ requestLayout();
+ invalidate(true);
+ }
+
+ private boolean removeViewInternal(View view) {
+ final int index = indexOfChild(view);
+ if (index >= 0) {
+ removeViewInternal(index, view);
+ return true;
+ }
+ return false;
+ }
+
+ private void removeViewInternal(int index, View view) {
+ if (mTransition != null) {
+ mTransition.removeChild(this, view);
+ }
+
+ boolean clearChildFocus = false;
+ if (view == mFocused) {
+ view.unFocus(null);
+ clearChildFocus = true;
+ }
+ if (view == mFocusedInCluster) {
+ clearFocusedInCluster(view);
+ }
+
+ view.clearAccessibilityFocus();
+
+ cancelTouchTarget(view);
+ cancelHoverTarget(view);
+
+ if (view.getAnimation() != null ||
+ (mTransitioningViews != null && mTransitioningViews.contains(view))) {
+ addDisappearingView(view);
+ } else if (view.mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ if (view.hasTransientState()) {
+ childHasTransientStateChanged(view, false);
+ }
+
+ needGlobalAttributesUpdate(false);
+
+ removeFromArray(index);
+
+ if (view.hasUnhandledKeyListener()) {
+ decrementChildUnhandledKeyListeners();
+ }
+
+ if (view == mDefaultFocus) {
+ clearDefaultFocus(view);
+ }
+ if (clearChildFocus) {
+ clearChildFocus(view);
+ if (!rootViewRequestFocus()) {
+ notifyGlobalFocusCleared(this);
+ }
+ }
+
+ dispatchViewRemoved(view);
+
+ if (view.getVisibility() != View.GONE) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+
+ int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
+ for (int i = 0; i < transientCount; ++i) {
+ final int oldIndex = mTransientIndices.get(i);
+ if (index < oldIndex) {
+ mTransientIndices.set(i, oldIndex - 1);
+ }
+ }
+
+ if (mCurrentDragStartEvent != null) {
+ mChildrenInterestedInDrag.remove(view);
+ }
+ }
+
+ /**
+ * Sets the LayoutTransition object for this ViewGroup. If the LayoutTransition object is
+ * not null, changes in layout which occur because of children being added to or removed from
+ * the ViewGroup will be animated according to the animations defined in that LayoutTransition
+ * object. By default, the transition object is null (so layout changes are not animated).
+ *
+ * <p>Replacing a non-null transition will cause that previous transition to be
+ * canceled, if it is currently running, to restore this container to
+ * its correct post-transition state.</p>
+ *
+ * @param transition The LayoutTransition object that will animated changes in layout. A value
+ * of <code>null</code> means no transition will run on layout changes.
+ * @attr ref android.R.styleable#ViewGroup_animateLayoutChanges
+ */
+ public void setLayoutTransition(LayoutTransition transition) {
+ if (mTransition != null) {
+ LayoutTransition previousTransition = mTransition;
+ previousTransition.cancel();
+ previousTransition.removeTransitionListener(mLayoutTransitionListener);
+ }
+ mTransition = transition;
+ if (mTransition != null) {
+ mTransition.addTransitionListener(mLayoutTransitionListener);
+ }
+ }
+
+ /**
+ * Gets the LayoutTransition object for this ViewGroup. If the LayoutTransition object is
+ * not null, changes in layout which occur because of children being added to or removed from
+ * the ViewGroup will be animated according to the animations defined in that LayoutTransition
+ * object. By default, the transition object is null (so layout changes are not animated).
+ *
+ * @return LayoutTranstion The LayoutTransition object that will animated changes in layout.
+ * A value of <code>null</code> means no transition will run on layout changes.
+ */
+ public LayoutTransition getLayoutTransition() {
+ return mTransition;
+ }
+
+ private void removeViewsInternal(int start, int count) {
+ final int end = start + count;
+
+ if (start < 0 || count < 0 || end > mChildrenCount) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ final View focused = mFocused;
+ final boolean detach = mAttachInfo != null;
+ boolean clearChildFocus = false;
+ View clearDefaultFocus = null;
+
+ final View[] children = mChildren;
+
+ for (int i = start; i < end; i++) {
+ final View view = children[i];
+
+ if (mTransition != null) {
+ mTransition.removeChild(this, view);
+ }
+
+ if (view == focused) {
+ view.unFocus(null);
+ clearChildFocus = true;
+ }
+ if (view == mDefaultFocus) {
+ clearDefaultFocus = view;
+ }
+ if (view == mFocusedInCluster) {
+ clearFocusedInCluster(view);
+ }
+
+ view.clearAccessibilityFocus();
+
+ cancelTouchTarget(view);
+ cancelHoverTarget(view);
+
+ if (view.getAnimation() != null ||
+ (mTransitioningViews != null && mTransitioningViews.contains(view))) {
+ addDisappearingView(view);
+ } else if (detach) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ if (view.hasTransientState()) {
+ childHasTransientStateChanged(view, false);
+ }
+
+ needGlobalAttributesUpdate(false);
+
+ dispatchViewRemoved(view);
+ }
+
+ removeFromArray(start, count);
+
+ if (clearDefaultFocus != null) {
+ clearDefaultFocus(clearDefaultFocus);
+ }
+ if (clearChildFocus) {
+ clearChildFocus(focused);
+ if (!rootViewRequestFocus()) {
+ notifyGlobalFocusCleared(focused);
+ }
+ }
+ }
+
+ /**
+ * Call this method to remove all child views from the
+ * ViewGroup.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ */
+ public void removeAllViews() {
+ removeAllViewsInLayout();
+ requestLayout();
+ invalidate(true);
+ }
+
+ /**
+ * Called by a ViewGroup subclass to remove child views from itself,
+ * when it must first know its size on screen before it can calculate how many
+ * child views it will render. An example is a Gallery or a ListView, which
+ * may "have" 50 children, but actually only render the number of children
+ * that can currently fit inside the object on screen. Do not call
+ * this method unless you are extending ViewGroup and understand the
+ * view measuring and layout pipeline.
+ *
+ * <p><strong>Note:</strong> do not invoke this method from
+ * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
+ * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
+ */
+ public void removeAllViewsInLayout() {
+ final int count = mChildrenCount;
+ if (count <= 0) {
+ return;
+ }
+
+ final View[] children = mChildren;
+ mChildrenCount = 0;
+
+ final View focused = mFocused;
+ final boolean detach = mAttachInfo != null;
+ boolean clearChildFocus = false;
+
+ needGlobalAttributesUpdate(false);
+
+ for (int i = count - 1; i >= 0; i--) {
+ final View view = children[i];
+
+ if (mTransition != null) {
+ mTransition.removeChild(this, view);
+ }
+
+ if (view == focused) {
+ view.unFocus(null);
+ clearChildFocus = true;
+ }
+
+ view.clearAccessibilityFocus();
+
+ cancelTouchTarget(view);
+ cancelHoverTarget(view);
+
+ if (view.getAnimation() != null ||
+ (mTransitioningViews != null && mTransitioningViews.contains(view))) {
+ addDisappearingView(view);
+ } else if (detach) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ if (view.hasTransientState()) {
+ childHasTransientStateChanged(view, false);
+ }
+
+ dispatchViewRemoved(view);
+
+ view.mParent = null;
+ children[i] = null;
+ }
+
+ if (mDefaultFocus != null) {
+ clearDefaultFocus(mDefaultFocus);
+ }
+ if (mFocusedInCluster != null) {
+ clearFocusedInCluster(mFocusedInCluster);
+ }
+ if (clearChildFocus) {
+ clearChildFocus(focused);
+ if (!rootViewRequestFocus()) {
+ notifyGlobalFocusCleared(focused);
+ }
+ }
+ }
+
+ /**
+ * Finishes the removal of a detached view. This method will dispatch the detached from
+ * window event and notify the hierarchy change listener.
+ * <p>
+ * This method is intended to be lightweight and makes no assumptions about whether the
+ * parent or child should be redrawn. Proper use of this method will include also making
+ * any appropriate {@link #requestLayout()} or {@link #invalidate()} calls.
+ * For example, callers can {@link #post(Runnable) post} a {@link Runnable}
+ * which performs a {@link #requestLayout()} on the next frame, after all detach/remove
+ * calls are finished, causing layout to be run prior to redrawing the view hierarchy.
+ *
+ * @param child the child to be definitely removed from the view hierarchy
+ * @param animate if true and the view has an animation, the view is placed in the
+ * disappearing views list, otherwise, it is detached from the window
+ *
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #detachAllViewsFromParent()
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ */
+ protected void removeDetachedView(View child, boolean animate) {
+ if (mTransition != null) {
+ mTransition.removeChild(this, child);
+ }
+
+ if (child == mFocused) {
+ child.clearFocus();
+ }
+ if (child == mDefaultFocus) {
+ clearDefaultFocus(child);
+ }
+ if (child == mFocusedInCluster) {
+ clearFocusedInCluster(child);
+ }
+
+ child.clearAccessibilityFocus();
+
+ cancelTouchTarget(child);
+ cancelHoverTarget(child);
+
+ if ((animate && child.getAnimation() != null) ||
+ (mTransitioningViews != null && mTransitioningViews.contains(child))) {
+ addDisappearingView(child);
+ } else if (child.mAttachInfo != null) {
+ child.dispatchDetachedFromWindow();
+ }
+
+ if (child.hasTransientState()) {
+ childHasTransientStateChanged(child, false);
+ }
+
+ dispatchViewRemoved(child);
+ }
+
+ /**
+ * Attaches a view to this view group. Attaching a view assigns this group as the parent,
+ * sets the layout parameters and puts the view in the list of children so that
+ * it can be retrieved by calling {@link #getChildAt(int)}.
+ * <p>
+ * This method is intended to be lightweight and makes no assumptions about whether the
+ * parent or child should be redrawn. Proper use of this method will include also making
+ * any appropriate {@link #requestLayout()} or {@link #invalidate()} calls.
+ * For example, callers can {@link #post(Runnable) post} a {@link Runnable}
+ * which performs a {@link #requestLayout()} on the next frame, after all detach/attach
+ * calls are finished, causing layout to be run prior to redrawing the view hierarchy.
+ * <p>
+ * This method should be called only for views which were detached from their parent.
+ *
+ * @param child the child to attach
+ * @param index the index at which the child should be attached
+ * @param params the layout parameters of the child
+ *
+ * @see #removeDetachedView(View, boolean)
+ * @see #detachAllViewsFromParent()
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ */
+ protected void attachViewToParent(View child, int index, LayoutParams params) {
+ child.mLayoutParams = params;
+
+ if (index < 0) {
+ index = mChildrenCount;
+ }
+
+ addInArray(child, index);
+
+ child.mParent = this;
+ child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
+ & ~PFLAG_DRAWING_CACHE_VALID)
+ | PFLAG_DRAWN | PFLAG_INVALIDATED;
+ child.setDetached(false);
+ this.mPrivateFlags |= PFLAG_INVALIDATED;
+
+ if (child.hasFocus()) {
+ requestChildFocus(child, child.findFocus());
+ }
+ dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
+ && isShown());
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+
+ /**
+ * Detaches a view from its parent. Detaching a view should be followed
+ * either by a call to
+ * {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be
+ * temporary; reattachment or removal should happen within the same drawing cycle as
+ * detachment. When a view is detached, its parent is null and cannot be retrieved by a
+ * call to {@link #getChildAt(int)}.
+ *
+ * @param child the child to detach
+ *
+ * @see #detachViewFromParent(int)
+ * @see #detachViewsFromParent(int, int)
+ * @see #detachAllViewsFromParent()
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachViewFromParent(View child) {
+ child.setDetached(true);
+ removeFromArray(indexOfChild(child));
+ }
+
+ /**
+ * Detaches a view from its parent. Detaching a view should be followed
+ * either by a call to
+ * {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be
+ * temporary; reattachment or removal should happen within the same drawing cycle as
+ * detachment. When a view is detached, its parent is null and cannot be retrieved by a
+ * call to {@link #getChildAt(int)}.
+ *
+ * @param index the index of the child to detach
+ *
+ * @see #detachViewFromParent(View)
+ * @see #detachAllViewsFromParent()
+ * @see #detachViewsFromParent(int, int)
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachViewFromParent(int index) {
+ if (index >= 0 && index < mChildrenCount) {
+ mChildren[index].setDetached(true);
+ }
+ removeFromArray(index);
+ }
+
+ /**
+ * Detaches a range of views from their parents. Detaching a view should be followed
+ * either by a call to
+ * {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be
+ * temporary; reattachment or removal should happen within the same drawing cycle as
+ * detachment. When a view is detached, its parent is null and cannot be retrieved by a
+ * call to {@link #getChildAt(int)}.
+ *
+ * @param start the first index of the childrend range to detach
+ * @param count the number of children to detach
+ *
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ * @see #detachAllViewsFromParent()
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachViewsFromParent(int start, int count) {
+ start = Math.max(0, start);
+ final int end = Math.min(mChildrenCount, start + count);
+ for (int i = start; i < end; i++) {
+ mChildren[i].setDetached(true);
+ }
+ removeFromArray(start, count);
+ }
+
+ /**
+ * Detaches all views from the parent. Detaching a view should be followed
+ * either by a call to
+ * {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be
+ * temporary; reattachment or removal should happen within the same drawing cycle as
+ * detachment. When a view is detached, its parent is null and cannot be retrieved by a
+ * call to {@link #getChildAt(int)}.
+ *
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ * @see #detachViewsFromParent(int, int)
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachAllViewsFromParent() {
+ final int count = mChildrenCount;
+ if (count <= 0) {
+ return;
+ }
+
+ final View[] children = mChildren;
+ mChildrenCount = 0;
+
+ for (int i = count - 1; i >= 0; i--) {
+ children[i].mParent = null;
+ children[i].setDetached(true);
+ children[i] = null;
+ }
+ }
+
+ @Override
+ @CallSuper
+ public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
+ /*
+ * HW-only, Rect-ignoring damage codepath
+ *
+ * We don't deal with rectangles here, since RenderThread native code computes damage for
+ * everything drawn by HWUI (and SW layer / drawing cache doesn't keep track of damage area)
+ */
+
+ // if set, combine the animation flag into the parent
+ mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
+
+ if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
+ // We lazily use PFLAG_DIRTY, since computing opaque isn't worth the potential
+ // optimization in provides in a DisplayList world.
+ mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
+
+ // simplified invalidateChildInParent behavior: clear cache validity to be safe...
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ }
+
+ // ... and mark inval if in software layer that needs to repaint (hw handled in native)
+ if (mLayerType == LAYER_TYPE_SOFTWARE) {
+ // Layered parents should be invalidated. Escalate to a full invalidate (and note that
+ // we do this after consuming any relevant flags from the originating descendant)
+ mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
+ target = this;
+ }
+
+ if (mParent != null) {
+ mParent.onDescendantInvalidated(this, target);
+ }
+ }
+
+
+ /**
+ * Don't call or override this method. It is used for the implementation of
+ * the view hierarchy.
+ *
+ * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
+ * draw state in descendants.
+ */
+ @Deprecated
+ @Override
+ public final void invalidateChild(View child, final Rect dirty) {
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null && attachInfo.mHardwareAccelerated) {
+ // HW accelerated fast path
+ onDescendantInvalidated(child, child);
+ return;
+ }
+
+ ViewParent parent = this;
+ if (attachInfo != null) {
+ // If the child is drawing an animation, we want to copy this flag onto
+ // ourselves and the parent to make sure the invalidate request goes
+ // through
+ final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;
+
+ // Check whether the child that requests the invalidate is fully opaque
+ // Views being animated or transformed are not considered opaque because we may
+ // be invalidating their old position and need the parent to paint behind them.
+ Matrix childMatrix = child.getMatrix();
+ // Mark the child as dirty, using the appropriate flag
+ // Make sure we do not set both flags at the same time
+
+ if (child.mLayerType != LAYER_TYPE_NONE) {
+ mPrivateFlags |= PFLAG_INVALIDATED;
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ }
+
+ final int[] location = attachInfo.mInvalidateChildLocation;
+ location[CHILD_LEFT_INDEX] = child.mLeft;
+ location[CHILD_TOP_INDEX] = child.mTop;
+ if (!childMatrix.isIdentity() ||
+ (mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
+ RectF boundingRect = attachInfo.mTmpTransformRect;
+ boundingRect.set(dirty);
+ Matrix transformMatrix;
+ if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
+ Transformation t = attachInfo.mTmpTransformation;
+ boolean transformed = getChildStaticTransformation(child, t);
+ if (transformed) {
+ transformMatrix = attachInfo.mTmpMatrix;
+ transformMatrix.set(t.getMatrix());
+ if (!childMatrix.isIdentity()) {
+ transformMatrix.preConcat(childMatrix);
+ }
+ } else {
+ transformMatrix = childMatrix;
+ }
+ } else {
+ transformMatrix = childMatrix;
+ }
+ transformMatrix.mapRect(boundingRect);
+ dirty.set((int) Math.floor(boundingRect.left),
+ (int) Math.floor(boundingRect.top),
+ (int) Math.ceil(boundingRect.right),
+ (int) Math.ceil(boundingRect.bottom));
+ }
+
+ do {
+ View view = null;
+ if (parent instanceof View) {
+ view = (View) parent;
+ }
+
+ if (drawAnimation) {
+ if (view != null) {
+ view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
+ } else if (parent instanceof ViewRootImpl) {
+ ((ViewRootImpl) parent).mIsAnimating = true;
+ }
+ }
+
+ // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
+ // flag coming from the child that initiated the invalidate
+ if (view != null) {
+ if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
+ view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
+ }
+ }
+
+ parent = parent.invalidateChildInParent(location, dirty);
+ if (view != null) {
+ // Account for transform on current parent
+ Matrix m = view.getMatrix();
+ if (!m.isIdentity()) {
+ RectF boundingRect = attachInfo.mTmpTransformRect;
+ boundingRect.set(dirty);
+ m.mapRect(boundingRect);
+ dirty.set((int) Math.floor(boundingRect.left),
+ (int) Math.floor(boundingRect.top),
+ (int) Math.ceil(boundingRect.right),
+ (int) Math.ceil(boundingRect.bottom));
+ }
+ }
+ } while (parent != null);
+ }
+ }
+
+ /**
+ * Don't call or override this method. It is used for the implementation of
+ * the view hierarchy.
+ *
+ * This implementation returns null if this ViewGroup does not have a parent,
+ * if this ViewGroup is already fully invalidated or if the dirty rectangle
+ * does not intersect with this ViewGroup's bounds.
+ *
+ * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
+ * draw state in descendants.
+ */
+ @Deprecated
+ @Override
+ public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
+ if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
+ // either DRAWN, or DRAWING_CACHE_VALID
+ if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
+ != FLAG_OPTIMIZE_INVALIDATE) {
+ dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
+ location[CHILD_TOP_INDEX] - mScrollY);
+ if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
+ dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
+ }
+
+ final int left = mLeft;
+ final int top = mTop;
+
+ if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
+ if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
+ dirty.setEmpty();
+ }
+ }
+
+ location[CHILD_LEFT_INDEX] = left;
+ location[CHILD_TOP_INDEX] = top;
+ } else {
+
+ if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
+ dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
+ } else {
+ // in case the dirty rect extends outside the bounds of this container
+ dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
+ }
+ location[CHILD_LEFT_INDEX] = mLeft;
+ location[CHILD_TOP_INDEX] = mTop;
+
+ mPrivateFlags &= ~PFLAG_DRAWN;
+ }
+ mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
+ if (mLayerType != LAYER_TYPE_NONE) {
+ mPrivateFlags |= PFLAG_INVALIDATED;
+ }
+
+ return mParent;
+ }
+
+ return null;
+ }
+
+ /**
+ * Offset a rectangle that is in a descendant's coordinate
+ * space into our coordinate space.
+ * @param descendant A descendant of this view
+ * @param rect A rectangle defined in descendant's coordinate space.
+ */
+ public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) {
+ offsetRectBetweenParentAndChild(descendant, rect, true, false);
+ }
+
+ /**
+ * Offset a rectangle that is in our coordinate space into an ancestor's
+ * coordinate space.
+ * @param descendant A descendant of this view
+ * @param rect A rectangle defined in descendant's coordinate space.
+ */
+ public final void offsetRectIntoDescendantCoords(View descendant, Rect rect) {
+ offsetRectBetweenParentAndChild(descendant, rect, false, false);
+ }
+
+ /**
+ * Helper method that offsets a rect either from parent to descendant or
+ * descendant to parent.
+ */
+ void offsetRectBetweenParentAndChild(View descendant, Rect rect,
+ boolean offsetFromChildToParent, boolean clipToBounds) {
+
+ // already in the same coord system :)
+ if (descendant == this) {
+ return;
+ }
+
+ ViewParent theParent = descendant.mParent;
+
+ // search and offset up to the parent
+ while ((theParent != null)
+ && (theParent instanceof View)
+ && (theParent != this)) {
+
+ if (offsetFromChildToParent) {
+ rect.offset(descendant.mLeft - descendant.mScrollX,
+ descendant.mTop - descendant.mScrollY);
+ if (clipToBounds) {
+ View p = (View) theParent;
+ boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft,
+ p.mBottom - p.mTop);
+ if (!intersected) {
+ rect.setEmpty();
+ }
+ }
+ } else {
+ if (clipToBounds) {
+ View p = (View) theParent;
+ boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft,
+ p.mBottom - p.mTop);
+ if (!intersected) {
+ rect.setEmpty();
+ }
+ }
+ rect.offset(descendant.mScrollX - descendant.mLeft,
+ descendant.mScrollY - descendant.mTop);
+ }
+
+ descendant = (View) theParent;
+ theParent = descendant.mParent;
+ }
+
+ // now that we are up to this view, need to offset one more time
+ // to get into our coordinate space
+ if (theParent == this) {
+ if (offsetFromChildToParent) {
+ rect.offset(descendant.mLeft - descendant.mScrollX,
+ descendant.mTop - descendant.mScrollY);
+ } else {
+ rect.offset(descendant.mScrollX - descendant.mLeft,
+ descendant.mScrollY - descendant.mTop);
+ }
+ } else {
+ throw new IllegalArgumentException("parameter must be a descendant of this view");
+ }
+ }
+
+ /**
+ * Offset the vertical location of all children of this view by the specified number of pixels.
+ *
+ * @param offset the number of pixels to offset
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void offsetChildrenTopAndBottom(int offset) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ boolean invalidate = false;
+
+ for (int i = 0; i < count; i++) {
+ final View v = children[i];
+ v.mTop += offset;
+ v.mBottom += offset;
+ if (v.mRenderNode != null) {
+ invalidate = true;
+ v.mRenderNode.offsetTopAndBottom(offset);
+ }
+ }
+
+ if (invalidate) {
+ invalidateViewProperty(false, false);
+ }
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+
+ @Override
+ public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
+ return getChildVisibleRect(child, r, offset, false);
+ }
+
+ /**
+ * @param forceParentCheck true to guarantee that this call will propagate to all ancestors,
+ * false otherwise
+ *
+ * @hide
+ */
+ public boolean getChildVisibleRect(
+ View child, Rect r, android.graphics.Point offset, boolean forceParentCheck) {
+ // It doesn't make a whole lot of sense to call this on a view that isn't attached,
+ // but for some simple tests it can be useful. If we don't have attach info this
+ // will allocate memory.
+ final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF();
+ rect.set(r);
+
+ if (!child.hasIdentityMatrix()) {
+ child.getMatrix().mapRect(rect);
+ }
+
+ final int dx = child.mLeft - mScrollX;
+ final int dy = child.mTop - mScrollY;
+
+ rect.offset(dx, dy);
+
+ if (offset != null) {
+ if (!child.hasIdentityMatrix()) {
+ float[] position = mAttachInfo != null ? mAttachInfo.mTmpTransformLocation
+ : new float[2];
+ position[0] = offset.x;
+ position[1] = offset.y;
+ child.getMatrix().mapPoints(position);
+ offset.x = Math.round(position[0]);
+ offset.y = Math.round(position[1]);
+ }
+ offset.x += dx;
+ offset.y += dy;
+ }
+
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
+
+ boolean rectIsVisible = true;
+ if (mParent == null ||
+ (mParent instanceof ViewGroup && ((ViewGroup) mParent).getClipChildren())) {
+ // Clip to bounds.
+ rectIsVisible = rect.intersect(0, 0, width, height);
+ }
+
+ if ((forceParentCheck || rectIsVisible)
+ && (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ // Clip to padding.
+ rectIsVisible = rect.intersect(mPaddingLeft, mPaddingTop,
+ width - mPaddingRight, height - mPaddingBottom);
+ }
+
+ if ((forceParentCheck || rectIsVisible) && mClipBounds != null) {
+ // Clip to clipBounds.
+ rectIsVisible = rect.intersect(mClipBounds.left, mClipBounds.top, mClipBounds.right,
+ mClipBounds.bottom);
+ }
+ r.set((int) Math.floor(rect.left), (int) Math.floor(rect.top),
+ (int) Math.ceil(rect.right), (int) Math.ceil(rect.bottom));
+
+ if ((forceParentCheck || rectIsVisible) && mParent != null) {
+ if (mParent instanceof ViewGroup) {
+ rectIsVisible = ((ViewGroup) mParent)
+ .getChildVisibleRect(this, r, offset, forceParentCheck);
+ } else {
+ rectIsVisible = mParent.getChildVisibleRect(this, r, offset);
+ }
+ }
+ return rectIsVisible;
+ }
+
+ @Override
+ public final void layout(int l, int t, int r, int b) {
+ if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
+ if (mTransition != null) {
+ mTransition.layoutChange(this);
+ }
+ super.layout(l, t, r, b);
+ } else {
+ // record the fact that we noop'd it; request layout when transition finishes
+ mLayoutCalledWhileSuppressed = true;
+ }
+ }
+
+ @Override
+ protected abstract void onLayout(boolean changed,
+ int l, int t, int r, int b);
+
+ /**
+ * Indicates whether the view group has the ability to animate its children
+ * after the first layout.
+ *
+ * @return true if the children can be animated, false otherwise
+ */
+ protected boolean canAnimate() {
+ return mLayoutAnimationController != null;
+ }
+
+ /**
+ * Runs the layout animation. Calling this method triggers a relayout of
+ * this view group.
+ */
+ public void startLayoutAnimation() {
+ if (mLayoutAnimationController != null) {
+ mGroupFlags |= FLAG_RUN_ANIMATION;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Schedules the layout animation to be played after the next layout pass
+ * of this view group. This can be used to restart the layout animation
+ * when the content of the view group changes or when the activity is
+ * paused and resumed.
+ */
+ public void scheduleLayoutAnimation() {
+ mGroupFlags |= FLAG_RUN_ANIMATION;
+ }
+
+ /**
+ * Sets the layout animation controller used to animate the group's
+ * children after the first layout.
+ *
+ * @param controller the animation controller
+ */
+ public void setLayoutAnimation(LayoutAnimationController controller) {
+ mLayoutAnimationController = controller;
+ if (mLayoutAnimationController != null) {
+ mGroupFlags |= FLAG_RUN_ANIMATION;
+ }
+ }
+
+ /**
+ * Returns the layout animation controller used to animate the group's
+ * children.
+ *
+ * @return the current animation controller
+ */
+ @InspectableProperty
+ public LayoutAnimationController getLayoutAnimation() {
+ return mLayoutAnimationController;
+ }
+
+ /**
+ * Indicates whether the children's drawing cache is used during a layout
+ * animation. By default, the drawing cache is enabled but this will prevent
+ * nested layout animations from working. To nest animations, you must disable
+ * the cache.
+ *
+ * @return true if the animation cache is enabled, false otherwise
+ *
+ * @see #setAnimationCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#M}, this property is ignored.
+ * Caching behavior of children may be controlled through {@link View#setLayerType(int, Paint)}.
+ */
+ @Deprecated
+ @InspectableProperty(name = "animationCache")
+ public boolean isAnimationCacheEnabled() {
+ return (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
+ }
+
+ /**
+ * Enables or disables the children's drawing cache during a layout animation.
+ * By default, the drawing cache is enabled but this will prevent nested
+ * layout animations from working. To nest animations, you must disable the
+ * cache.
+ *
+ * @param enabled true to enable the animation cache, false otherwise
+ *
+ * @see #isAnimationCacheEnabled()
+ * @see View#setDrawingCacheEnabled(boolean)
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#M}, this property is ignored.
+ * Caching behavior of children may be controlled through {@link View#setLayerType(int, Paint)}.
+ */
+ @Deprecated
+ public void setAnimationCacheEnabled(boolean enabled) {
+ setBooleanFlag(FLAG_ANIMATION_CACHE, enabled);
+ }
+
+ /**
+ * Indicates whether this ViewGroup will always try to draw its children using their
+ * drawing cache. By default this property is enabled.
+ *
+ * @return true if the animation cache is enabled, false otherwise
+ *
+ * @see #setAlwaysDrawnWithCacheEnabled(boolean)
+ * @see #setChildrenDrawnWithCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#M}, this property is ignored.
+ * Child views may no longer have their caching behavior disabled by parents.
+ */
+ @Deprecated
+ @InspectableProperty(name = "alwaysDrawnWithCache")
+ public boolean isAlwaysDrawnWithCacheEnabled() {
+ return (mGroupFlags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE;
+ }
+
+ /**
+ * Indicates whether this ViewGroup will always try to draw its children using their
+ * drawing cache. This property can be set to true when the cache rendering is
+ * slightly different from the children's normal rendering. Renderings can be different,
+ * for instance, when the cache's quality is set to low.
+ *
+ * When this property is disabled, the ViewGroup will use the drawing cache of its
+ * children only when asked to. It's usually the task of subclasses to tell ViewGroup
+ * when to start using the drawing cache and when to stop using it.
+ *
+ * @param always true to always draw with the drawing cache, false otherwise
+ *
+ * @see #isAlwaysDrawnWithCacheEnabled()
+ * @see #setChildrenDrawnWithCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ * @see View#setDrawingCacheQuality(int)
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#M}, this property is ignored.
+ * Child views may no longer have their caching behavior disabled by parents.
+ */
+ @Deprecated
+ public void setAlwaysDrawnWithCacheEnabled(boolean always) {
+ setBooleanFlag(FLAG_ALWAYS_DRAWN_WITH_CACHE, always);
+ }
+
+ /**
+ * Indicates whether the ViewGroup is currently drawing its children using
+ * their drawing cache.
+ *
+ * @return true if children should be drawn with their cache, false otherwise
+ *
+ * @see #setAlwaysDrawnWithCacheEnabled(boolean)
+ * @see #setChildrenDrawnWithCacheEnabled(boolean)
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#M}, this property is ignored.
+ * Child views may no longer be forced to cache their rendering state by their parents.
+ * Use {@link View#setLayerType(int, Paint)} on individual Views instead.
+ */
+ @Deprecated
+ protected boolean isChildrenDrawnWithCacheEnabled() {
+ return (mGroupFlags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE;
+ }
+
+ /**
+ * Tells the ViewGroup to draw its children using their drawing cache. This property
+ * is ignored when {@link #isAlwaysDrawnWithCacheEnabled()} is true. A child's drawing cache
+ * will be used only if it has been enabled.
+ *
+ * Subclasses should call this method to start and stop using the drawing cache when
+ * they perform performance sensitive operations, like scrolling or animating.
+ *
+ * @param enabled true if children should be drawn with their cache, false otherwise
+ *
+ * @see #setAlwaysDrawnWithCacheEnabled(boolean)
+ * @see #isChildrenDrawnWithCacheEnabled()
+ *
+ * @deprecated As of {@link android.os.Build.VERSION_CODES#M}, this property is ignored.
+ * Child views may no longer be forced to cache their rendering state by their parents.
+ * Use {@link View#setLayerType(int, Paint)} on individual Views instead.
+ */
+ @Deprecated
+ protected void setChildrenDrawnWithCacheEnabled(boolean enabled) {
+ setBooleanFlag(FLAG_CHILDREN_DRAWN_WITH_CACHE, enabled);
+ }
+
+ /**
+ * Indicates whether the ViewGroup is drawing its children in the order defined by
+ * {@link #getChildDrawingOrder(int, int)}.
+ *
+ * @return true if children drawing order is defined by {@link #getChildDrawingOrder(int, int)},
+ * false otherwise
+ *
+ * @see #setChildrenDrawingOrderEnabled(boolean)
+ * @see #getChildDrawingOrder(int, int)
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ protected boolean isChildrenDrawingOrderEnabled() {
+ return (mGroupFlags & FLAG_USE_CHILD_DRAWING_ORDER) == FLAG_USE_CHILD_DRAWING_ORDER;
+ }
+
+ /**
+ * Tells the ViewGroup whether to draw its children in the order defined by the method
+ * {@link #getChildDrawingOrder(int, int)}.
+ * <p>
+ * Note that {@link View#getZ() Z} reordering, done by {@link #dispatchDraw(Canvas)},
+ * will override custom child ordering done via this method.
+ *
+ * @param enabled true if the order of the children when drawing is determined by
+ * {@link #getChildDrawingOrder(int, int)}, false otherwise
+ *
+ * @see #isChildrenDrawingOrderEnabled()
+ * @see #getChildDrawingOrder(int, int)
+ */
+ protected void setChildrenDrawingOrderEnabled(boolean enabled) {
+ setBooleanFlag(FLAG_USE_CHILD_DRAWING_ORDER, enabled);
+ }
+
+ private boolean hasBooleanFlag(int flag) {
+ return (mGroupFlags & flag) == flag;
+ }
+
+ private void setBooleanFlag(int flag, boolean value) {
+ if (value) {
+ mGroupFlags |= flag;
+ } else {
+ mGroupFlags &= ~flag;
+ }
+ }
+
+ /**
+ * Returns an integer indicating what types of drawing caches are kept in memory.
+ *
+ * @see #setPersistentDrawingCache(int)
+ * @see #setAnimationCacheEnabled(boolean)
+ *
+ * @return one or a combination of {@link #PERSISTENT_NO_CACHE},
+ * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE}
+ * and {@link #PERSISTENT_ALL_CACHES}
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ @ViewDebug.ExportedProperty(category = "drawing", mapping = {
+ @ViewDebug.IntToString(from = PERSISTENT_NO_CACHE, to = "NONE"),
+ @ViewDebug.IntToString(from = PERSISTENT_ANIMATION_CACHE, to = "ANIMATION"),
+ @ViewDebug.IntToString(from = PERSISTENT_SCROLLING_CACHE, to = "SCROLLING"),
+ @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ALL")
+ })
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = PERSISTENT_NO_CACHE, name = "none"),
+ @EnumEntry(value = PERSISTENT_ANIMATION_CACHE, name = "animation"),
+ @EnumEntry(value = PERSISTENT_SCROLLING_CACHE, name = "scrolling"),
+ @EnumEntry(value = PERSISTENT_ALL_CACHES, name = "all"),
+ })
+ public int getPersistentDrawingCache() {
+ return mPersistentDrawingCache;
+ }
+
+ /**
+ * Indicates what types of drawing caches should be kept in memory after
+ * they have been created.
+ *
+ * @see #getPersistentDrawingCache()
+ * @see #setAnimationCacheEnabled(boolean)
+ *
+ * @param drawingCacheToKeep one or a combination of {@link #PERSISTENT_NO_CACHE},
+ * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE}
+ * and {@link #PERSISTENT_ALL_CACHES}
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
+ */
+ @Deprecated
+ public void setPersistentDrawingCache(int drawingCacheToKeep) {
+ mPersistentDrawingCache = drawingCacheToKeep & PERSISTENT_ALL_CACHES;
+ }
+
+ private void setLayoutMode(int layoutMode, boolean explicitly) {
+ mLayoutMode = layoutMode;
+ setBooleanFlag(FLAG_LAYOUT_MODE_WAS_EXPLICITLY_SET, explicitly);
+ }
+
+ /**
+ * Recursively traverse the view hierarchy, resetting the layoutMode of any
+ * descendants that had inherited a different layoutMode from a previous parent.
+ * Recursion terminates when a descendant's mode is:
+ * <ul>
+ * <li>Undefined</li>
+ * <li>The same as the root node's</li>
+ * <li>A mode that had been explicitly set</li>
+ * <ul/>
+ * The first two clauses are optimizations.
+ * @param layoutModeOfRoot
+ */
+ @Override
+ void invalidateInheritedLayoutMode(int layoutModeOfRoot) {
+ if (mLayoutMode == LAYOUT_MODE_UNDEFINED ||
+ mLayoutMode == layoutModeOfRoot ||
+ hasBooleanFlag(FLAG_LAYOUT_MODE_WAS_EXPLICITLY_SET)) {
+ return;
+ }
+ setLayoutMode(LAYOUT_MODE_UNDEFINED, false);
+
+ // apply recursively
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ getChildAt(i).invalidateInheritedLayoutMode(layoutModeOfRoot);
+ }
+ }
+
+ /**
+ * Returns the basis of alignment during layout operations on this ViewGroup:
+ * either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}.
+ * <p>
+ * If no layoutMode was explicitly set, either programmatically or in an XML resource,
+ * the method returns the layoutMode of the view's parent ViewGroup if such a parent exists,
+ * otherwise the method returns a default value of {@link #LAYOUT_MODE_CLIP_BOUNDS}.
+ *
+ * @return the layout mode to use during layout operations
+ *
+ * @see #setLayoutMode(int)
+ */
+ @InspectableProperty(enumMapping = {
+ @EnumEntry(value = LAYOUT_MODE_CLIP_BOUNDS, name = "clipBounds"),
+ @EnumEntry(value = LAYOUT_MODE_OPTICAL_BOUNDS, name = "opticalBounds")
+ })
+ public int getLayoutMode() {
+ if (mLayoutMode == LAYOUT_MODE_UNDEFINED) {
+ int inheritedLayoutMode = (mParent instanceof ViewGroup) ?
+ ((ViewGroup) mParent).getLayoutMode() : LAYOUT_MODE_DEFAULT;
+ setLayoutMode(inheritedLayoutMode, false);
+ }
+ return mLayoutMode;
+ }
+
+ /**
+ * Sets the basis of alignment during the layout of this ViewGroup.
+ * Valid values are either {@link #LAYOUT_MODE_CLIP_BOUNDS} or
+ * {@link #LAYOUT_MODE_OPTICAL_BOUNDS}.
+ *
+ * @param layoutMode the layout mode to use during layout operations
+ *
+ * @see #getLayoutMode()
+ * @attr ref android.R.styleable#ViewGroup_layoutMode
+ */
+ public void setLayoutMode(int layoutMode) {
+ if (mLayoutMode != layoutMode) {
+ invalidateInheritedLayoutMode(layoutMode);
+ setLayoutMode(layoutMode, layoutMode != LAYOUT_MODE_UNDEFINED);
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns a new set of layout parameters based on the supplied attributes set.
+ *
+ * @param attrs the attributes to build the layout parameters from
+ *
+ * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
+ * of its descendants
+ */
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Returns a safe set of layout parameters based on the supplied layout params.
+ * When a ViewGroup is passed a View whose layout params do not pass the test of
+ * {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method
+ * is invoked. This method should return a new set of layout params suitable for
+ * this ViewGroup, possibly by copying the appropriate attributes from the
+ * specified set of layout params.
+ *
+ * @param p The layout parameters to convert into a suitable set of layout parameters
+ * for this ViewGroup.
+ *
+ * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
+ * of its descendants
+ */
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return p;
+ }
+
+ /**
+ * Returns a set of default layout parameters. These parameters are requested
+ * when the View passed to {@link #addView(View)} has no layout parameters
+ * already set. If null is returned, an exception is thrown from addView.
+ *
+ * @return a set of default layout parameters or null
+ */
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected void debug(int depth) {
+ super.debug(depth);
+ String output;
+
+ if (mFocused != null) {
+ output = debugIndent(depth);
+ output += "mFocused";
+ Log.d(VIEW_LOG_TAG, output);
+ mFocused.debug(depth + 1);
+ }
+ if (mDefaultFocus != null) {
+ output = debugIndent(depth);
+ output += "mDefaultFocus";
+ Log.d(VIEW_LOG_TAG, output);
+ mDefaultFocus.debug(depth + 1);
+ }
+ if (mFocusedInCluster != null) {
+ output = debugIndent(depth);
+ output += "mFocusedInCluster";
+ Log.d(VIEW_LOG_TAG, output);
+ mFocusedInCluster.debug(depth + 1);
+ }
+ if (mChildrenCount != 0) {
+ output = debugIndent(depth);
+ output += "{";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+ int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ View child = mChildren[i];
+ child.debug(depth + 1);
+ }
+
+ if (mChildrenCount != 0) {
+ output = debugIndent(depth);
+ output += "}";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+ }
+
+ /**
+ * Returns the position in the group of the specified child view.
+ *
+ * @param child the view for which to get the position
+ * @return a positive integer representing the position of the view in the
+ * group, or -1 if the view does not exist in the group
+ */
+ public int indexOfChild(View child) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ if (children[i] == child) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the number of children in the group.
+ *
+ * @return a positive integer representing the number of children in
+ * the group
+ */
+ public int getChildCount() {
+ return mChildrenCount;
+ }
+
+ /**
+ * Returns the view at the specified position in the group.
+ *
+ * @param index the position at which to get the view from
+ * @return the view at the specified position or null if the position
+ * does not exist within the group
+ */
+ public View getChildAt(int index) {
+ if (index < 0 || index >= mChildrenCount) {
+ return null;
+ }
+ return mChildren[index];
+ }
+
+ /**
+ * Ask all of the children of this view to measure themselves, taking into
+ * account both the MeasureSpec requirements for this view and its padding.
+ * We skip children that are in the GONE state The heavy lifting is done in
+ * getChildMeasureSpec.
+ *
+ * @param widthMeasureSpec The width requirements for this view
+ * @param heightMeasureSpec The height requirements for this view
+ */
+ protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
+ final int size = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < size; ++i) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+
+ /**
+ * Ask one of the children of this view to measure itself, taking into
+ * account both the MeasureSpec requirements for this view and its padding.
+ * The heavy lifting is done in getChildMeasureSpec.
+ *
+ * @param child The child to measure
+ * @param parentWidthMeasureSpec The width requirements for this view
+ * @param parentHeightMeasureSpec The height requirements for this view
+ */
+ protected void measureChild(View child, int parentWidthMeasureSpec,
+ int parentHeightMeasureSpec) {
+ final LayoutParams lp = child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ mPaddingLeft + mPaddingRight, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ /**
+ * Ask one of the children of this view to measure itself, taking into
+ * account both the MeasureSpec requirements for this view and its padding
+ * and margins. The child must have MarginLayoutParams The heavy lifting is
+ * done in getChildMeasureSpec.
+ *
+ * @param child The child to measure
+ * @param parentWidthMeasureSpec The width requirements for this view
+ * @param widthUsed Extra space that has been used up by the parent
+ * horizontally (possibly by other children of the parent)
+ * @param parentHeightMeasureSpec The height requirements for this view
+ * @param heightUsed Extra space that has been used up by the parent
+ * vertically (possibly by other children of the parent)
+ */
+ protected void measureChildWithMargins(View child,
+ int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ /**
+ * Does the hard part of measureChildren: figuring out the MeasureSpec to
+ * pass to a particular child. This method figures out the right MeasureSpec
+ * for one dimension (height or width) of one child view.
+ *
+ * The goal is to combine information from our MeasureSpec with the
+ * LayoutParams of the child to get the best possible results. For example,
+ * if the this view knows its size (because its MeasureSpec has a mode of
+ * EXACTLY), and the child has indicated in its LayoutParams that it wants
+ * to be the same size as the parent, the parent should ask the child to
+ * layout given an exact size.
+ *
+ * @param spec The requirements for this view
+ * @param padding The padding of this view for the current dimension and
+ * margins, if applicable
+ * @param childDimension How big the child wants to be in the current
+ * dimension
+ * @return a MeasureSpec integer for the child
+ */
+ public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
+ int specMode = MeasureSpec.getMode(spec);
+ int specSize = MeasureSpec.getSize(spec);
+
+ int size = Math.max(0, specSize - padding);
+
+ int resultSize = 0;
+ int resultMode = 0;
+
+ switch (specMode) {
+ // Parent has imposed an exact size on us
+ case MeasureSpec.EXACTLY:
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.MATCH_PARENT) {
+ // Child wants to be our size. So be it.
+ resultSize = size;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ // Child wants to determine its own size. It can't be
+ // bigger than us.
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ }
+ break;
+
+ // Parent has imposed a maximum size on us
+ case MeasureSpec.AT_MOST:
+ if (childDimension >= 0) {
+ // Child wants a specific size... so be it
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.MATCH_PARENT) {
+ // Child wants to be our size, but our size is not fixed.
+ // Constrain child to not be bigger than us.
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ // Child wants to determine its own size. It can't be
+ // bigger than us.
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ }
+ break;
+
+ // Parent asked to see how big we want to be
+ case MeasureSpec.UNSPECIFIED:
+ if (childDimension >= 0) {
+ // Child wants a specific size... let them have it
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.MATCH_PARENT) {
+ // Child wants to be our size... find out how big it should
+ // be
+ resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ // Child wants to determine its own size.... find out how
+ // big it should be
+ resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+ break;
+ }
+ //noinspection ResourceType
+ return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+ }
+
+
+ /**
+ * Removes any pending animations for views that have been removed. Call
+ * this if you don't want animations for exiting views to stack up.
+ */
+ public void clearDisappearingChildren() {
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ if (disappearingChildren != null) {
+ final int count = disappearingChildren.size();
+ for (int i = 0; i < count; i++) {
+ final View view = disappearingChildren.get(i);
+ if (view.mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+ view.clearAnimation();
+ }
+ disappearingChildren.clear();
+ invalidate();
+ }
+ }
+
+ /**
+ * Add a view which is removed from mChildren but still needs animation
+ *
+ * @param v View to add
+ */
+ private void addDisappearingView(View v) {
+ ArrayList<View> disappearingChildren = mDisappearingChildren;
+
+ if (disappearingChildren == null) {
+ disappearingChildren = mDisappearingChildren = new ArrayList<View>();
+ }
+
+ disappearingChildren.add(v);
+ }
+
+ /**
+ * Cleanup a view when its animation is done. This may mean removing it from
+ * the list of disappearing views.
+ *
+ * @param view The view whose animation has finished
+ * @param animation The animation, cannot be null
+ */
+ void finishAnimatingView(final View view, Animation animation) {
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ if (disappearingChildren != null) {
+ if (disappearingChildren.contains(view)) {
+ disappearingChildren.remove(view);
+
+ if (view.mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ view.clearAnimation();
+ mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
+ }
+ }
+
+ if (animation != null && !animation.getFillAfter()) {
+ view.clearAnimation();
+ }
+
+ if ((view.mPrivateFlags & PFLAG_ANIMATION_STARTED) == PFLAG_ANIMATION_STARTED) {
+ view.onAnimationEnd();
+ // Should be performed by onAnimationEnd() but this avoid an infinite loop,
+ // so we'd rather be safe than sorry
+ view.mPrivateFlags &= ~PFLAG_ANIMATION_STARTED;
+ // Draw one more frame after the animation is done
+ mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
+ }
+ }
+
+ /**
+ * Utility function called by View during invalidation to determine whether a view that
+ * is invisible or gone should still be invalidated because it is being transitioned (and
+ * therefore still needs to be drawn).
+ */
+ boolean isViewTransitioning(View view) {
+ return (mTransitioningViews != null && mTransitioningViews.contains(view));
+ }
+
+ /**
+ * This method tells the ViewGroup that the given View object, which should have this
+ * ViewGroup as its parent,
+ * should be kept around (re-displayed when the ViewGroup draws its children) even if it
+ * is removed from its parent. This allows animations, such as those used by
+ * {@link android.app.Fragment} and {@link android.animation.LayoutTransition} to animate
+ * the removal of views. A call to this method should always be accompanied by a later call
+ * to {@link #endViewTransition(View)}, such as after an animation on the View has finished,
+ * so that the View finally gets removed.
+ *
+ * @param view The View object to be kept visible even if it gets removed from its parent.
+ */
+ public void startViewTransition(View view) {
+ if (view.mParent == this) {
+ if (mTransitioningViews == null) {
+ mTransitioningViews = new ArrayList<View>();
+ }
+ mTransitioningViews.add(view);
+ }
+ }
+
+ /**
+ * This method should always be called following an earlier call to
+ * {@link #startViewTransition(View)}. The given View is finally removed from its parent
+ * and will no longer be displayed. Note that this method does not perform the functionality
+ * of removing a view from its parent; it just discontinues the display of a View that
+ * has previously been removed.
+ *
+ * @return view The View object that has been removed but is being kept around in the visible
+ * hierarchy by an earlier call to {@link #startViewTransition(View)}.
+ */
+ public void endViewTransition(View view) {
+ if (mTransitioningViews != null) {
+ mTransitioningViews.remove(view);
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ if (disappearingChildren != null && disappearingChildren.contains(view)) {
+ disappearingChildren.remove(view);
+ if (mVisibilityChangingChildren != null &&
+ mVisibilityChangingChildren.contains(view)) {
+ mVisibilityChangingChildren.remove(view);
+ } else {
+ if (view.mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+ if (view.mParent != null) {
+ view.mParent = null;
+ }
+ }
+ invalidate();
+ }
+ }
+ }
+
+ private LayoutTransition.TransitionListener mLayoutTransitionListener =
+ new LayoutTransition.TransitionListener() {
+ @Override
+ public void startTransition(LayoutTransition transition, ViewGroup container,
+ View view, int transitionType) {
+ // We only care about disappearing items, since we need special logic to keep
+ // those items visible after they've been 'removed'
+ if (transitionType == LayoutTransition.DISAPPEARING) {
+ startViewTransition(view);
+ }
+ }
+
+ @Override
+ public void endTransition(LayoutTransition transition, ViewGroup container,
+ View view, int transitionType) {
+ if (mLayoutCalledWhileSuppressed && !transition.isChangingLayout()) {
+ requestLayout();
+ mLayoutCalledWhileSuppressed = false;
+ }
+ if (transitionType == LayoutTransition.DISAPPEARING && mTransitioningViews != null) {
+ endViewTransition(view);
+ }
+ }
+ };
+
+ /**
+ * Tells this ViewGroup to suppress all layout() calls until layout
+ * suppression is disabled with a later call to suppressLayout(false).
+ * When layout suppression is disabled, a requestLayout() call is sent
+ * if layout() was attempted while layout was being suppressed.
+ */
+ public void suppressLayout(boolean suppress) {
+ mSuppressLayout = suppress;
+ if (!suppress) {
+ if (mLayoutCalledWhileSuppressed) {
+ requestLayout();
+ mLayoutCalledWhileSuppressed = false;
+ }
+ }
+ }
+
+ /**
+ * Returns whether layout calls on this container are currently being
+ * suppressed, due to an earlier call to {@link #suppressLayout(boolean)}.
+ *
+ * @return true if layout calls are currently suppressed, false otherwise.
+ */
+ public boolean isLayoutSuppressed() {
+ return mSuppressLayout;
+ }
+
+ @Override
+ public boolean gatherTransparentRegion(Region region) {
+ // If no transparent regions requested, we are always opaque.
+ final boolean meOpaque = (mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0;
+ if (meOpaque && region == null) {
+ // The caller doesn't care about the region, so stop now.
+ return true;
+ }
+ super.gatherTransparentRegion(region);
+ // Instead of naively traversing the view tree, we have to traverse according to the Z
+ // order here. We need to go with the same order as dispatchDraw().
+ // One example is that after surfaceView punch a hole, we will still allow other views drawn
+ // on top of that hole. In this case, those other views should be able to cut the
+ // transparent region into smaller area.
+ final int childrenCount = mChildrenCount;
+ boolean noneOfTheChildrenAreTransparent = true;
+ if (childrenCount > 0) {
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = 0; i < childrenCount; i++) {
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
+ if (!child.gatherTransparentRegion(region)) {
+ noneOfTheChildrenAreTransparent = false;
+ }
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ }
+ return meOpaque || noneOfTheChildrenAreTransparent;
+ }
+
+ @Override
+ public void requestTransparentRegion(View child) {
+ if (child != null) {
+ child.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
+ if (mParent != null) {
+ mParent.requestTransparentRegion(this);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
+ final int childrenCount = mChildrenCount;
+ final ArrayList<View> preorderedList = buildTouchDispatchChildList();
+ final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
+ final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
+ if (child == view) {
+ // We've reached the target view.
+ break;
+ }
+ if (!child.canReceivePointerEvents()) {
+ // This child cannot be touched. Skip it.
+ continue;
+ }
+ applyOpToRegionByBounds(touchableRegion, child, Region.Op.DIFFERENCE);
+ }
+
+ // The touchable region should not exceed the bounds of its container.
+ applyOpToRegionByBounds(touchableRegion, this, Region.Op.INTERSECT);
+
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.subtractObscuredTouchableRegion(touchableRegion, this);
+ }
+ }
+
+ private static void applyOpToRegionByBounds(Region region, View view, Region.Op op) {
+ final int[] locationInWindow = new int[2];
+ view.getLocationInWindow(locationInWindow);
+ final int x = locationInWindow[0];
+ final int y = locationInWindow[1];
+ region.op(x, y, x + view.getWidth(), y + view.getHeight(), op);
+ }
+
+ @Override
+ public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
+ insets = super.dispatchApplyWindowInsets(insets);
+ if (insets.isConsumed()) {
+ return insets;
+ }
+ if (View.sBrokenInsetsDispatch) {
+ return brokenDispatchApplyWindowInsets(insets);
+ } else {
+ return newDispatchApplyWindowInsets(insets);
+ }
+ }
+
+ private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ insets = getChildAt(i).dispatchApplyWindowInsets(insets);
+ if (insets.isConsumed()) {
+ break;
+ }
+ }
+ return insets;
+ }
+
+ private WindowInsets newDispatchApplyWindowInsets(WindowInsets insets) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).dispatchApplyWindowInsets(insets);
+ }
+ return insets;
+ }
+
+ @Override
+ public void setWindowInsetsAnimationCallback(
+ @Nullable WindowInsetsAnimation.Callback callback) {
+ super.setWindowInsetsAnimationCallback(callback);
+ mInsetsAnimationDispatchMode = callback != null
+ ? callback.getDispatchMode()
+ : DISPATCH_MODE_CONTINUE_ON_SUBTREE;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean hasWindowInsetsAnimationCallback() {
+ if (super.hasWindowInsetsAnimationCallback()) {
+ return true;
+ }
+
+ // If we are root-level content view that fits insets, we imitate consuming behavior, so
+ // no child will retrieve window insets animation callback.
+ // See dispatchWindowInsetsAnimationPrepare.
+ boolean isOptionalFitSystemWindows = (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) != 0
+ || isFrameworkOptionalFitsSystemWindows();
+ if (isOptionalFitSystemWindows && mAttachInfo != null
+ && mAttachInfo.mContentOnApplyWindowInsetsListener != null
+ && (getWindowSystemUiVisibility() & SYSTEM_UI_LAYOUT_FLAGS) == 0) {
+ return false;
+ }
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ if (getChildAt(i).hasWindowInsetsAnimationCallback()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void dispatchWindowInsetsAnimationPrepare(
+ @NonNull WindowInsetsAnimation animation) {
+ super.dispatchWindowInsetsAnimationPrepare(animation);
+
+ // If we are root-level content view that fits insets, set dispatch mode to stop to imitate
+ // consume behavior.
+ boolean isOptionalFitSystemWindows = (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) != 0
+ || isFrameworkOptionalFitsSystemWindows();
+ if (isOptionalFitSystemWindows && mAttachInfo != null
+ && getListenerInfo().mWindowInsetsAnimationCallback == null
+ && mAttachInfo.mContentOnApplyWindowInsetsListener != null
+ && (getWindowSystemUiVisibility() & SYSTEM_UI_LAYOUT_FLAGS) == 0) {
+ mInsetsAnimationDispatchMode = DISPATCH_MODE_STOP;
+ return;
+ }
+
+ if (mInsetsAnimationDispatchMode == DISPATCH_MODE_STOP) {
+ return;
+ }
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).dispatchWindowInsetsAnimationPrepare(animation);
+ }
+ }
+
+ @Override
+ @NonNull
+ public Bounds dispatchWindowInsetsAnimationStart(
+ @NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds) {
+ bounds = super.dispatchWindowInsetsAnimationStart(animation, bounds);
+ if (mInsetsAnimationDispatchMode == DISPATCH_MODE_STOP) {
+ return bounds;
+ }
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).dispatchWindowInsetsAnimationStart(animation, bounds);
+ }
+ return bounds;
+ }
+
+ @Override
+ @NonNull
+ public WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets,
+ @NonNull List<WindowInsetsAnimation> runningAnimations) {
+ insets = super.dispatchWindowInsetsAnimationProgress(insets, runningAnimations);
+ if (mInsetsAnimationDispatchMode == DISPATCH_MODE_STOP) {
+ return insets;
+ }
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).dispatchWindowInsetsAnimationProgress(insets, runningAnimations);
+ }
+ return insets;
+ }
+
+ @Override
+ public void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation) {
+ super.dispatchWindowInsetsAnimationEnd(animation);
+ if (mInsetsAnimationDispatchMode == DISPATCH_MODE_STOP) {
+ return;
+ }
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).dispatchWindowInsetsAnimationEnd(animation);
+ }
+ }
+
+ /**
+ * Handle the scroll capture search request by checking this view if applicable, then to each
+ * child view.
+ *
+ * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to
+ * the parent
+ * @param windowOffset the offset of this view within the window
+ * @param targets accepts potential scroll capture targets; {@link Consumer#accept
+ * results.accept} may be called zero or more times on the calling
+ * thread before onScrollCaptureSearch returns
+ */
+ @Override
+ public void dispatchScrollCaptureSearch(
+ @NonNull Rect localVisibleRect, @NonNull Point windowOffset,
+ @NonNull Consumer<ScrollCaptureTarget> targets) {
+
+ if (getClipToPadding() && !localVisibleRect.intersect(mPaddingLeft, mPaddingTop,
+ (mRight - mLeft) - mPaddingRight, (mBottom - mTop) - mPaddingBottom)) {
+ return;
+ }
+
+ // Dispatch to self first.
+ super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+
+ // Skip children if descendants excluded.
+ if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) != 0) {
+ return;
+ }
+
+ final Rect tmpRect = getTempRect();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ // Only visible views can be captured.
+ if (child.getVisibility() != View.VISIBLE) {
+ continue;
+ }
+ // Offset the given rectangle (in parent's local coordinates) into child's coordinate
+ // space and clip the result to the child View's bounds, padding and clipRect as needed.
+ // If the resulting rectangle is not empty, the request is forwarded to the child.
+
+ // copy local visible rect for modification and dispatch
+ tmpRect.set(localVisibleRect);
+
+ // transform to child coords
+ final Point childWindowOffset = getTempPoint();
+ childWindowOffset.set(windowOffset.x, windowOffset.y);
+
+ final int dx = child.mLeft - mScrollX;
+ final int dy = child.mTop - mScrollY;
+
+ tmpRect.offset(-dx, -dy);
+ childWindowOffset.offset(dx, dy);
+
+ boolean rectIsVisible = true;
+
+ // Clip to child bounds
+ if (getClipChildren()) {
+ rectIsVisible = tmpRect.intersect(0, 0, child.getWidth(), child.getHeight());
+ }
+
+ if (rectIsVisible) {
+ child.dispatchScrollCaptureSearch(tmpRect, childWindowOffset, targets);
+ }
+ }
+ }
+
+ /**
+ * Returns the animation listener to which layout animation events are
+ * sent.
+ *
+ * @return an {@link android.view.animation.Animation.AnimationListener}
+ */
+ public Animation.AnimationListener getLayoutAnimationListener() {
+ return mAnimationListener;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if ((mGroupFlags & FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE) != 0) {
+ if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0) {
+ throw new IllegalStateException("addStateFromChildren cannot be enabled if a"
+ + " child has duplicateParentState set to true");
+ }
+
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & DUPLICATE_PARENT_STATE) != 0) {
+ child.refreshDrawableState();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].jumpDrawablesToCurrentState();
+ }
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) == 0) {
+ return super.onCreateDrawableState(extraSpace);
+ }
+
+ int need = 0;
+ int n = getChildCount();
+ for (int i = 0; i < n; i++) {
+ int[] childState = getChildAt(i).getDrawableState();
+
+ if (childState != null) {
+ need += childState.length;
+ }
+ }
+
+ int[] state = super.onCreateDrawableState(extraSpace + need);
+
+ for (int i = 0; i < n; i++) {
+ int[] childState = getChildAt(i).getDrawableState();
+
+ if (childState != null) {
+ state = mergeDrawableStates(state, childState);
+ }
+ }
+
+ return state;
+ }
+
+ /**
+ * Sets whether this ViewGroup's drawable states also include
+ * its children's drawable states. This is used, for example, to
+ * make a group appear to be focused when its child EditText or button
+ * is focused.
+ */
+ public void setAddStatesFromChildren(boolean addsStates) {
+ if (addsStates) {
+ mGroupFlags |= FLAG_ADD_STATES_FROM_CHILDREN;
+ } else {
+ mGroupFlags &= ~FLAG_ADD_STATES_FROM_CHILDREN;
+ }
+
+ refreshDrawableState();
+ }
+
+ /**
+ * Returns whether this ViewGroup's drawable states also include
+ * its children's drawable states. This is used, for example, to
+ * make a group appear to be focused when its child EditText or button
+ * is focused.
+ */
+ @InspectableProperty
+ public boolean addStatesFromChildren() {
+ return (mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0;
+ }
+
+ /**
+ * If {@link #addStatesFromChildren} is true, refreshes this group's
+ * drawable state (to include the states from its children).
+ */
+ @Override
+ public void childDrawableStateChanged(View child) {
+ if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0) {
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Specifies the animation listener to which layout animation events must
+ * be sent. Only
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationStart(Animation)}
+ * and
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationEnd(Animation)}
+ * are invoked.
+ *
+ * @param animationListener the layout animation listener
+ */
+ public void setLayoutAnimationListener(Animation.AnimationListener animationListener) {
+ mAnimationListener = animationListener;
+ }
+
+ /**
+ * This method is called by LayoutTransition when there are 'changing' animations that need
+ * to start after the layout/setup phase. The request is forwarded to the ViewAncestor, who
+ * starts all pending transitions prior to the drawing phase in the current traversal.
+ *
+ * @param transition The LayoutTransition to be started on the next traversal.
+ *
+ * @hide
+ */
+ public void requestTransitionStart(LayoutTransition transition) {
+ ViewRootImpl viewAncestor = getViewRootImpl();
+ if (viewAncestor != null) {
+ viewAncestor.requestTransitionStart(transition);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean resolveRtlPropertiesIfNeeded() {
+ final boolean result = super.resolveRtlPropertiesIfNeeded();
+ // We dont need to resolve the children RTL properties if nothing has changed for the parent
+ if (result) {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited()) {
+ child.resolveRtlPropertiesIfNeeded();
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean resolveLayoutDirection() {
+ final boolean result = super.resolveLayoutDirection();
+ if (result) {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited()) {
+ child.resolveLayoutDirection();
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean resolveTextDirection() {
+ final boolean result = super.resolveTextDirection();
+ if (result) {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isTextDirectionInherited()) {
+ child.resolveTextDirection();
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean resolveTextAlignment() {
+ final boolean result = super.resolveTextAlignment();
+ if (result) {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isTextAlignmentInherited()) {
+ child.resolveTextAlignment();
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void resolvePadding() {
+ super.resolvePadding();
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited() && !child.isPaddingResolved()) {
+ child.resolvePadding();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void resolveDrawables() {
+ super.resolveDrawables();
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited() && !child.areDrawablesResolved()) {
+ child.resolveDrawables();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void resolveLayoutParams() {
+ super.resolveLayoutParams();
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ child.resolveLayoutParams();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ @Override
+ public void resetResolvedLayoutDirection() {
+ super.resetResolvedLayoutDirection();
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited()) {
+ child.resetResolvedLayoutDirection();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ @Override
+ public void resetResolvedTextDirection() {
+ super.resetResolvedTextDirection();
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isTextDirectionInherited()) {
+ child.resetResolvedTextDirection();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ @Override
+ public void resetResolvedTextAlignment() {
+ super.resetResolvedTextAlignment();
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isTextAlignmentInherited()) {
+ child.resetResolvedTextAlignment();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ @Override
+ public void resetResolvedPadding() {
+ super.resetResolvedPadding();
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited()) {
+ child.resetResolvedPadding();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ @Override
+ protected void resetResolvedDrawables() {
+ super.resetResolvedDrawables();
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.isLayoutDirectionInherited()) {
+ child.resetResolvedDrawables();
+ }
+ }
+ }
+
+ /**
+ * Return true if the pressed state should be delayed for children or descendants of this
+ * ViewGroup. Generally, this should be done for containers that can scroll, such as a List.
+ * This prevents the pressed state from appearing when the user is actually trying to scroll
+ * the content.
+ *
+ * The default implementation returns true for compatibility reasons. Subclasses that do
+ * not scroll should generally override this method and return false.
+ */
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ mNestedScrollAxes = axes;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * <p>The default implementation of onStopNestedScroll calls
+ * {@link #stopNestedScroll()} to halt any recursive nested scrolling in progress.</p>
+ */
+ @Override
+ public void onStopNestedScroll(View child) {
+ // Stop any recursive nested scrolling.
+ stopNestedScroll();
+ mNestedScrollAxes = 0;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ // Re-dispatch up the tree by default
+ dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ // Re-dispatch up the tree by default
+ dispatchNestedPreScroll(dx, dy, consumed, null);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ // Re-dispatch up the tree by default
+ return dispatchNestedFling(velocityX, velocityY, consumed);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ // Re-dispatch up the tree by default
+ return dispatchNestedPreFling(velocityX, velocityY);
+ }
+
+ /**
+ * Return the current axes of nested scrolling for this ViewGroup.
+ *
+ * <p>A ViewGroup returning something other than {@link #SCROLL_AXIS_NONE} is currently
+ * acting as a nested scrolling parent for one or more descendant views in the hierarchy.</p>
+ *
+ * @return Flags indicating the current axes of nested scrolling
+ * @see #SCROLL_AXIS_HORIZONTAL
+ * @see #SCROLL_AXIS_VERTICAL
+ * @see #SCROLL_AXIS_NONE
+ */
+ public int getNestedScrollAxes() {
+ return mNestedScrollAxes;
+ }
+
+ /** @hide */
+ protected void onSetLayoutParams(View child, LayoutParams layoutParams) {
+ requestLayout();
+ }
+
+ /** @hide */
+ @Override
+ public void captureTransitioningViews(List<View> transitioningViews) {
+ if (getVisibility() != View.VISIBLE) {
+ return;
+ }
+ if (isTransitionGroup()) {
+ transitioningViews.add(this);
+ } else {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ child.captureTransitioningViews(transitioningViews);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void findNamedViews(Map<String, View> namedElements) {
+ if (getVisibility() != VISIBLE && mGhostView == null) {
+ return;
+ }
+ super.findNamedViews(namedElements);
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ child.findNamedViews(namedElements);
+ }
+ }
+
+ @Override
+ boolean hasUnhandledKeyListener() {
+ return (mChildUnhandledKeyListeners > 0) || super.hasUnhandledKeyListener();
+ }
+
+ void incrementChildUnhandledKeyListeners() {
+ mChildUnhandledKeyListeners += 1;
+ if (mChildUnhandledKeyListeners == 1) {
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).incrementChildUnhandledKeyListeners();
+ }
+ }
+ }
+
+ void decrementChildUnhandledKeyListeners() {
+ mChildUnhandledKeyListeners -= 1;
+ if (mChildUnhandledKeyListeners == 0) {
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).decrementChildUnhandledKeyListeners();
+ }
+ }
+ }
+
+ @Override
+ View dispatchUnhandledKeyEvent(KeyEvent evt) {
+ if (!hasUnhandledKeyListener()) {
+ return null;
+ }
+ ArrayList<View> orderedViews = buildOrderedChildList();
+ if (orderedViews != null) {
+ try {
+ for (int i = orderedViews.size() - 1; i >= 0; --i) {
+ View v = orderedViews.get(i);
+ View consumer = v.dispatchUnhandledKeyEvent(evt);
+ if (consumer != null) {
+ return consumer;
+ }
+ }
+ } finally {
+ orderedViews.clear();
+ }
+ } else {
+ for (int i = getChildCount() - 1; i >= 0; --i) {
+ View v = getChildAt(i);
+ View consumer = v.dispatchUnhandledKeyEvent(evt);
+ if (consumer != null) {
+ return consumer;
+ }
+ }
+ }
+ if (onUnhandledKeyEvent(evt)) {
+ return this;
+ }
+ return null;
+ }
+
+ /**
+ * LayoutParams are used by views to tell their parents how they want to be
+ * laid out. See
+ * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes}
+ * for a list of all child view attributes that this class supports.
+ *
+ * <p>
+ * The base LayoutParams class just describes how big the view wants to be
+ * for both width and height. For each dimension, it can specify one of:
+ * <ul>
+ * <li>FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which
+ * means that the view wants to be as big as its parent (minus padding)
+ * <li> WRAP_CONTENT, which means that the view wants to be just big enough
+ * to enclose its content (plus padding)
+ * <li> an exact number
+ * </ul>
+ * There are subclasses of LayoutParams for different subclasses of
+ * ViewGroup. For example, AbsoluteLayout has its own subclass of
+ * LayoutParams which adds an X and Y value.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating user interface layouts, read the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html">XML Layouts</a> developer
+ * guide.</p></div>
+ *
+ * @attr ref android.R.styleable#ViewGroup_Layout_layout_height
+ * @attr ref android.R.styleable#ViewGroup_Layout_layout_width
+ */
+ public static class LayoutParams {
+ /**
+ * Special value for the height or width requested by a View.
+ * FILL_PARENT means that the view wants to be as big as its parent,
+ * minus the parent's padding, if any. This value is deprecated
+ * starting in API Level 8 and replaced by {@link #MATCH_PARENT}.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ @Deprecated
+ public static final int FILL_PARENT = -1;
+
+ /**
+ * Special value for the height or width requested by a View.
+ * MATCH_PARENT means that the view wants to be as big as its parent,
+ * minus the parent's padding, if any. Introduced in API Level 8.
+ */
+ public static final int MATCH_PARENT = -1;
+
+ /**
+ * Special value for the height or width requested by a View.
+ * WRAP_CONTENT means that the view wants to be just large enough to fit
+ * its own internal content, taking its own padding into account.
+ */
+ public static final int WRAP_CONTENT = -2;
+
+ /**
+ * Information about how wide the view wants to be. Can be one of the
+ * constants FILL_PARENT (replaced by MATCH_PARENT
+ * in API Level 8) or WRAP_CONTENT, or an exact size.
+ */
+ @ViewDebug.ExportedProperty(category = "layout", mapping = {
+ @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
+ @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
+ })
+ @InspectableProperty(name = "layout_width", enumMapping = {
+ @EnumEntry(name = "match_parent", value = MATCH_PARENT),
+ @EnumEntry(name = "wrap_content", value = WRAP_CONTENT)
+ })
+ public int width;
+
+ /**
+ * Information about how tall the view wants to be. Can be one of the
+ * constants FILL_PARENT (replaced by MATCH_PARENT
+ * in API Level 8) or WRAP_CONTENT, or an exact size.
+ */
+ @ViewDebug.ExportedProperty(category = "layout", mapping = {
+ @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
+ @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
+ })
+ @InspectableProperty(name = "layout_height", enumMapping = {
+ @EnumEntry(name = "match_parent", value = MATCH_PARENT),
+ @EnumEntry(name = "wrap_content", value = WRAP_CONTENT)
+ })
+ public int height;
+
+ /**
+ * Used to animate layouts.
+ */
+ public LayoutAnimationController.AnimationParameters layoutAnimationParameters;
+
+ /**
+ * Creates a new set of layout parameters. The values are extracted from
+ * the supplied attributes set and context. The XML attributes mapped
+ * to this set of layout parameters are:
+ *
+ * <ul>
+ * <li><code>layout_width</code>: the width, either an exact value,
+ * {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by
+ * {@link #MATCH_PARENT} in API Level 8)</li>
+ * <li><code>layout_height</code>: the height, either an exact value,
+ * {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by
+ * {@link #MATCH_PARENT} in API Level 8)</li>
+ * </ul>
+ *
+ * @param c the application environment
+ * @param attrs the set of attributes from which to extract the layout
+ * parameters' values
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
+ setBaseAttributes(a,
+ R.styleable.ViewGroup_Layout_layout_width,
+ R.styleable.ViewGroup_Layout_layout_height);
+ a.recycle();
+ }
+
+ /**
+ * Creates a new set of layout parameters with the specified width
+ * and height.
+ *
+ * @param width the width, either {@link #WRAP_CONTENT},
+ * {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
+ * API Level 8), or a fixed size in pixels
+ * @param height the height, either {@link #WRAP_CONTENT},
+ * {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
+ * API Level 8), or a fixed size in pixels
+ */
+ public LayoutParams(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ /**
+ * Copy constructor. Clones the width and height values of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(LayoutParams source) {
+ this.width = source.width;
+ this.height = source.height;
+ }
+
+ /**
+ * Used internally by MarginLayoutParams.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ LayoutParams() {
+ }
+
+ /**
+ * Extracts the layout parameters from the supplied attributes.
+ *
+ * @param a the style attributes to extract the parameters from
+ * @param widthAttr the identifier of the width attribute
+ * @param heightAttr the identifier of the height attribute
+ */
+ protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
+ width = a.getLayoutDimension(widthAttr, "layout_width");
+ height = a.getLayoutDimension(heightAttr, "layout_height");
+ }
+
+ /**
+ * Resolve layout parameters depending on the layout direction. Subclasses that care about
+ * layoutDirection changes should override this method. The default implementation does
+ * nothing.
+ *
+ * @param layoutDirection the direction of the layout
+ *
+ * {@link View#LAYOUT_DIRECTION_LTR}
+ * {@link View#LAYOUT_DIRECTION_RTL}
+ */
+ public void resolveLayoutDirection(int layoutDirection) {
+ }
+
+ /**
+ * Returns a String representation of this set of layout parameters.
+ *
+ * @param output the String to prepend to the internal representation
+ * @return a String with the following format: output +
+ * "ViewGroup.LayoutParams={ width=WIDTH, height=HEIGHT }"
+ *
+ * @hide
+ */
+ public String debug(String output) {
+ return output + "ViewGroup.LayoutParams={ width="
+ + sizeToString(width) + ", height=" + sizeToString(height) + " }";
+ }
+
+ /**
+ * Use {@code canvas} to draw suitable debugging annotations for these LayoutParameters.
+ *
+ * @param view the view that contains these layout parameters
+ * @param canvas the canvas on which to draw
+ *
+ * @hide
+ */
+ public void onDebugDraw(View view, Canvas canvas, Paint paint) {
+ }
+
+ /**
+ * Converts the specified size to a readable String.
+ *
+ * @param size the size to convert
+ * @return a String instance representing the supplied size
+ *
+ * @hide
+ */
+ protected static String sizeToString(int size) {
+ if (size == WRAP_CONTENT) {
+ return "wrap-content";
+ }
+ if (size == MATCH_PARENT) {
+ return "match-parent";
+ }
+ return String.valueOf(size);
+ }
+
+ /** @hide */
+ void encode(@NonNull ViewHierarchyEncoder encoder) {
+ encoder.beginObject(this);
+ encodeProperties(encoder);
+ encoder.endObject();
+ }
+
+ /** @hide */
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ encoder.addProperty("width", width);
+ encoder.addProperty("height", height);
+ }
+ }
+
+ /**
+ * Per-child layout information for layouts that support margins.
+ * See
+ * {@link android.R.styleable#ViewGroup_MarginLayout ViewGroup Margin Layout Attributes}
+ * for a list of all child view attributes that this class supports.
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_margin
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginHorizontal
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginVertical
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginLeft
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginTop
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginRight
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginBottom
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginStart
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginEnd
+ */
+ public static class MarginLayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * The left margin in pixels of the child. Margin values should be positive.
+ * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
+ * to this field.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @InspectableProperty(name = "layout_marginLeft")
+ public int leftMargin;
+
+ /**
+ * The top margin in pixels of the child. Margin values should be positive.
+ * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
+ * to this field.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @InspectableProperty(name = "layout_marginTop")
+ public int topMargin;
+
+ /**
+ * The right margin in pixels of the child. Margin values should be positive.
+ * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
+ * to this field.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @InspectableProperty(name = "layout_marginRight")
+ public int rightMargin;
+
+ /**
+ * The bottom margin in pixels of the child. Margin values should be positive.
+ * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
+ * to this field.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @InspectableProperty(name = "layout_marginBottom")
+ public int bottomMargin;
+
+ /**
+ * The start margin in pixels of the child. Margin values should be positive.
+ * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
+ * to this field.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage
+ private int startMargin = DEFAULT_MARGIN_RELATIVE;
+
+ /**
+ * The end margin in pixels of the child. Margin values should be positive.
+ * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
+ * to this field.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ @UnsupportedAppUsage
+ private int endMargin = DEFAULT_MARGIN_RELATIVE;
+
+ /**
+ * The default start and end margin.
+ * @hide
+ */
+ public static final int DEFAULT_MARGIN_RELATIVE = Integer.MIN_VALUE;
+
+ /**
+ * Bit 0: layout direction
+ * Bit 1: layout direction
+ * Bit 2: left margin undefined
+ * Bit 3: right margin undefined
+ * Bit 4: is RTL compatibility mode
+ * Bit 5: need resolution
+ *
+ * Bit 6 to 7 not used
+ *
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "layout", flagMapping = {
+ @ViewDebug.FlagToString(mask = LAYOUT_DIRECTION_MASK,
+ equals = LAYOUT_DIRECTION_MASK, name = "LAYOUT_DIRECTION"),
+ @ViewDebug.FlagToString(mask = LEFT_MARGIN_UNDEFINED_MASK,
+ equals = LEFT_MARGIN_UNDEFINED_MASK, name = "LEFT_MARGIN_UNDEFINED_MASK"),
+ @ViewDebug.FlagToString(mask = RIGHT_MARGIN_UNDEFINED_MASK,
+ equals = RIGHT_MARGIN_UNDEFINED_MASK, name = "RIGHT_MARGIN_UNDEFINED_MASK"),
+ @ViewDebug.FlagToString(mask = RTL_COMPATIBILITY_MODE_MASK,
+ equals = RTL_COMPATIBILITY_MODE_MASK, name = "RTL_COMPATIBILITY_MODE_MASK"),
+ @ViewDebug.FlagToString(mask = NEED_RESOLUTION_MASK,
+ equals = NEED_RESOLUTION_MASK, name = "NEED_RESOLUTION_MASK")
+ }, formatToHexString = true)
+ byte mMarginFlags;
+
+ private static final int LAYOUT_DIRECTION_MASK = 0x00000003;
+ private static final int LEFT_MARGIN_UNDEFINED_MASK = 0x00000004;
+ private static final int RIGHT_MARGIN_UNDEFINED_MASK = 0x00000008;
+ private static final int RTL_COMPATIBILITY_MODE_MASK = 0x00000010;
+ private static final int NEED_RESOLUTION_MASK = 0x00000020;
+
+ private static final int DEFAULT_MARGIN_RESOLVED = 0;
+ private static final int UNDEFINED_MARGIN = DEFAULT_MARGIN_RELATIVE;
+
+ /**
+ * Creates a new set of layout parameters. The values are extracted from
+ * the supplied attributes set and context.
+ *
+ * @param c the application environment
+ * @param attrs the set of attributes from which to extract the layout
+ * parameters' values
+ */
+ public MarginLayoutParams(Context c, AttributeSet attrs) {
+ super();
+
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
+ setBaseAttributes(a,
+ R.styleable.ViewGroup_MarginLayout_layout_width,
+ R.styleable.ViewGroup_MarginLayout_layout_height);
+
+ int margin = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
+ if (margin >= 0) {
+ leftMargin = margin;
+ topMargin = margin;
+ rightMargin= margin;
+ bottomMargin = margin;
+ } else {
+ int horizontalMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
+ int verticalMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
+
+ if (horizontalMargin >= 0) {
+ leftMargin = horizontalMargin;
+ rightMargin = horizontalMargin;
+ } else {
+ leftMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
+ UNDEFINED_MARGIN);
+ if (leftMargin == UNDEFINED_MARGIN) {
+ mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
+ leftMargin = DEFAULT_MARGIN_RESOLVED;
+ }
+ rightMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginRight,
+ UNDEFINED_MARGIN);
+ if (rightMargin == UNDEFINED_MARGIN) {
+ mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
+ rightMargin = DEFAULT_MARGIN_RESOLVED;
+ }
+ }
+
+ startMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginStart,
+ DEFAULT_MARGIN_RELATIVE);
+ endMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
+ DEFAULT_MARGIN_RELATIVE);
+
+ if (verticalMargin >= 0) {
+ topMargin = verticalMargin;
+ bottomMargin = verticalMargin;
+ } else {
+ topMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginTop,
+ DEFAULT_MARGIN_RESOLVED);
+ bottomMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
+ DEFAULT_MARGIN_RESOLVED);
+ }
+
+ if (isMarginRelative()) {
+ mMarginFlags |= NEED_RESOLUTION_MASK;
+ }
+ }
+
+ final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport();
+ final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
+ if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) {
+ mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK;
+ }
+
+ // Layout direction is LTR by default
+ mMarginFlags |= LAYOUT_DIRECTION_LTR;
+
+ a.recycle();
+ }
+
+ public MarginLayoutParams(int width, int height) {
+ super(width, height);
+
+ mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
+ mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
+
+ mMarginFlags &= ~NEED_RESOLUTION_MASK;
+ mMarginFlags &= ~RTL_COMPATIBILITY_MODE_MASK;
+ }
+
+ /**
+ * Copy constructor. Clones the width, height and margin values of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public MarginLayoutParams(MarginLayoutParams source) {
+ this.width = source.width;
+ this.height = source.height;
+
+ this.leftMargin = source.leftMargin;
+ this.topMargin = source.topMargin;
+ this.rightMargin = source.rightMargin;
+ this.bottomMargin = source.bottomMargin;
+ this.startMargin = source.startMargin;
+ this.endMargin = source.endMargin;
+
+ this.mMarginFlags = source.mMarginFlags;
+ }
+
+ public MarginLayoutParams(LayoutParams source) {
+ super(source);
+
+ mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
+ mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
+
+ mMarginFlags &= ~NEED_RESOLUTION_MASK;
+ mMarginFlags &= ~RTL_COMPATIBILITY_MODE_MASK;
+ }
+
+ /**
+ * @hide Used internally.
+ */
+ public final void copyMarginsFrom(MarginLayoutParams source) {
+ this.leftMargin = source.leftMargin;
+ this.topMargin = source.topMargin;
+ this.rightMargin = source.rightMargin;
+ this.bottomMargin = source.bottomMargin;
+ this.startMargin = source.startMargin;
+ this.endMargin = source.endMargin;
+
+ this.mMarginFlags = source.mMarginFlags;
+ }
+
+ /**
+ * Sets the margins, in pixels. A call to {@link android.view.View#requestLayout()} needs
+ * to be done so that the new margins are taken into account. Left and right margins may be
+ * overridden by {@link android.view.View#requestLayout()} depending on layout direction.
+ * Margin values should be positive.
+ *
+ * @param left the left margin size
+ * @param top the top margin size
+ * @param right the right margin size
+ * @param bottom the bottom margin size
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginLeft
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginTop
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginRight
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginBottom
+ */
+ public void setMargins(int left, int top, int right, int bottom) {
+ leftMargin = left;
+ topMargin = top;
+ rightMargin = right;
+ bottomMargin = bottom;
+ mMarginFlags &= ~LEFT_MARGIN_UNDEFINED_MASK;
+ mMarginFlags &= ~RIGHT_MARGIN_UNDEFINED_MASK;
+ if (isMarginRelative()) {
+ mMarginFlags |= NEED_RESOLUTION_MASK;
+ } else {
+ mMarginFlags &= ~NEED_RESOLUTION_MASK;
+ }
+ }
+
+ /**
+ * Sets the relative margins, in pixels. A call to {@link android.view.View#requestLayout()}
+ * needs to be done so that the new relative margins are taken into account. Left and right
+ * margins may be overridden by {@link android.view.View#requestLayout()} depending on
+ * layout direction. Margin values should be positive.
+ *
+ * @param start the start margin size
+ * @param top the top margin size
+ * @param end the right margin size
+ * @param bottom the bottom margin size
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginStart
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginTop
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginEnd
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginBottom
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setMarginsRelative(int start, int top, int end, int bottom) {
+ startMargin = start;
+ topMargin = top;
+ endMargin = end;
+ bottomMargin = bottom;
+ mMarginFlags |= NEED_RESOLUTION_MASK;
+ }
+
+ /**
+ * Sets the relative start margin. Margin values should be positive.
+ *
+ * @param start the start margin size
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginStart
+ */
+ public void setMarginStart(int start) {
+ startMargin = start;
+ mMarginFlags |= NEED_RESOLUTION_MASK;
+ }
+
+ /**
+ * Returns the start margin in pixels.
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginStart
+ *
+ * @return the start margin in pixels.
+ */
+ public int getMarginStart() {
+ if (startMargin != DEFAULT_MARGIN_RELATIVE) return startMargin;
+ if ((mMarginFlags & NEED_RESOLUTION_MASK) == NEED_RESOLUTION_MASK) {
+ doResolveMargins();
+ }
+ switch(mMarginFlags & LAYOUT_DIRECTION_MASK) {
+ case View.LAYOUT_DIRECTION_RTL:
+ return rightMargin;
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ return leftMargin;
+ }
+ }
+
+ /**
+ * Sets the relative end margin. Margin values should be positive.
+ *
+ * @param end the end margin size
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginEnd
+ */
+ public void setMarginEnd(int end) {
+ endMargin = end;
+ mMarginFlags |= NEED_RESOLUTION_MASK;
+ }
+
+ /**
+ * Returns the end margin in pixels.
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginEnd
+ *
+ * @return the end margin in pixels.
+ */
+ public int getMarginEnd() {
+ if (endMargin != DEFAULT_MARGIN_RELATIVE) return endMargin;
+ if ((mMarginFlags & NEED_RESOLUTION_MASK) == NEED_RESOLUTION_MASK) {
+ doResolveMargins();
+ }
+ switch(mMarginFlags & LAYOUT_DIRECTION_MASK) {
+ case View.LAYOUT_DIRECTION_RTL:
+ return leftMargin;
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ return rightMargin;
+ }
+ }
+
+ /**
+ * Check if margins are relative.
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginStart
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginEnd
+ *
+ * @return true if either marginStart or marginEnd has been set.
+ */
+ public boolean isMarginRelative() {
+ return (startMargin != DEFAULT_MARGIN_RELATIVE || endMargin != DEFAULT_MARGIN_RELATIVE);
+ }
+
+ /**
+ * Set the layout direction
+ * @param layoutDirection the layout direction.
+ * Should be either {@link View#LAYOUT_DIRECTION_LTR}
+ * or {@link View#LAYOUT_DIRECTION_RTL}.
+ */
+ public void setLayoutDirection(int layoutDirection) {
+ if (layoutDirection != View.LAYOUT_DIRECTION_LTR &&
+ layoutDirection != View.LAYOUT_DIRECTION_RTL) return;
+ if (layoutDirection != (mMarginFlags & LAYOUT_DIRECTION_MASK)) {
+ mMarginFlags &= ~LAYOUT_DIRECTION_MASK;
+ mMarginFlags |= (layoutDirection & LAYOUT_DIRECTION_MASK);
+ if (isMarginRelative()) {
+ mMarginFlags |= NEED_RESOLUTION_MASK;
+ } else {
+ mMarginFlags &= ~NEED_RESOLUTION_MASK;
+ }
+ }
+ }
+
+ /**
+ * Retuns the layout direction. Can be either {@link View#LAYOUT_DIRECTION_LTR} or
+ * {@link View#LAYOUT_DIRECTION_RTL}.
+ *
+ * @return the layout direction.
+ */
+ public int getLayoutDirection() {
+ return (mMarginFlags & LAYOUT_DIRECTION_MASK);
+ }
+
+ /**
+ * This will be called by {@link android.view.View#requestLayout()}. Left and Right margins
+ * may be overridden depending on layout direction.
+ */
+ @Override
+ public void resolveLayoutDirection(int layoutDirection) {
+ setLayoutDirection(layoutDirection);
+
+ // No relative margin or pre JB-MR1 case or no need to resolve, just dont do anything
+ // Will use the left and right margins if no relative margin is defined.
+ if (!isMarginRelative() ||
+ (mMarginFlags & NEED_RESOLUTION_MASK) != NEED_RESOLUTION_MASK) return;
+
+ // Proceed with resolution
+ doResolveMargins();
+ }
+
+ private void doResolveMargins() {
+ if ((mMarginFlags & RTL_COMPATIBILITY_MODE_MASK) == RTL_COMPATIBILITY_MODE_MASK) {
+ // if left or right margins are not defined and if we have some start or end margin
+ // defined then use those start and end margins.
+ if ((mMarginFlags & LEFT_MARGIN_UNDEFINED_MASK) == LEFT_MARGIN_UNDEFINED_MASK
+ && startMargin > DEFAULT_MARGIN_RELATIVE) {
+ leftMargin = startMargin;
+ }
+ if ((mMarginFlags & RIGHT_MARGIN_UNDEFINED_MASK) == RIGHT_MARGIN_UNDEFINED_MASK
+ && endMargin > DEFAULT_MARGIN_RELATIVE) {
+ rightMargin = endMargin;
+ }
+ } else {
+ // We have some relative margins (either the start one or the end one or both). So use
+ // them and override what has been defined for left and right margins. If either start
+ // or end margin is not defined, just set it to default "0".
+ switch(mMarginFlags & LAYOUT_DIRECTION_MASK) {
+ case View.LAYOUT_DIRECTION_RTL:
+ leftMargin = (endMargin > DEFAULT_MARGIN_RELATIVE) ?
+ endMargin : DEFAULT_MARGIN_RESOLVED;
+ rightMargin = (startMargin > DEFAULT_MARGIN_RELATIVE) ?
+ startMargin : DEFAULT_MARGIN_RESOLVED;
+ break;
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ leftMargin = (startMargin > DEFAULT_MARGIN_RELATIVE) ?
+ startMargin : DEFAULT_MARGIN_RESOLVED;
+ rightMargin = (endMargin > DEFAULT_MARGIN_RELATIVE) ?
+ endMargin : DEFAULT_MARGIN_RESOLVED;
+ break;
+ }
+ }
+ mMarginFlags &= ~NEED_RESOLUTION_MASK;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isLayoutRtl() {
+ return ((mMarginFlags & LAYOUT_DIRECTION_MASK) == View.LAYOUT_DIRECTION_RTL);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onDebugDraw(View view, Canvas canvas, Paint paint) {
+ Insets oi = isLayoutModeOptical(view.mParent) ? view.getOpticalInsets() : Insets.NONE;
+
+ fillDifference(canvas,
+ view.getLeft() + oi.left,
+ view.getTop() + oi.top,
+ view.getRight() - oi.right,
+ view.getBottom() - oi.bottom,
+ leftMargin,
+ topMargin,
+ rightMargin,
+ bottomMargin,
+ paint);
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("leftMargin", leftMargin);
+ encoder.addProperty("topMargin", topMargin);
+ encoder.addProperty("rightMargin", rightMargin);
+ encoder.addProperty("bottomMargin", bottomMargin);
+ encoder.addProperty("startMargin", startMargin);
+ encoder.addProperty("endMargin", endMargin);
+ }
+ }
+
+ /* Describes a touched view and the ids of the pointers that it has captured.
+ *
+ * This code assumes that pointer ids are always in the range 0..31 such that
+ * it can use a bitfield to track which pointer ids are present.
+ * As it happens, the lower layers of the input dispatch pipeline also use the
+ * same trick so the assumption should be safe here...
+ */
+ private static final class TouchTarget {
+ private static final int MAX_RECYCLED = 32;
+ private static final Object sRecycleLock = new Object[0];
+ private static TouchTarget sRecycleBin;
+ private static int sRecycledCount;
+
+ public static final int ALL_POINTER_IDS = -1; // all ones
+
+ // The touched child view.
+ @UnsupportedAppUsage
+ public View child;
+
+ // The combined bit mask of pointer ids for all pointers captured by the target.
+ public int pointerIdBits;
+
+ // The next target in the target list.
+ public TouchTarget next;
+
+ @UnsupportedAppUsage
+ private TouchTarget() {
+ }
+
+ public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
+ if (child == null) {
+ throw new IllegalArgumentException("child must be non-null");
+ }
+
+ final TouchTarget target;
+ synchronized (sRecycleLock) {
+ if (sRecycleBin == null) {
+ target = new TouchTarget();
+ } else {
+ target = sRecycleBin;
+ sRecycleBin = target.next;
+ sRecycledCount--;
+ target.next = null;
+ }
+ }
+ target.child = child;
+ target.pointerIdBits = pointerIdBits;
+ return target;
+ }
+
+ public void recycle() {
+ if (child == null) {
+ throw new IllegalStateException("already recycled once");
+ }
+
+ synchronized (sRecycleLock) {
+ if (sRecycledCount < MAX_RECYCLED) {
+ next = sRecycleBin;
+ sRecycleBin = this;
+ sRecycledCount += 1;
+ } else {
+ next = null;
+ }
+ child = null;
+ }
+ }
+ }
+
+ /* Describes a hovered view. */
+ private static final class HoverTarget {
+ private static final int MAX_RECYCLED = 32;
+ private static final Object sRecycleLock = new Object[0];
+ private static HoverTarget sRecycleBin;
+ private static int sRecycledCount;
+
+ // The hovered child view.
+ public View child;
+
+ // The next target in the target list.
+ public HoverTarget next;
+
+ private HoverTarget() {
+ }
+
+ public static HoverTarget obtain(@NonNull View child) {
+ if (child == null) {
+ throw new IllegalArgumentException("child must be non-null");
+ }
+
+ final HoverTarget target;
+ synchronized (sRecycleLock) {
+ if (sRecycleBin == null) {
+ target = new HoverTarget();
+ } else {
+ target = sRecycleBin;
+ sRecycleBin = target.next;
+ sRecycledCount--;
+ target.next = null;
+ }
+ }
+ target.child = child;
+ return target;
+ }
+
+ public void recycle() {
+ if (child == null) {
+ throw new IllegalStateException("already recycled once");
+ }
+
+ synchronized (sRecycleLock) {
+ if (sRecycledCount < MAX_RECYCLED) {
+ next = sRecycleBin;
+ sRecycleBin = this;
+ sRecycledCount += 1;
+ } else {
+ next = null;
+ }
+ child = null;
+ }
+ }
+ }
+
+ /**
+ * Pooled class that to hold the children for autifill.
+ */
+ private static class ChildListForAutoFillOrContentCapture extends ArrayList<View> {
+ private static final int MAX_POOL_SIZE = 32;
+
+ private static final Pools.SimplePool<ChildListForAutoFillOrContentCapture> sPool =
+ new Pools.SimplePool<>(MAX_POOL_SIZE);
+
+ public static ChildListForAutoFillOrContentCapture obtain() {
+ ChildListForAutoFillOrContentCapture list = sPool.acquire();
+ if (list == null) {
+ list = new ChildListForAutoFillOrContentCapture();
+ }
+ return list;
+ }
+
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ }
+ }
+
+ /**
+ * Pooled class that orderes the children of a ViewGroup from start
+ * to end based on how they are laid out and the layout direction.
+ */
+ static class ChildListForAccessibility {
+
+ private static final int MAX_POOL_SIZE = 32;
+
+ private static final SynchronizedPool<ChildListForAccessibility> sPool =
+ new SynchronizedPool<ChildListForAccessibility>(MAX_POOL_SIZE);
+
+ private final ArrayList<View> mChildren = new ArrayList<View>();
+
+ private final ArrayList<ViewLocationHolder> mHolders = new ArrayList<ViewLocationHolder>();
+
+ public static ChildListForAccessibility obtain(ViewGroup parent, boolean sort) {
+ ChildListForAccessibility list = sPool.acquire();
+ if (list == null) {
+ list = new ChildListForAccessibility();
+ }
+ list.init(parent, sort);
+ return list;
+ }
+
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ public int getChildCount() {
+ return mChildren.size();
+ }
+
+ public View getChildAt(int index) {
+ return mChildren.get(index);
+ }
+
+ private void init(ViewGroup parent, boolean sort) {
+ ArrayList<View> children = mChildren;
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ children.add(child);
+ }
+ if (sort) {
+ ArrayList<ViewLocationHolder> holders = mHolders;
+ for (int i = 0; i < childCount; i++) {
+ View child = children.get(i);
+ ViewLocationHolder holder = ViewLocationHolder.obtain(parent, child);
+ holders.add(holder);
+ }
+ sort(holders);
+ for (int i = 0; i < childCount; i++) {
+ ViewLocationHolder holder = holders.get(i);
+ children.set(i, holder.mView);
+ holder.recycle();
+ }
+ holders.clear();
+ }
+ }
+
+ private void sort(ArrayList<ViewLocationHolder> holders) {
+ // This is gross but the least risky solution. The current comparison
+ // strategy breaks transitivity but produces very good results. Coming
+ // up with a new strategy requires time which we do not have, so ...
+ try {
+ ViewLocationHolder.setComparisonStrategy(
+ ViewLocationHolder.COMPARISON_STRATEGY_STRIPE);
+ Collections.sort(holders);
+ } catch (IllegalArgumentException iae) {
+ // Note that in practice this occurs extremely rarely in a couple
+ // of pathological cases.
+ ViewLocationHolder.setComparisonStrategy(
+ ViewLocationHolder.COMPARISON_STRATEGY_LOCATION);
+ Collections.sort(holders);
+ }
+ }
+
+ private void clear() {
+ mChildren.clear();
+ }
+ }
+
+ /**
+ * Pooled class that holds a View and its location with respect to
+ * a specified root. This enables sorting of views based on their
+ * coordinates without recomputing the position relative to the root
+ * on every comparison.
+ */
+ static class ViewLocationHolder implements Comparable<ViewLocationHolder> {
+
+ private static final int MAX_POOL_SIZE = 32;
+
+ private static final SynchronizedPool<ViewLocationHolder> sPool =
+ new SynchronizedPool<ViewLocationHolder>(MAX_POOL_SIZE);
+
+ public static final int COMPARISON_STRATEGY_STRIPE = 1;
+
+ public static final int COMPARISON_STRATEGY_LOCATION = 2;
+
+ private static int sComparisonStrategy = COMPARISON_STRATEGY_STRIPE;
+
+ private final Rect mLocation = new Rect();
+
+ private ViewGroup mRoot;
+
+ public View mView;
+
+ private int mLayoutDirection;
+
+ public static ViewLocationHolder obtain(ViewGroup root, View view) {
+ ViewLocationHolder holder = sPool.acquire();
+ if (holder == null) {
+ holder = new ViewLocationHolder();
+ }
+ holder.init(root, view);
+ return holder;
+ }
+
+ public static void setComparisonStrategy(int strategy) {
+ sComparisonStrategy = strategy;
+ }
+
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ @Override
+ public int compareTo(ViewLocationHolder another) {
+ // This instance is greater than an invalid argument.
+ if (another == null) {
+ return 1;
+ }
+
+ int boundsResult = compareBoundsOfTree(this, another);
+ if (boundsResult != 0) {
+ return boundsResult;
+ }
+
+ // Just break the tie somehow. The accessibility ids are unique
+ // and stable, hence this is deterministic tie breaking.
+ return mView.getAccessibilityViewId() - another.mView.getAccessibilityViewId();
+ }
+
+ /**
+ * Compare two views based on their bounds. Use the bounds of their children to break ties.
+ *
+ * @param holder1 Holder of first view to compare
+ * @param holder2 Holder of second view to compare. Must have the same root as holder1.
+ * @return The compare result, with equality if no good comparison was found.
+ */
+ private static int compareBoundsOfTree(
+ ViewLocationHolder holder1, ViewLocationHolder holder2) {
+ if (sComparisonStrategy == COMPARISON_STRATEGY_STRIPE) {
+ // First is above second.
+ if (holder1.mLocation.bottom - holder2.mLocation.top <= 0) {
+ return -1;
+ }
+ // First is below second.
+ if (holder1.mLocation.top - holder2.mLocation.bottom >= 0) {
+ return 1;
+ }
+ }
+
+ // We are ordering left-to-right, top-to-bottom.
+ if (holder1.mLayoutDirection == LAYOUT_DIRECTION_LTR) {
+ final int leftDifference = holder1.mLocation.left - holder2.mLocation.left;
+ if (leftDifference != 0) {
+ return leftDifference;
+ }
+ } else { // RTL
+ final int rightDifference = holder1.mLocation.right - holder2.mLocation.right;
+ if (rightDifference != 0) {
+ return -rightDifference;
+ }
+ }
+ // We are ordering left-to-right, top-to-bottom.
+ final int topDifference = holder1.mLocation.top - holder2.mLocation.top;
+ if (topDifference != 0) {
+ return topDifference;
+ }
+ // Break tie by height.
+ final int heightDiference = holder1.mLocation.height() - holder2.mLocation.height();
+ if (heightDiference != 0) {
+ return -heightDiference;
+ }
+ // Break tie by width.
+ final int widthDifference = holder1.mLocation.width() - holder2.mLocation.width();
+ if (widthDifference != 0) {
+ return -widthDifference;
+ }
+
+ // Find a child of each view with different screen bounds.
+ final Rect view1Bounds = new Rect();
+ final Rect view2Bounds = new Rect();
+ final Rect tempRect = new Rect();
+ holder1.mView.getBoundsOnScreen(view1Bounds, true);
+ holder2.mView.getBoundsOnScreen(view2Bounds, true);
+ final View child1 = holder1.mView.findViewByPredicateTraversal((view) -> {
+ view.getBoundsOnScreen(tempRect, true);
+ return !tempRect.equals(view1Bounds);
+ }, null);
+ final View child2 = holder2.mView.findViewByPredicateTraversal((view) -> {
+ view.getBoundsOnScreen(tempRect, true);
+ return !tempRect.equals(view2Bounds);
+ }, null);
+
+
+ // Compare the children recursively
+ if ((child1 != null) && (child2 != null)) {
+ final ViewLocationHolder childHolder1 =
+ ViewLocationHolder.obtain(holder1.mRoot, child1);
+ final ViewLocationHolder childHolder2 =
+ ViewLocationHolder.obtain(holder1.mRoot, child2);
+ return compareBoundsOfTree(childHolder1, childHolder2);
+ }
+
+ // If only one has a child, use that one
+ if (child1 != null) {
+ return 1;
+ }
+
+ if (child2 != null) {
+ return -1;
+ }
+
+ // Give up
+ return 0;
+ }
+
+ private void init(ViewGroup root, View view) {
+ Rect viewLocation = mLocation;
+ view.getDrawingRect(viewLocation);
+ root.offsetDescendantRectToMyCoords(view, viewLocation);
+ mView = view;
+ mRoot = root;
+ mLayoutDirection = root.getLayoutDirection();
+ }
+
+ private void clear() {
+ mView = null;
+ mRoot = null;
+ mLocation.set(0, 0, 0, 0);
+ }
+ }
+
+ private static void drawRect(Canvas canvas, Paint paint, int x1, int y1, int x2, int y2) {
+ if (sDebugLines== null) {
+ // TODO: This won't work with multiple UI threads in a single process
+ sDebugLines = new float[16];
+ }
+
+ sDebugLines[0] = x1;
+ sDebugLines[1] = y1;
+ sDebugLines[2] = x2;
+ sDebugLines[3] = y1;
+
+ sDebugLines[4] = x2;
+ sDebugLines[5] = y1;
+ sDebugLines[6] = x2;
+ sDebugLines[7] = y2;
+
+ sDebugLines[8] = x2;
+ sDebugLines[9] = y2;
+ sDebugLines[10] = x1;
+ sDebugLines[11] = y2;
+
+ sDebugLines[12] = x1;
+ sDebugLines[13] = y2;
+ sDebugLines[14] = x1;
+ sDebugLines[15] = y1;
+
+ canvas.drawLines(sDebugLines, paint);
+ }
+
+ /** @hide */
+ @Override
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("focus:descendantFocusability", getDescendantFocusability());
+ encoder.addProperty("drawing:clipChildren", getClipChildren());
+ encoder.addProperty("drawing:clipToPadding", getClipToPadding());
+ encoder.addProperty("drawing:childrenDrawingOrderEnabled", isChildrenDrawingOrderEnabled());
+ encoder.addProperty("drawing:persistentDrawingCache", getPersistentDrawingCache());
+
+ int n = getChildCount();
+ encoder.addProperty("meta:__childCount__", (short)n);
+ for (int i = 0; i < n; i++) {
+ encoder.addPropertyKey("meta:__child__" + i);
+ getChildAt(i).encode(encoder);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public final void onDescendantUnbufferedRequested() {
+ // First look at the focused child for focused events
+ int focusedChildNonPointerSource = InputDevice.SOURCE_CLASS_NONE;
+ if (mFocused != null) {
+ focusedChildNonPointerSource = mFocused.mUnbufferedInputSource
+ & (~InputDevice.SOURCE_CLASS_POINTER);
+ }
+ mUnbufferedInputSource = focusedChildNonPointerSource;
+
+ // Request unbuffered dispatch for pointer events for this view if any child requested
+ // unbuffered dispatch for pointer events. This is because we can't expect that the pointer
+ // source would dispatch to the focused view.
+ for (int i = 0; i < mChildrenCount; i++) {
+ final View child = mChildren[i];
+ if ((child.mUnbufferedInputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ mUnbufferedInputSource |= InputDevice.SOURCE_CLASS_POINTER;
+ break;
+ }
+ }
+ if (mParent != null) {
+ mParent.onDescendantUnbufferedRequested();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The implementation calls {@link #dispatchCreateViewTranslationRequest} for all the child
+ * views.
+ */
+ @Override
+ public void dispatchCreateViewTranslationRequest(@NonNull Map<AutofillId, long[]> viewIds,
+ @NonNull @DataFormat int[] supportedFormats,
+ @Nullable TranslationCapability capability,
+ @NonNull List<ViewTranslationRequest> requests) {
+ super.dispatchCreateViewTranslationRequest(viewIds, supportedFormats, capability, requests);
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return;
+ }
+ for (int i = 0; i < childCount; ++i) {
+ final View child = getChildAt(i);
+ child.dispatchCreateViewTranslationRequest(viewIds, supportedFormats, capability,
+ requests);
+ }
+ }
+}
diff --git a/android/view/ViewGroupOverlay.java b/android/view/ViewGroupOverlay.java
new file mode 100644
index 0000000..c2807ba
--- /dev/null
+++ b/android/view/ViewGroupOverlay.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A group overlay is an extra layer that sits on top of a ViewGroup
+ * (the "host view") which is drawn after all other content in that view
+ * (including the view group's children). Interaction with the overlay
+ * layer is done by adding and removing views and drawables.
+ *
+ * <p>ViewGroupOverlay is a subclass of {@link ViewOverlay}, adding the ability to
+ * manage views for overlays on ViewGroups, in addition to the drawable
+ * support in ViewOverlay.</p>
+ *
+ * @see ViewGroup#getOverlay()
+ */
+public class ViewGroupOverlay extends ViewOverlay {
+
+ ViewGroupOverlay(Context context, View hostView) {
+ super(context, hostView);
+ }
+
+ /**
+ * Adds a {@code View} to the overlay. The bounds of the added view should be
+ * relative to the host view. Any view added to the overlay should be
+ * removed when it is no longer needed or no longer visible.
+ *
+ * <p>Views in the overlay are visual-only; they do not receive input
+ * events and do not participate in focus traversal. Overlay views
+ * are intended to be transient, such as might be needed by a temporary
+ * animation effect.</p>
+ *
+ * <p>If the view has a parent, the view will be removed from that parent
+ * before being added to the overlay. Also, if that parent is attached
+ * in the current view hierarchy, the view will be repositioned
+ * such that it is in the same relative location inside the activity. For
+ * example, if the view's current parent lies 100 pixels to the right
+ * and 200 pixels down from the origin of the overlay's
+ * host view, then the view will be offset by (100, 200).</p>
+ *
+ * <p>{@code View}s added with this API will be drawn in the order they were
+ * added. Drawing of the overlay views will happen before drawing of any of the
+ * {@code Drawable}s added with {@link #add(Drawable)} API even if a call to
+ * this API happened after the call to {@link #add(Drawable)}.</p>
+ *
+ * <p>Passing <code>null</code> parameter will result in an
+ * {@link IllegalArgumentException} being thrown.</p>
+ *
+ * @param view The {@code View} to be added to the overlay. The added view will be
+ * drawn when the overlay is drawn.
+ * @see #remove(View)
+ * @see ViewOverlay#add(Drawable)
+ */
+ public void add(@NonNull View view) {
+ mOverlayViewGroup.add(view);
+ }
+
+ /**
+ * Removes the specified {@code View} from the overlay. Passing <code>null</code> parameter
+ * will result in an {@link IllegalArgumentException} being thrown.
+ *
+ * @param view The {@code View} to be removed from the overlay.
+ * @see #add(View)
+ * @see ViewOverlay#remove(Drawable)
+ */
+ public void remove(@NonNull View view) {
+ mOverlayViewGroup.remove(view);
+ }
+}
diff --git a/android/view/ViewGroup_Delegate.java b/android/view/ViewGroup_Delegate.java
new file mode 100644
index 0000000..e71af61
--- /dev/null
+++ b/android/view/ViewGroup_Delegate.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.resources.Density;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap_Delegate;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Path_Delegate;
+import android.graphics.Rect;
+import android.view.animation.Transformation;
+import android.view.shadow.HighQualityShadowPainter;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link ViewGroup}
+ * <p/>
+ * Through the layoutlib_create tool, the original methods of ViewGroup have been replaced by calls
+ * to methods of the same name in this delegate class.
+ */
+public class ViewGroup_Delegate {
+
+ /**
+ * Overrides the original drawChild call in ViewGroup to draw the shadow.
+ */
+ @LayoutlibDelegate
+ /*package*/ static boolean drawChild(ViewGroup thisVG, Canvas canvas, View child,
+ long drawingTime) {
+ if (child.getZ() > thisVG.getZ()) {
+ // The background's bounds are set lazily. Make sure they are set correctly so that
+ // the outline obtained is correct.
+ child.setBackgroundBounds();
+ ViewOutlineProvider outlineProvider = child.getOutlineProvider();
+ if (outlineProvider != null) {
+ Outline outline = child.mAttachInfo.mTmpOutline;
+ outlineProvider.getOutline(child, outline);
+ if (outline.mPath != null || (outline.mRect != null && !outline.mRect.isEmpty())) {
+ int restoreTo = transformCanvas(thisVG, canvas, child);
+ drawShadow(thisVG, canvas, child, outline);
+ canvas.restoreToCount(restoreTo);
+ }
+ }
+ }
+ return thisVG.drawChild_Original(canvas, child, drawingTime);
+ }
+
+ private static void drawShadow(ViewGroup parent, Canvas canvas, View child,
+ Outline outline) {
+ boolean highQualityShadow = false;
+ boolean enableShadow = true;
+ float elevation = getElevation(child, parent);
+ Context bridgeContext = parent.getContext();
+ if (bridgeContext instanceof BridgeContext) {
+ highQualityShadow = ((BridgeContext) bridgeContext).isHighQualityShadows();
+ enableShadow = ((BridgeContext) bridgeContext).isShadowsEnabled();
+ }
+
+ if (!enableShadow) {
+ return;
+ }
+
+ if(outline.mMode == Outline.MODE_ROUND_RECT && outline.mRect != null) {
+ if (highQualityShadow) {
+ float densityDpi = bridgeContext.getResources().getDisplayMetrics().densityDpi;
+ HighQualityShadowPainter.paintRectShadow(
+ parent, outline, elevation, canvas, child.getAlpha(), densityDpi);
+ } else {
+ RectShadowPainter.paintShadow(outline, elevation, canvas, child.getAlpha());
+ }
+ return;
+ }
+
+ BufferedImage shadow = null;
+ if (outline.mPath != null) {
+ shadow = getPathShadow(outline, canvas, elevation, child.getAlpha());
+ }
+ if (shadow == null) {
+ return;
+ }
+ Bitmap bitmap = Bitmap_Delegate.createBitmap(shadow, false,
+ Density.getEnum(canvas.getDensity()));
+ canvas.save();
+ Rect clipBounds = canvas.getClipBounds();
+ Rect newBounds = new Rect(clipBounds);
+ newBounds.inset((int)-elevation, (int)-elevation);
+ canvas.clipRectUnion(newBounds);
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ canvas.restore();
+ }
+
+ private static float getElevation(View child, ViewGroup parent) {
+ return child.getZ() - parent.getZ();
+ }
+
+ private static BufferedImage getPathShadow(Outline outline, Canvas canvas, float elevation,
+ float alpha) {
+ Rect clipBounds = canvas.getClipBounds();
+ if (clipBounds.isEmpty()) {
+ return null;
+ }
+ BufferedImage image = new BufferedImage(clipBounds.width(), clipBounds.height(),
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics2D graphics = image.createGraphics();
+ graphics.draw(Path_Delegate.getDelegate(outline.mPath.mNativePath).getJavaShape());
+ graphics.dispose();
+ return ShadowPainter.createDropShadow(image, (int) elevation, alpha);
+ }
+
+ // Copied from android.view.View#draw(Canvas, ViewGroup, long) and removed code paths
+ // which were never taken. Ideally, we should hook up the shadow code in the same method so
+ // that we don't have to transform the canvas twice.
+ private static int transformCanvas(ViewGroup thisVG, Canvas canvas, View child) {
+ final int restoreTo = canvas.save();
+ final boolean childHasIdentityMatrix = child.hasIdentityMatrix();
+ int flags = thisVG.mGroupFlags;
+ Transformation transformToApply = null;
+ boolean concatMatrix = false;
+ if ((flags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
+ final Transformation t = thisVG.getChildTransformation();
+ final boolean hasTransform = thisVG.getChildStaticTransformation(child, t);
+ if (hasTransform) {
+ final int transformType = t.getTransformationType();
+ transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
+ concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
+ }
+ }
+ concatMatrix |= childHasIdentityMatrix;
+
+ canvas.translate(child.mLeft, child.mTop);
+ float alpha = child.getAlpha() * child.getTransitionAlpha();
+
+ if (transformToApply != null || alpha < 1 || !childHasIdentityMatrix) {
+ if (transformToApply != null || !childHasIdentityMatrix) {
+
+ if (transformToApply != null) {
+ if (concatMatrix) {
+ canvas.concat(transformToApply.getMatrix());
+ }
+ }
+ if (!childHasIdentityMatrix) {
+ canvas.concat(child.getMatrix());
+ }
+
+ }
+ }
+ return restoreTo;
+ }
+}
diff --git a/android/view/ViewHierarchyEncoder.java b/android/view/ViewHierarchyEncoder.java
new file mode 100644
index 0000000..ace05a6
--- /dev/null
+++ b/android/view/ViewHierarchyEncoder.java
@@ -0,0 +1,223 @@
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link ViewHierarchyEncoder} is a serializer that is tailored towards writing out
+ * view hierarchies (the view tree, along with the properties for each view) to a stream.
+ *
+ * It is typically used as follows:
+ * <pre>
+ * ViewHierarchyEncoder e = new ViewHierarchyEncoder();
+ *
+ * for (View view : views) {
+ * e.beginObject(view);
+ * e.addProperty("prop1", value);
+ * ...
+ * e.endObject();
+ * }
+ *
+ * // repeat above snippet for each view, finally end with:
+ * e.endStream();
+ * </pre>
+ *
+ * <p>On the stream, a snippet such as the above gets encoded as a series of Map's (one
+ * corresponding to each view) with the property name as the key and the property value
+ * as the value.
+ *
+ * <p>Since the property names are practically the same across all views, rather than using
+ * the property name directly as the key, we use a short integer id corresponding to each
+ * property name as the key. A final map is added at the end which contains the mapping
+ * from the integer to its property name.
+ *
+ * <p>A value is encoded as a single byte type identifier followed by the encoding of the
+ * value. Only primitive types are supported as values, in addition to the Map type.
+ *
+ * @hide
+ */
+public class ViewHierarchyEncoder {
+ // Prefixes for simple primitives. These match the JNI definitions.
+ private static final byte SIG_BOOLEAN = 'Z';
+ private static final byte SIG_BYTE = 'B';
+ private static final byte SIG_SHORT = 'S';
+ private static final byte SIG_INT = 'I';
+ private static final byte SIG_LONG = 'J';
+ private static final byte SIG_FLOAT = 'F';
+ private static final byte SIG_DOUBLE = 'D';
+
+ // Prefixes for some commonly used objects
+ private static final byte SIG_STRING = 'R';
+
+ private static final byte SIG_MAP = 'M'; // a map with an short key
+ private static final short SIG_END_MAP = 0;
+
+ private final DataOutputStream mStream;
+
+ private final Map<String,Short> mPropertyNames = new HashMap<String, Short>(200);
+ private short mPropertyId = 1;
+ private Charset mCharset = Charset.forName("utf-8");
+
+ private boolean mUserPropertiesEnabled = true;
+
+ public ViewHierarchyEncoder(@NonNull ByteArrayOutputStream stream) {
+ mStream = new DataOutputStream(stream);
+ }
+
+ public void setUserPropertiesEnabled(boolean enabled) {
+ mUserPropertiesEnabled = enabled;
+ }
+
+ public void beginObject(@NonNull Object o) {
+ startPropertyMap();
+ addProperty("meta:__name__", o.getClass().getName());
+ addProperty("meta:__hash__", o.hashCode());
+ }
+
+ public void endObject() {
+ endPropertyMap();
+ }
+
+ public void endStream() {
+ // write out the string table
+ startPropertyMap();
+ addProperty("__name__", "propertyIndex");
+ for (Map.Entry<String,Short> entry : mPropertyNames.entrySet()) {
+ writeShort(entry.getValue());
+ writeString(entry.getKey());
+ }
+ endPropertyMap();
+ }
+
+ @UnsupportedAppUsage
+ public void addProperty(@NonNull String name, boolean v) {
+ writeShort(createPropertyIndex(name));
+ writeBoolean(v);
+ }
+
+ public void addProperty(@NonNull String name, short s) {
+ writeShort(createPropertyIndex(name));
+ writeShort(s);
+ }
+
+ @UnsupportedAppUsage
+ public void addProperty(@NonNull String name, int v) {
+ writeShort(createPropertyIndex(name));
+ writeInt(v);
+ }
+
+ @UnsupportedAppUsage
+ public void addProperty(@NonNull String name, float v) {
+ writeShort(createPropertyIndex(name));
+ writeFloat(v);
+ }
+
+ @UnsupportedAppUsage
+ public void addProperty(@NonNull String name, @Nullable String s) {
+ writeShort(createPropertyIndex(name));
+ writeString(s);
+ }
+
+ /**
+ * Encodes a user defined property if they are allowed to be encoded
+ *
+ * @see #setUserPropertiesEnabled(boolean)
+ */
+ public void addUserProperty(@NonNull String name, @Nullable String s) {
+ if (mUserPropertiesEnabled) {
+ addProperty(name, s);
+ }
+ }
+
+ /**
+ * Writes the given name as the property name, and leaves it to the callee
+ * to fill in value for this property.
+ */
+ public void addPropertyKey(@NonNull String name) {
+ writeShort(createPropertyIndex(name));
+ }
+
+ private short createPropertyIndex(@NonNull String name) {
+ Short index = mPropertyNames.get(name);
+ if (index == null) {
+ index = mPropertyId++;
+ mPropertyNames.put(name, index);
+ }
+
+ return index;
+ }
+
+ private void startPropertyMap() {
+ try {
+ mStream.write(SIG_MAP);
+ } catch (IOException e) {
+ // does not happen since the stream simply wraps a ByteArrayOutputStream
+ }
+ }
+
+ private void endPropertyMap() {
+ writeShort(SIG_END_MAP);
+ }
+
+ private void writeBoolean(boolean v) {
+ try {
+ mStream.write(SIG_BOOLEAN);
+ mStream.write(v ? 1 : 0);
+ } catch (IOException e) {
+ // does not happen since the stream simply wraps a ByteArrayOutputStream
+ }
+ }
+
+ private void writeShort(short s) {
+ try {
+ mStream.write(SIG_SHORT);
+ mStream.writeShort(s);
+ } catch (IOException e) {
+ // does not happen since the stream simply wraps a ByteArrayOutputStream
+ }
+ }
+
+ private void writeInt(int i) {
+ try {
+ mStream.write(SIG_INT);
+ mStream.writeInt(i);
+ } catch (IOException e) {
+ // does not happen since the stream simply wraps a ByteArrayOutputStream
+ }
+ }
+
+ private void writeFloat(float v) {
+ try {
+ mStream.write(SIG_FLOAT);
+ mStream.writeFloat(v);
+ } catch (IOException e) {
+ // does not happen since the stream simply wraps a ByteArrayOutputStream
+ }
+ }
+
+ private void writeString(@Nullable String s) {
+ if (s == null) {
+ s = "";
+ }
+
+ try {
+ mStream.write(SIG_STRING);
+ byte[] bytes = s.getBytes(mCharset);
+
+ short len = (short)Math.min(bytes.length, Short.MAX_VALUE);
+ mStream.writeShort(len);
+
+ mStream.write(bytes, 0, len);
+ } catch (IOException e) {
+ // does not happen since the stream simply wraps a ByteArrayOutputStream
+ }
+ }
+}
diff --git a/android/view/ViewManager.java b/android/view/ViewManager.java
new file mode 100644
index 0000000..ab6856f
--- /dev/null
+++ b/android/view/ViewManager.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/** Interface to let you add and remove child views to an Activity. To get an instance
+ * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
+ */
+public interface ViewManager
+{
+ /**
+ * Assign the passed LayoutParams to the passed View and add the view to the window.
+ * <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
+ * errors, such as adding a second view to a window without removing the first view.
+ * <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
+ * secondary {@link Display} and the specified display can't be found
+ * (see {@link android.app.Presentation}).
+ * @param view The view to be added to this window.
+ * @param params The LayoutParams to assign to view.
+ */
+ public void addView(View view, ViewGroup.LayoutParams params);
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params);
+ public void removeView(View view);
+}
diff --git a/android/view/ViewOutlineProvider.java b/android/view/ViewOutlineProvider.java
new file mode 100644
index 0000000..a1a02f6
--- /dev/null
+++ b/android/view/ViewOutlineProvider.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Outline;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Interface by which a View builds its {@link Outline}, used for shadow casting and clipping.
+ */
+public abstract class ViewOutlineProvider {
+ /**
+ * Default outline provider for Views, which queries the Outline from the View's background,
+ * or generates a 0 alpha, rectangular Outline the size of the View if a background
+ * isn't present.
+ *
+ * @see Drawable#getOutline(Outline)
+ */
+ public static final ViewOutlineProvider BACKGROUND = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ Drawable background = view.getBackground();
+ if (background != null) {
+ background.getOutline(outline);
+ } else {
+ outline.setRect(0, 0, view.getWidth(), view.getHeight());
+ outline.setAlpha(0.0f);
+ }
+ }
+ };
+
+ /**
+ * Maintains the outline of the View to match its rectangular bounds,
+ * at <code>1.0f</code> alpha.
+ *
+ * This can be used to enable Views that are opaque but lacking a background cast a shadow.
+ */
+ public static final ViewOutlineProvider BOUNDS = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRect(0, 0, view.getWidth(), view.getHeight());
+ }
+ };
+
+ /**
+ * Maintains the outline of the View to match its rectangular padded bounds,
+ * at <code>1.0f</code> alpha.
+ *
+ * This can be used to enable Views that are opaque but lacking a background cast a shadow.
+ */
+ public static final ViewOutlineProvider PADDED_BOUNDS = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRect(view.getPaddingLeft(),
+ view.getPaddingTop(),
+ view.getWidth() - view.getPaddingRight(),
+ view.getHeight() - view.getPaddingBottom());
+ }
+ };
+
+ /**
+ * Called to get the provider to populate the Outline.
+ *
+ * This method will be called by a View when its owned Drawables are invalidated, when the
+ * View's size changes, or if {@link View#invalidateOutline()} is called
+ * explicitly.
+ *
+ * The input outline is empty and has an alpha of <code>1.0f</code>.
+ *
+ * @param view The view building the outline.
+ * @param outline The empty outline to be populated.
+ */
+ public abstract void getOutline(View view, Outline outline);
+}
diff --git a/android/view/ViewOverlay.java b/android/view/ViewOverlay.java
new file mode 100644
index 0000000..02f7e95
--- /dev/null
+++ b/android/view/ViewOverlay.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view;
+
+import android.animation.LayoutTransition;
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+import java.util.ArrayList;
+
+/**
+ * An overlay is an extra layer that sits on top of a View (the "host view")
+ * which is drawn after all other content in that view (including children,
+ * if the view is a ViewGroup). Interaction with the overlay layer is done
+ * by adding and removing drawables.
+ *
+ * <p>An overlay requested from a ViewGroup is of type {@link ViewGroupOverlay},
+ * which also supports adding and removing views.</p>
+ *
+ * @see View#getOverlay() View.getOverlay()
+ * @see ViewGroup#getOverlay() ViewGroup.getOverlay()
+ * @see ViewGroupOverlay
+ */
+public class ViewOverlay {
+
+ /**
+ * The actual container for the drawables (and views, if it's a ViewGroupOverlay).
+ * All of the management and rendering details for the overlay are handled in
+ * OverlayViewGroup.
+ */
+ OverlayViewGroup mOverlayViewGroup;
+
+ ViewOverlay(Context context, View hostView) {
+ mOverlayViewGroup = new OverlayViewGroup(context, hostView);
+ }
+
+ /**
+ * Used internally by View and ViewGroup to handle drawing and invalidation
+ * of the overlay
+ * @return
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ ViewGroup getOverlayView() {
+ return mOverlayViewGroup;
+ }
+
+ /**
+ * Adds a {@link Drawable} to the overlay. The bounds of the drawable should be relative to
+ * the host view. Any drawable added to the overlay should be removed when it is no longer
+ * needed or no longer visible. Adding an already existing {@link Drawable}
+ * is a no-op. Passing <code>null</code> parameter will result in an
+ * {@link IllegalArgumentException} being thrown.
+ *
+ * @param drawable The {@link Drawable} to be added to the overlay. This drawable will be
+ * drawn when the view redraws its overlay. {@link Drawable}s will be drawn in the order that
+ * they were added.
+ * @see #remove(Drawable)
+ */
+ public void add(@NonNull Drawable drawable) {
+ mOverlayViewGroup.add(drawable);
+ }
+
+ /**
+ * Removes the specified {@link Drawable} from the overlay. Removing a {@link Drawable} that was
+ * not added with {@link #add(Drawable)} is a no-op. Passing <code>null</code> parameter will
+ * result in an {@link IllegalArgumentException} being thrown.
+ *
+ * @param drawable The {@link Drawable} to be removed from the overlay.
+ * @see #add(Drawable)
+ */
+ public void remove(@NonNull Drawable drawable) {
+ mOverlayViewGroup.remove(drawable);
+ }
+
+ /**
+ * Removes all content from the overlay.
+ */
+ public void clear() {
+ mOverlayViewGroup.clear();
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ boolean isEmpty() {
+ return mOverlayViewGroup.isEmpty();
+ }
+
+ /**
+ * OverlayViewGroup is a container that View and ViewGroup use to host
+ * drawables and views added to their overlays ({@link ViewOverlay} and
+ * {@link ViewGroupOverlay}, respectively). Drawables are added to the overlay
+ * via the add/remove methods in ViewOverlay, Views are added/removed via
+ * ViewGroupOverlay. These drawable and view objects are
+ * drawn whenever the view itself is drawn; first the view draws its own
+ * content (and children, if it is a ViewGroup), then it draws its overlay
+ * (if it has one).
+ *
+ * <p>Besides managing and drawing the list of drawables, this class serves
+ * two purposes:
+ * (1) it noops layout calls because children are absolutely positioned and
+ * (2) it forwards all invalidation calls to its host view. The invalidation
+ * redirect is necessary because the overlay is not a child of the host view
+ * and invalidation cannot therefore follow the normal path up through the
+ * parent hierarchy.</p>
+ *
+ * @see View#getOverlay()
+ * @see ViewGroup#getOverlay()
+ */
+ static class OverlayViewGroup extends ViewGroup {
+
+ /**
+ * The View for which this is an overlay. Invalidations of the overlay are redirected to
+ * this host view.
+ */
+ final View mHostView;
+
+ /**
+ * The set of drawables to draw when the overlay is rendered.
+ */
+ ArrayList<Drawable> mDrawables = null;
+
+ OverlayViewGroup(Context context, View hostView) {
+ super(context);
+ mHostView = hostView;
+ mAttachInfo = mHostView.mAttachInfo;
+
+ mRight = hostView.getWidth();
+ mBottom = hostView.getHeight();
+ // pass right+bottom directly to RenderNode, since not going through setters
+ mRenderNode.setLeftTopRightBottom(0, 0, mRight, mBottom);
+ }
+
+ public void add(@NonNull Drawable drawable) {
+ if (drawable == null) {
+ throw new IllegalArgumentException("drawable must be non-null");
+ }
+ if (mDrawables == null) {
+ mDrawables = new ArrayList<>();
+ }
+ if (!mDrawables.contains(drawable)) {
+ // Make each drawable unique in the overlay; can't add it more than once
+ mDrawables.add(drawable);
+ invalidate(drawable.getBounds());
+ drawable.setCallback(this);
+ }
+ }
+
+ public void remove(@NonNull Drawable drawable) {
+ if (drawable == null) {
+ throw new IllegalArgumentException("drawable must be non-null");
+ }
+ if (mDrawables != null) {
+ mDrawables.remove(drawable);
+ invalidate(drawable.getBounds());
+ drawable.setCallback(null);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || (mDrawables != null && mDrawables.contains(who));
+ }
+
+ public void add(@NonNull View child) {
+ if (child == null) {
+ throw new IllegalArgumentException("view must be non-null");
+ }
+
+ if (child.getParent() instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) child.getParent();
+ if (parent != mHostView && parent.getParent() != null &&
+ parent.mAttachInfo != null) {
+ // Moving to different container; figure out how to position child such that
+ // it is in the same location on the screen
+ int[] parentLocation = new int[2];
+ int[] hostViewLocation = new int[2];
+ parent.getLocationOnScreen(parentLocation);
+ mHostView.getLocationOnScreen(hostViewLocation);
+ child.offsetLeftAndRight(parentLocation[0] - hostViewLocation[0]);
+ child.offsetTopAndBottom(parentLocation[1] - hostViewLocation[1]);
+ }
+ parent.removeView(child);
+ if (parent.getLayoutTransition() != null) {
+ // LayoutTransition will cause the child to delay removal - cancel it
+ parent.getLayoutTransition().cancel(LayoutTransition.DISAPPEARING);
+ }
+ // fail-safe if view is still attached for any reason
+ if (child.getParent() != null) {
+ child.mParent = null;
+ }
+ }
+ super.addView(child);
+ }
+
+ public void remove(@NonNull View view) {
+ if (view == null) {
+ throw new IllegalArgumentException("view must be non-null");
+ }
+
+ super.removeView(view);
+ }
+
+ public void clear() {
+ removeAllViews();
+ if (mDrawables != null) {
+ for (Drawable drawable : mDrawables) {
+ drawable.setCallback(null);
+ }
+ mDrawables.clear();
+ }
+ }
+
+ boolean isEmpty() {
+ if (getChildCount() == 0 &&
+ (mDrawables == null || mDrawables.size() == 0)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ invalidate(drawable.getBounds());
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ /*
+ * The OverlayViewGroup doesn't draw with a DisplayList, because
+ * draw(Canvas, View, long) is never called on it. This is fine, since it doesn't need
+ * RenderNode/DisplayList features, and can just draw into the owner's Canvas.
+ *
+ * This means that we need to insert reorder barriers manually though, so that children
+ * of the OverlayViewGroup can cast shadows and Z reorder with each other.
+ */
+ canvas.enableZ();
+
+ super.dispatchDraw(canvas);
+
+ canvas.disableZ();
+ final int numDrawables = (mDrawables == null) ? 0 : mDrawables.size();
+ for (int i = 0; i < numDrawables; ++i) {
+ mDrawables.get(i).draw(canvas);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Noop: children are positioned absolutely
+ }
+
+ /*
+ The following invalidation overrides exist for the purpose of redirecting invalidation to
+ the host view. The overlay is not parented to the host view (since a View cannot be a
+ parent), so the invalidation cannot proceed through the normal parent hierarchy.
+ There is a built-in assumption that the overlay exactly covers the host view, therefore
+ the invalidation rectangles received do not need to be adjusted when forwarded to
+ the host view.
+ */
+
+ @Override
+ public void invalidate(Rect dirty) {
+ super.invalidate(dirty);
+ if (mHostView != null) {
+ mHostView.invalidate(dirty);
+ }
+ }
+
+ @Override
+ public void invalidate(int l, int t, int r, int b) {
+ super.invalidate(l, t, r, b);
+ if (mHostView != null) {
+ mHostView.invalidate(l, t, r, b);
+ }
+ }
+
+ @Override
+ public void invalidate() {
+ super.invalidate();
+ if (mHostView != null) {
+ mHostView.invalidate();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void invalidate(boolean invalidateCache) {
+ super.invalidate(invalidateCache);
+ if (mHostView != null) {
+ mHostView.invalidate(invalidateCache);
+ }
+ }
+
+ @Override
+ void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
+ super.invalidateViewProperty(invalidateParent, forceRedraw);
+ if (mHostView != null) {
+ mHostView.invalidateViewProperty(invalidateParent, forceRedraw);
+ }
+ }
+
+ @Override
+ protected void invalidateParentCaches() {
+ super.invalidateParentCaches();
+ if (mHostView != null) {
+ mHostView.invalidateParentCaches();
+ }
+ }
+
+ @Override
+ protected void invalidateParentIfNeeded() {
+ super.invalidateParentIfNeeded();
+ if (mHostView != null) {
+ mHostView.invalidateParentIfNeeded();
+ }
+ }
+
+ @Override
+ public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
+ if (mHostView != null) {
+ if (mHostView instanceof ViewGroup) {
+ // Propagate invalidate through the host...
+ ((ViewGroup) mHostView).onDescendantInvalidated(mHostView, target);
+
+ // ...and also this view, since it will hold the descendant, and must later
+ // propagate the calls to update display lists if dirty
+ super.onDescendantInvalidated(child, target);
+ } else {
+ // Can't use onDescendantInvalidated because host isn't a ViewGroup - fall back
+ // to invalidating.
+ invalidate();
+ }
+ }
+ }
+
+ @Override
+ public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
+ if (mHostView != null) {
+ dirty.offset(location[0], location[1]);
+ if (mHostView instanceof ViewGroup) {
+ location[0] = 0;
+ location[1] = 0;
+ super.invalidateChildInParent(location, dirty);
+ return ((ViewGroup) mHostView).invalidateChildInParent(location, dirty);
+ } else {
+ invalidate(dirty);
+ }
+ }
+ return null;
+ }
+ }
+
+}
diff --git a/android/view/ViewParent.java b/android/view/ViewParent.java
new file mode 100644
index 0000000..775c15e
--- /dev/null
+++ b/android/view/ViewParent.java
@@ -0,0 +1,692 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Bundle;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * Defines the responsibilities for a class that will be a parent of a View.
+ * This is the API that a view sees when it wants to interact with its parent.
+ *
+ */
+public interface ViewParent {
+ /**
+ * Called when something has changed which has invalidated the layout of a
+ * child of this view parent. This will schedule a layout pass of the view
+ * tree.
+ */
+ public void requestLayout();
+
+ /**
+ * Indicates whether layout was requested on this view parent.
+ *
+ * @return true if layout was requested, false otherwise
+ */
+ public boolean isLayoutRequested();
+
+ /**
+ * Called when a child wants the view hierarchy to gather and report
+ * transparent regions to the window compositor. Views that "punch" holes in
+ * the view hierarchy, such as SurfaceView can use this API to improve
+ * performance of the system. When no such a view is present in the
+ * hierarchy, this optimization in unnecessary and might slightly reduce the
+ * view hierarchy performance.
+ *
+ * @param child the view requesting the transparent region computation
+ *
+ */
+ public void requestTransparentRegion(View child);
+
+
+ /**
+ * The target View has been invalidated, or has had a drawing property changed that
+ * requires the hierarchy to re-render.
+ *
+ * This method is called by the View hierarchy to signal ancestors that a View either needs to
+ * re-record its drawing commands, or drawing properties have changed. This is how Views
+ * schedule a drawing traversal.
+ *
+ * This signal is generally only dispatched for attached Views, since only they need to draw.
+ *
+ * @param child Direct child of this ViewParent containing target
+ * @param target The view that needs to redraw
+ */
+ default void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
+ if (getParent() != null) {
+ // Note: should pass 'this' as default, but can't since we may not be a View
+ getParent().onDescendantInvalidated(child, target);
+ }
+ }
+
+ /**
+ * All or part of a child is dirty and needs to be redrawn.
+ *
+ * @param child The child which is dirty
+ * @param r The area within the child that is invalid
+ *
+ * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead.
+ */
+ @Deprecated
+ public void invalidateChild(View child, Rect r);
+
+ /**
+ * All or part of a child is dirty and needs to be redrawn.
+ *
+ * <p>The location array is an array of two int values which respectively
+ * define the left and the top position of the dirty child.</p>
+ *
+ * <p>This method must return the parent of this ViewParent if the specified
+ * rectangle must be invalidated in the parent. If the specified rectangle
+ * does not require invalidation in the parent or if the parent does not
+ * exist, this method must return null.</p>
+ *
+ * <p>When this method returns a non-null value, the location array must
+ * have been updated with the left and top coordinates of this ViewParent.</p>
+ *
+ * @param location An array of 2 ints containing the left and top
+ * coordinates of the child to invalidate
+ * @param r The area within the child that is invalid
+ *
+ * @return the parent of this ViewParent or null
+ *
+ * @deprecated Use {@link #onDescendantInvalidated(View, View)} instead.
+ */
+ @Deprecated
+ public ViewParent invalidateChildInParent(int[] location, Rect r);
+
+ /**
+ * Returns the parent if it exists, or null.
+ *
+ * @return a ViewParent or null if this ViewParent does not have a parent
+ */
+ public ViewParent getParent();
+
+ /**
+ * Called when a child of this parent wants focus
+ *
+ * @param child The child of this ViewParent that wants focus. This view
+ * will contain the focused view. It is not necessarily the view that
+ * actually has focus.
+ * @param focused The view that is a descendant of child that actually has
+ * focus
+ */
+ public void requestChildFocus(View child, View focused);
+
+ /**
+ * Tell view hierarchy that the global view attributes need to be
+ * re-evaluated.
+ *
+ * @param child View whose attributes have changed.
+ */
+ public void recomputeViewAttributes(View child);
+
+ /**
+ * Called when a child of this parent is giving up focus
+ *
+ * @param child The view that is giving up focus
+ */
+ public void clearChildFocus(View child);
+
+ /**
+ * Compute the visible part of a rectangular region defined in terms of a child view's
+ * coordinates.
+ *
+ * <p>Returns the clipped visible part of the rectangle <code>r</code>, defined in the
+ * <code>child</code>'s local coordinate system. <code>r</code> is modified by this method to
+ * contain the result, expressed in the global (root) coordinate system.</p>
+ *
+ * <p>The resulting rectangle is always axis aligned. If a rotation is applied to a node in the
+ * View hierarchy, the result is the axis-aligned bounding box of the visible rectangle.</p>
+ *
+ * @param child A child View, whose rectangular visible region we want to compute
+ * @param r The input rectangle, defined in the child coordinate system. Will be overwritten to
+ * contain the resulting visible rectangle, expressed in global (root) coordinates
+ * @param offset The input coordinates of a point, defined in the child coordinate system.
+ * As with the <code>r</code> parameter, this will be overwritten to contain the global (root)
+ * coordinates of that point.
+ * A <code>null</code> value is valid (in case you are not interested in this result)
+ * @return true if the resulting rectangle is not empty, false otherwise
+ */
+ public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset);
+
+ /**
+ * Find the nearest view in the specified direction that wants to take focus
+ *
+ * @param v The view that currently has focus
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ */
+ public View focusSearch(View v, int direction);
+
+ /**
+ * Find the nearest keyboard navigation cluster in the specified direction.
+ * This does not actually give focus to that cluster.
+ *
+ * @param currentCluster The starting point of the search. Null means the current cluster is not
+ * found yet
+ * @param direction Direction to look
+ *
+ * @return The nearest keyboard navigation cluster in the specified direction, or null if none
+ * can be found
+ */
+ View keyboardNavigationClusterSearch(View currentCluster, int direction);
+
+ /**
+ * Change the z order of the child so it's on top of all other children.
+ * This ordering change may affect layout, if this container
+ * uses an order-dependent layout scheme (e.g., LinearLayout). Prior
+ * to {@link android.os.Build.VERSION_CODES#KITKAT} this
+ * method should be followed by calls to {@link #requestLayout()} and
+ * {@link View#invalidate()} on this parent to force the parent to redraw
+ * with the new child ordering.
+ *
+ * @param child The child to bring to the top of the z order
+ */
+ public void bringChildToFront(View child);
+
+ /**
+ * Tells the parent that a new focusable view has become available. This is
+ * to handle transitions from the case where there are no focusable views to
+ * the case where the first focusable view appears.
+ *
+ * @param v The view that has become newly focusable
+ */
+ public void focusableViewAvailable(View v);
+
+ /**
+ * Shows the context menu for the specified view or its ancestors.
+ * <p>
+ * In most cases, a subclass does not need to override this. However, if
+ * the subclass is added directly to the window manager (for example,
+ * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
+ * then it should override this and show the context menu.
+ *
+ * @param originalView the source view where the context menu was first
+ * invoked
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
+ * @see #showContextMenuForChild(View, float, float)
+ */
+ public boolean showContextMenuForChild(View originalView);
+
+ /**
+ * Shows the context menu for the specified view or its ancestors anchored
+ * to the specified view-relative coordinate.
+ * <p>
+ * In most cases, a subclass does not need to override this. However, if
+ * the subclass is added directly to the window manager (for example,
+ * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
+ * then it should override this and show the context menu.
+ * <p>
+ * If a subclass overrides this method it should also override
+ * {@link #showContextMenuForChild(View)}.
+ *
+ * @param originalView the source view where the context menu was first
+ * invoked
+ * @param x the X coordinate in pixels relative to the original view to
+ * which the menu should be anchored, or {@link Float#NaN} to
+ * disable anchoring
+ * @param y the Y coordinate in pixels relative to the original view to
+ * which the menu should be anchored, or {@link Float#NaN} to
+ * disable anchoring
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
+ */
+ boolean showContextMenuForChild(View originalView, float x, float y);
+
+ /**
+ * Have the parent populate the specified context menu if it has anything to
+ * add (and then recurse on its parent).
+ *
+ * @param menu The menu to populate
+ */
+ public void createContextMenu(ContextMenu menu);
+
+ /**
+ * Start an action mode for the specified view with the default type
+ * {@link ActionMode#TYPE_PRIMARY}.
+ *
+ * <p>In most cases, a subclass does not need to override this. However, if the
+ * subclass is added directly to the window manager (for example,
+ * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
+ * then it should override this and start the action mode.</p>
+ *
+ * @param originalView The source view where the action mode was first invoked
+ * @param callback The callback that will handle lifecycle events for the action mode
+ * @return The new action mode if it was started, null otherwise
+ *
+ * @see #startActionModeForChild(View, android.view.ActionMode.Callback, int)
+ */
+ public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback);
+
+ /**
+ * Start an action mode of a specific type for the specified view.
+ *
+ * <p>In most cases, a subclass does not need to override this. However, if the
+ * subclass is added directly to the window manager (for example,
+ * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
+ * then it should override this and start the action mode.</p>
+ *
+ * @param originalView The source view where the action mode was first invoked
+ * @param callback The callback that will handle lifecycle events for the action mode
+ * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}.
+ * @return The new action mode if it was started, null otherwise
+ */
+ public ActionMode startActionModeForChild(
+ View originalView, ActionMode.Callback callback, int type);
+
+ /**
+ * This method is called on the parent when a child's drawable state
+ * has changed.
+ *
+ * @param child The child whose drawable state has changed.
+ */
+ public void childDrawableStateChanged(View child);
+
+ /**
+ * Called when a child does not want this parent and its ancestors to
+ * intercept touch events with
+ * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
+ *
+ * <p>This parent should pass this call onto its parents. This parent must obey
+ * this request for the duration of the touch (that is, only clear the flag
+ * after this parent has received an up or a cancel.</p>
+ *
+ * @param disallowIntercept True if the child does not want the parent to
+ * intercept touch events.
+ */
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
+
+ /**
+ * Called when a child of this group wants a particular rectangle to be
+ * positioned onto the screen. {@link ViewGroup}s overriding this can trust
+ * that:
+ * <ul>
+ * <li>child will be a direct child of this group</li>
+ * <li>rectangle will be in the child's content coordinates</li>
+ * </ul>
+ *
+ * <p>{@link ViewGroup}s overriding this should uphold the contract:</p>
+ * <ul>
+ * <li>nothing will change if the rectangle is already visible</li>
+ * <li>the view port will be scrolled only just enough to make the
+ * rectangle visible</li>
+ * <ul>
+ *
+ * @param child The direct child making the request.
+ * @param rectangle The rectangle in the child's coordinates the child
+ * wishes to be on the screen.
+ * @param immediate True to forbid animated or delayed scrolling,
+ * false otherwise
+ * @return Whether the group scrolled to handle the operation
+ */
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+ boolean immediate);
+
+ /**
+ * Called by a child to request from its parent to send an {@link AccessibilityEvent}.
+ * The child has already populated a record for itself in the event and is delegating
+ * to its parent to send the event. The parent can optionally add a record for itself.
+ * <p>
+ * Note: An accessibility event is fired by an individual view which populates the
+ * event with a record for its state and requests from its parent to perform
+ * the sending. The parent can optionally add a record for itself before
+ * dispatching the request to its parent. A parent can also choose not to
+ * respect the request for sending the event. The accessibility event is sent
+ * by the topmost view in the view tree.</p>
+ *
+ * @param child The child which requests sending the event.
+ * @param event The event to be sent.
+ * @return True if the event was sent.
+ */
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event);
+
+ /**
+ * Called when a child view now has or no longer is tracking transient state.
+ *
+ * <p>"Transient state" is any state that a View might hold that is not expected to
+ * be reflected in the data model that the View currently presents. This state only
+ * affects the presentation to the user within the View itself, such as the current
+ * state of animations in progress or the state of a text selection operation.</p>
+ *
+ * <p>Transient state is useful for hinting to other components of the View system
+ * that a particular view is tracking something complex but encapsulated.
+ * A <code>ListView</code> for example may acknowledge that list item Views
+ * with transient state should be preserved within their position or stable item ID
+ * instead of treating that view as trivially replaceable by the backing adapter.
+ * This allows adapter implementations to be simpler instead of needing to track
+ * the state of item view animations in progress such that they could be restored
+ * in the event of an unexpected recycling and rebinding of attached item views.</p>
+ *
+ * <p>This method is called on a parent view when a child view or a view within
+ * its subtree begins or ends tracking of internal transient state.</p>
+ *
+ * @param child Child view whose state has changed
+ * @param hasTransientState true if this child has transient state
+ */
+ public void childHasTransientStateChanged(View child, boolean hasTransientState);
+
+ /**
+ * Ask that a new dispatch of {@link View#fitSystemWindows(Rect)
+ * View.fitSystemWindows(Rect)} be performed.
+ */
+ public void requestFitSystemWindows();
+
+ /**
+ * Gets the parent of a given View for accessibility. Since some Views are not
+ * exposed to the accessibility layer the parent for accessibility is not
+ * necessarily the direct parent of the View, rather it is a predecessor.
+ *
+ * @return The parent or <code>null</code> if no such is found.
+ */
+ public ViewParent getParentForAccessibility();
+
+ /**
+ * Notifies a view parent that the accessibility state of one of its
+ * descendants has changed and that the structure of the subtree is
+ * different.
+ * @param child The direct child whose subtree has changed.
+ * @param source The descendant view that changed. May not be {@code null}.
+ * @param changeType A bit mask of the types of changes that occurred. One
+ * or more of:
+ * <ul>
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_STATE_DESCRIPTION}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
+ * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
+ * </ul>
+ */
+ public void notifySubtreeAccessibilityStateChanged(
+ View child, @NonNull View source, int changeType);
+
+ /**
+ * Tells if this view parent can resolve the layout direction.
+ * See {@link View#setLayoutDirection(int)}
+ *
+ * @return True if this view parent can resolve the layout direction.
+ */
+ public boolean canResolveLayoutDirection();
+
+ /**
+ * Tells if this view parent layout direction is resolved.
+ * See {@link View#setLayoutDirection(int)}
+ *
+ * @return True if this view parent layout direction is resolved.
+ */
+ public boolean isLayoutDirectionResolved();
+
+ /**
+ * Return this view parent layout direction. See {@link View#getLayoutDirection()}
+ *
+ * @return {@link View#LAYOUT_DIRECTION_RTL} if the layout direction is RTL or returns
+ * {@link View#LAYOUT_DIRECTION_LTR} if the layout direction is not RTL.
+ */
+ public int getLayoutDirection();
+
+ /**
+ * Tells if this view parent can resolve the text direction.
+ * See {@link View#setTextDirection(int)}
+ *
+ * @return True if this view parent can resolve the text direction.
+ */
+ public boolean canResolveTextDirection();
+
+ /**
+ * Tells if this view parent text direction is resolved.
+ * See {@link View#setTextDirection(int)}
+ *
+ * @return True if this view parent text direction is resolved.
+ */
+ public boolean isTextDirectionResolved();
+
+ /**
+ * Return this view parent text direction. See {@link View#getTextDirection()}
+ *
+ * @return the resolved text direction. Returns one of:
+ *
+ * {@link View#TEXT_DIRECTION_FIRST_STRONG}
+ * {@link View#TEXT_DIRECTION_ANY_RTL},
+ * {@link View#TEXT_DIRECTION_LTR},
+ * {@link View#TEXT_DIRECTION_RTL},
+ * {@link View#TEXT_DIRECTION_LOCALE}
+ */
+ public int getTextDirection();
+
+ /**
+ * Tells if this view parent can resolve the text alignment.
+ * See {@link View#setTextAlignment(int)}
+ *
+ * @return True if this view parent can resolve the text alignment.
+ */
+ public boolean canResolveTextAlignment();
+
+ /**
+ * Tells if this view parent text alignment is resolved.
+ * See {@link View#setTextAlignment(int)}
+ *
+ * @return True if this view parent text alignment is resolved.
+ */
+ public boolean isTextAlignmentResolved();
+
+ /**
+ * Return this view parent text alignment. See {@link android.view.View#getTextAlignment()}
+ *
+ * @return the resolved text alignment. Returns one of:
+ *
+ * {@link View#TEXT_ALIGNMENT_GRAVITY},
+ * {@link View#TEXT_ALIGNMENT_CENTER},
+ * {@link View#TEXT_ALIGNMENT_TEXT_START},
+ * {@link View#TEXT_ALIGNMENT_TEXT_END},
+ * {@link View#TEXT_ALIGNMENT_VIEW_START},
+ * {@link View#TEXT_ALIGNMENT_VIEW_END}
+ */
+ public int getTextAlignment();
+
+ /**
+ * React to a descendant view initiating a nestable scroll operation, claiming the
+ * nested scroll operation if appropriate.
+ *
+ * <p>This method will be called in response to a descendant view invoking
+ * {@link View#startNestedScroll(int)}. Each parent up the view hierarchy will be
+ * given an opportunity to respond and claim the nested scrolling operation by returning
+ * <code>true</code>.</p>
+ *
+ * <p>This method may be overridden by ViewParent implementations to indicate when the view
+ * is willing to support a nested scrolling operation that is about to begin. If it returns
+ * true, this ViewParent will become the target view's nested scrolling parent for the duration
+ * of the scroll operation in progress. When the nested scroll is finished this ViewParent
+ * will receive a call to {@link #onStopNestedScroll(View)}.
+ * </p>
+ *
+ * @param child Direct child of this ViewParent containing target
+ * @param target View that initiated the nested scroll
+ * @param nestedScrollAxes Flags consisting of {@link View#SCROLL_AXIS_HORIZONTAL},
+ * {@link View#SCROLL_AXIS_VERTICAL} or both
+ * @return true if this ViewParent accepts the nested scroll operation
+ */
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
+
+ /**
+ * React to the successful claiming of a nested scroll operation.
+ *
+ * <p>This method will be called after
+ * {@link #onStartNestedScroll(View, View, int) onStartNestedScroll} returns true. It offers
+ * an opportunity for the view and its superclasses to perform initial configuration
+ * for the nested scroll. Implementations of this method should always call their superclass's
+ * implementation of this method if one is present.</p>
+ *
+ * @param child Direct child of this ViewParent containing target
+ * @param target View that initiated the nested scroll
+ * @param nestedScrollAxes Flags consisting of {@link View#SCROLL_AXIS_HORIZONTAL},
+ * {@link View#SCROLL_AXIS_VERTICAL} or both
+ * @see #onStartNestedScroll(View, View, int)
+ * @see #onStopNestedScroll(View)
+ */
+ public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
+
+ /**
+ * React to a nested scroll operation ending.
+ *
+ * <p>Perform cleanup after a nested scrolling operation.
+ * This method will be called when a nested scroll stops, for example when a nested touch
+ * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event.
+ * Implementations of this method should always call their superclass's implementation of this
+ * method if one is present.</p>
+ *
+ * @param target View that initiated the nested scroll
+ */
+ public void onStopNestedScroll(View target);
+
+ /**
+ * React to a nested scroll in progress.
+ *
+ * <p>This method will be called when the ViewParent's current nested scrolling child view
+ * dispatches a nested scroll event. To receive calls to this method the ViewParent must have
+ * previously returned <code>true</code> for a call to
+ * {@link #onStartNestedScroll(View, View, int)}.</p>
+ *
+ * <p>Both the consumed and unconsumed portions of the scroll distance are reported to the
+ * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll
+ * position of multiple child elements, for example. The unconsumed portion may be used to
+ * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling
+ * a list within a vertical drawer where the drawer begins dragging once the edge of inner
+ * scrolling content is reached.</p>
+ *
+ * @param target The descendent view controlling the nested scroll
+ * @param dxConsumed Horizontal scroll distance in pixels already consumed by target
+ * @param dyConsumed Vertical scroll distance in pixels already consumed by target
+ * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target
+ * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target
+ */
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed);
+
+ /**
+ * React to a nested scroll in progress before the target view consumes a portion of the scroll.
+ *
+ * <p>When working with nested scrolling often the parent view may want an opportunity
+ * to consume the scroll before the nested scrolling child does. An example of this is a
+ * drawer that contains a scrollable list. The user will want to be able to scroll the list
+ * fully into view before the list itself begins scrolling.</p>
+ *
+ * <p><code>onNestedPreScroll</code> is called when a nested scrolling child invokes
+ * {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should
+ * report how any pixels of the scroll reported by dx, dy were consumed in the
+ * <code>consumed</code> array. Index 0 corresponds to dx and index 1 corresponds to dy.
+ * This parameter will never be null. Initial values for consumed[0] and consumed[1]
+ * will always be 0.</p>
+ *
+ * @param target View that initiated the nested scroll
+ * @param dx Horizontal scroll distance in pixels
+ * @param dy Vertical scroll distance in pixels
+ * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
+ */
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
+
+ /**
+ * Request a fling from a nested scroll.
+ *
+ * <p>This method signifies that a nested scrolling child has detected suitable conditions
+ * for a fling. Generally this means that a touch scroll has ended with a
+ * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
+ * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
+ * along a scrollable axis.</p>
+ *
+ * <p>If a nested scrolling child view would normally fling but it is at the edge of
+ * its own content, it can use this method to delegate the fling to its nested scrolling
+ * parent instead. The parent may optionally consume the fling or observe a child fling.</p>
+ *
+ * @param target View that initiated the nested scroll
+ * @param velocityX Horizontal velocity in pixels per second
+ * @param velocityY Vertical velocity in pixels per second
+ * @param consumed true if the child consumed the fling, false otherwise
+ * @return true if this parent consumed or otherwise reacted to the fling
+ */
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
+
+ /**
+ * React to a nested fling before the target view consumes it.
+ *
+ * <p>This method siginfies that a nested scrolling child has detected a fling with the given
+ * velocity along each axis. Generally this means that a touch scroll has ended with a
+ * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
+ * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
+ * along a scrollable axis.</p>
+ *
+ * <p>If a nested scrolling parent is consuming motion as part of a
+ * {@link #onNestedPreScroll(View, int, int, int[]) pre-scroll}, it may be appropriate for
+ * it to also consume the pre-fling to complete that same motion. By returning
+ * <code>true</code> from this method, the parent indicates that the child should not
+ * fling its own internal content as well.</p>
+ *
+ * @param target View that initiated the nested scroll
+ * @param velocityX Horizontal velocity in pixels per second
+ * @param velocityY Vertical velocity in pixels per second
+ * @return true if this parent consumed the fling ahead of the target view
+ */
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY);
+
+ /**
+ * React to an accessibility action delegated by a target descendant view before the target
+ * processes it.
+ *
+ * <p>This method may be called by a target descendant view if the target wishes to give
+ * a view in its parent chain a chance to react to the event before normal processing occurs.
+ * Most commonly this will be a scroll event such as
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_SCROLL_FORWARD}.
+ * A ViewParent that supports acting as a nested scrolling parent should override this
+ * method and act accordingly to implement scrolling via accesibility systems.</p>
+ *
+ * @param target The target view dispatching this action
+ * @param action Action being performed; see
+ * {@link android.view.accessibility.AccessibilityNodeInfo}
+ * @param arguments Optional action arguments
+ * @return true if the action was consumed by this ViewParent
+ */
+ public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle arguments);
+
+ /**
+ * Given a touchable region of a child, this method reduces region by the bounds of all views on
+ * top of the child for which {@link View#canReceivePointerEvents} returns {@code true}. This
+ * applies recursively for all views in the view hierarchy on top of this one.
+ *
+ * @param touchableRegion The touchable region we want to modify.
+ * @param view A child view of this ViewGroup which indicates the z-order of the touchable
+ * region.
+ * @hide
+ */
+ default void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
+ }
+
+ /**
+ * Unbuffered dispatch has been requested by a child of this view parent.
+ * This method is called by the View hierarchy to signal ancestors that a View needs to
+ * request unbuffered dispatch.
+ *
+ * @see View#requestUnbufferedDispatch(int)
+ * @hide
+ */
+ default void onDescendantUnbufferedRequested() {
+ if (getParent() != null) {
+ getParent().onDescendantUnbufferedRequested();
+ }
+ }
+}
diff --git a/android/view/ViewPerfTest.java b/android/view/ViewPerfTest.java
new file mode 100644
index 0000000..a2aeb31
--- /dev/null
+++ b/android/view/ViewPerfTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.view;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.perftests.utils.PerfTestActivity;
+import android.widget.FrameLayout;
+
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.perftests.core.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+@LargeTest
+public class ViewPerfTest {
+ @Rule
+ public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+
+ @Rule
+ public final ActivityTestRule<PerfTestActivity> mActivityRule =
+ new ActivityTestRule<>(PerfTestActivity.class);
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ }
+
+ @Test
+ public void testSimpleViewInflate() {
+ final BenchmarkState state = mBenchmarkRule.getState();
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ FrameLayout root = new FrameLayout(mContext);
+ while (state.keepRunning()) {
+ inflater.inflate(R.layout.test_simple_view, root, false);
+ }
+ }
+
+ @Test
+ public void testTwelveKeyInflate() {
+ final BenchmarkState state = mBenchmarkRule.getState();
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ FrameLayout root = new FrameLayout(mContext);
+ while (state.keepRunning()) {
+ inflater.inflate(R.layout.twelve_key_entry, root, false);
+ }
+ }
+
+ @Test
+ public void testPerformHapticFeedback() throws Throwable {
+ final BenchmarkState state = mBenchmarkRule.getState();
+ mActivityRule.runOnUiThread(() -> {
+ state.pauseTiming();
+ View view = new View(mContext);
+ mActivityRule.getActivity().setContentView(view);
+ assertTrue("View needs to be attached to Window to perform haptic feedback",
+ view.isAttachedToWindow());
+ state.resumeTiming();
+
+ // Disable settings so perform will never be ignored.
+ int flags = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+ | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING;
+
+ while (state.keepRunning()) {
+ assertTrue("Call to performHapticFeedback was ignored",
+ view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS, flags));
+ }
+ });
+ }
+}
diff --git a/android/view/ViewPropertyAnimator.java b/android/view/ViewPropertyAnimator.java
new file mode 100644
index 0000000..65cc2f8
--- /dev/null
+++ b/android/view/ViewPropertyAnimator.java
@@ -0,0 +1,1186 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.FloatRange;
+import android.graphics.RenderNode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * This class enables automatic and optimized animation of select properties on View objects.
+ * If only one or two properties on a View object are being animated, then using an
+ * {@link android.animation.ObjectAnimator} is fine; the property setters called by ObjectAnimator
+ * are well equipped to do the right thing to set the property and invalidate the view
+ * appropriately. But if several properties are animated simultaneously, or if you just want a
+ * more convenient syntax to animate a specific property, then ViewPropertyAnimator might be
+ * more well-suited to the task.
+ *
+ * <p>This class may provide better performance for several simultaneous animations, because
+ * it will optimize invalidate calls to take place only once for several properties instead of each
+ * animated property independently causing its own invalidation. Also, the syntax of using this
+ * class could be easier to use because the caller need only tell the View object which
+ * property to animate, and the value to animate either to or by, and this class handles the
+ * details of configuring the underlying Animator class and starting it.</p>
+ *
+ * <p>This class is not constructed by the caller, but rather by the View whose properties
+ * it will animate. Calls to {@link android.view.View#animate()} will return a reference
+ * to the appropriate ViewPropertyAnimator object for that View.</p>
+ *
+ */
+public class ViewPropertyAnimator {
+
+ /**
+ * The View whose properties are being animated by this class. This is set at
+ * construction time.
+ */
+ final View mView;
+
+ /**
+ * The duration of the underlying Animator object. By default, we don't set the duration
+ * on the Animator and just use its default duration. If the duration is ever set on this
+ * Animator, then we use the duration that it was set to.
+ */
+ private long mDuration;
+
+ /**
+ * A flag indicating whether the duration has been set on this object. If not, we don't set
+ * the duration on the underlying Animator, but instead just use its default duration.
+ */
+ private boolean mDurationSet = false;
+
+ /**
+ * The startDelay of the underlying Animator object. By default, we don't set the startDelay
+ * on the Animator and just use its default startDelay. If the startDelay is ever set on this
+ * Animator, then we use the startDelay that it was set to.
+ */
+ private long mStartDelay = 0;
+
+ /**
+ * A flag indicating whether the startDelay has been set on this object. If not, we don't set
+ * the startDelay on the underlying Animator, but instead just use its default startDelay.
+ */
+ private boolean mStartDelaySet = false;
+
+ /**
+ * The interpolator of the underlying Animator object. By default, we don't set the interpolator
+ * on the Animator and just use its default interpolator. If the interpolator is ever set on
+ * this Animator, then we use the interpolator that it was set to.
+ */
+ private TimeInterpolator mInterpolator;
+
+ /**
+ * A flag indicating whether the interpolator has been set on this object. If not, we don't set
+ * the interpolator on the underlying Animator, but instead just use its default interpolator.
+ */
+ private boolean mInterpolatorSet = false;
+
+ /**
+ * Listener for the lifecycle events of the underlying ValueAnimator object.
+ */
+ private Animator.AnimatorListener mListener = null;
+
+ /**
+ * Listener for the update events of the underlying ValueAnimator object.
+ */
+ private ValueAnimator.AnimatorUpdateListener mUpdateListener = null;
+
+ /**
+ * A lazily-created ValueAnimator used in order to get some default animator properties
+ * (duration, start delay, interpolator, etc.).
+ */
+ private ValueAnimator mTempValueAnimator;
+
+ /**
+ * This listener is the mechanism by which the underlying Animator causes changes to the
+ * properties currently being animated, as well as the cleanup after an animation is
+ * complete.
+ */
+ private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener();
+
+ /**
+ * This list holds the properties that have been asked to animate. We allow the caller to
+ * request several animations prior to actually starting the underlying animator. This
+ * enables us to run one single animator to handle several properties in parallel. Each
+ * property is tossed onto the pending list until the animation actually starts (which is
+ * done by posting it onto mView), at which time the pending list is cleared and the properties
+ * on that list are added to the list of properties associated with that animator.
+ */
+ ArrayList<NameValuesHolder> mPendingAnimations = new ArrayList<NameValuesHolder>();
+ private Runnable mPendingSetupAction;
+ private Runnable mPendingCleanupAction;
+ private Runnable mPendingOnStartAction;
+ private Runnable mPendingOnEndAction;
+
+ /**
+ * Constants used to associate a property being requested and the mechanism used to set
+ * the property (this class calls directly into View to set the properties in question).
+ */
+ static final int NONE = 0x0000;
+ static final int TRANSLATION_X = 0x0001;
+ static final int TRANSLATION_Y = 0x0002;
+ static final int TRANSLATION_Z = 0x0004;
+ static final int SCALE_X = 0x0008;
+ static final int SCALE_Y = 0x0010;
+ static final int ROTATION = 0x0020;
+ static final int ROTATION_X = 0x0040;
+ static final int ROTATION_Y = 0x0080;
+ static final int X = 0x0100;
+ static final int Y = 0x0200;
+ static final int Z = 0x0400;
+ static final int ALPHA = 0x0800;
+
+ private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | TRANSLATION_Z |
+ SCALE_X | SCALE_Y | ROTATION | ROTATION_X | ROTATION_Y | X | Y | Z;
+
+ /**
+ * The mechanism by which the user can request several properties that are then animated
+ * together works by posting this Runnable to start the underlying Animator. Every time
+ * a property animation is requested, we cancel any previous postings of the Runnable
+ * and re-post it. This means that we will only ever run the Runnable (and thus start the
+ * underlying animator) after the caller is done setting the properties that should be
+ * animated together.
+ */
+ private Runnable mAnimationStarter = new Runnable() {
+ @Override
+ public void run() {
+ startAnimation();
+ }
+ };
+
+ /**
+ * This class holds information about the overall animation being run on the set of
+ * properties. The mask describes which properties are being animated and the
+ * values holder is the list of all property/value objects.
+ */
+ private static class PropertyBundle {
+ int mPropertyMask;
+ ArrayList<NameValuesHolder> mNameValuesHolder;
+
+ PropertyBundle(int propertyMask, ArrayList<NameValuesHolder> nameValuesHolder) {
+ mPropertyMask = propertyMask;
+ mNameValuesHolder = nameValuesHolder;
+ }
+
+ /**
+ * Removes the given property from being animated as a part of this
+ * PropertyBundle. If the property was a part of this bundle, it returns
+ * true to indicate that it was, in fact, canceled. This is an indication
+ * to the caller that a cancellation actually occurred.
+ *
+ * @param propertyConstant The property whose cancellation is requested.
+ * @return true if the given property is a part of this bundle and if it
+ * has therefore been canceled.
+ */
+ boolean cancel(int propertyConstant) {
+ if ((mPropertyMask & propertyConstant) != 0 && mNameValuesHolder != null) {
+ int count = mNameValuesHolder.size();
+ for (int i = 0; i < count; ++i) {
+ NameValuesHolder nameValuesHolder = mNameValuesHolder.get(i);
+ if (nameValuesHolder.mNameConstant == propertyConstant) {
+ mNameValuesHolder.remove(i);
+ mPropertyMask &= ~propertyConstant;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * This list tracks the list of properties being animated by any particular animator.
+ * In most situations, there would only ever be one animator running at a time. But it is
+ * possible to request some properties to animate together, then while those properties
+ * are animating, to request some other properties to animate together. The way that
+ * works is by having this map associate the group of properties being animated with the
+ * animator handling the animation. On every update event for an Animator, we ask the
+ * map for the associated properties and set them accordingly.
+ */
+ private HashMap<Animator, PropertyBundle> mAnimatorMap =
+ new HashMap<Animator, PropertyBundle>();
+ private HashMap<Animator, Runnable> mAnimatorSetupMap;
+ private HashMap<Animator, Runnable> mAnimatorCleanupMap;
+ private HashMap<Animator, Runnable> mAnimatorOnStartMap;
+ private HashMap<Animator, Runnable> mAnimatorOnEndMap;
+
+ /**
+ * This is the information we need to set each property during the animation.
+ * mNameConstant is used to set the appropriate field in View, and the from/delta
+ * values are used to calculate the animated value for a given animation fraction
+ * during the animation.
+ */
+ static class NameValuesHolder {
+ int mNameConstant;
+ float mFromValue;
+ float mDeltaValue;
+ NameValuesHolder(int nameConstant, float fromValue, float deltaValue) {
+ mNameConstant = nameConstant;
+ mFromValue = fromValue;
+ mDeltaValue = deltaValue;
+ }
+ }
+
+ /**
+ * Constructor, called by View. This is private by design, as the user should only
+ * get a ViewPropertyAnimator by calling View.animate().
+ *
+ * @param view The View associated with this ViewPropertyAnimator
+ */
+ ViewPropertyAnimator(View view) {
+ mView = view;
+ view.ensureTransformationInfo();
+ }
+
+ /**
+ * Sets the duration for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default value for ValueAnimator. Calling this method
+ * will cause the declared value to be used instead.
+ * @param duration The length of ensuing property animations, in milliseconds. The value
+ * cannot be negative.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setDuration(long duration) {
+ if (duration < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative duration: " +
+ duration);
+ }
+ mDurationSet = true;
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Returns the current duration of property animations. If the duration was set on this
+ * object, that value is returned. Otherwise, the default value of the underlying Animator
+ * is returned.
+ *
+ * @see #setDuration(long)
+ * @return The duration of animations, in milliseconds.
+ */
+ public long getDuration() {
+ if (mDurationSet) {
+ return mDuration;
+ } else {
+ // Just return the default from ValueAnimator, since that's what we'd get if
+ // the value has not been set otherwise
+ if (mTempValueAnimator == null) {
+ mTempValueAnimator = new ValueAnimator();
+ }
+ return mTempValueAnimator.getDuration();
+ }
+ }
+
+ /**
+ * Returns the current startDelay of property animations. If the startDelay was set on this
+ * object, that value is returned. Otherwise, the default value of the underlying Animator
+ * is returned.
+ *
+ * @see #setStartDelay(long)
+ * @return The startDelay of animations, in milliseconds.
+ */
+ public long getStartDelay() {
+ if (mStartDelaySet) {
+ return mStartDelay;
+ } else {
+ // Just return the default from ValueAnimator (0), since that's what we'd get if
+ // the value has not been set otherwise
+ return 0;
+ }
+ }
+
+ /**
+ * Sets the startDelay for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default value for ValueAnimator. Calling this method
+ * will cause the declared value to be used instead.
+ * @param startDelay The delay of ensuing property animations, in milliseconds. The value
+ * cannot be negative.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setStartDelay(long startDelay) {
+ if (startDelay < 0) {
+ throw new IllegalArgumentException("Animators cannot have negative start " +
+ "delay: " + startDelay);
+ }
+ mStartDelaySet = true;
+ mStartDelay = startDelay;
+ return this;
+ }
+
+ /**
+ * Sets the interpolator for the underlying animator that animates the requested properties.
+ * By default, the animator uses the default interpolator for ValueAnimator. Calling this method
+ * will cause the declared object to be used instead.
+ *
+ * @param interpolator The TimeInterpolator to be used for ensuing property animations. A value
+ * of <code>null</code> will result in linear interpolation.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setInterpolator(TimeInterpolator interpolator) {
+ mInterpolatorSet = true;
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ /**
+ * Returns the timing interpolator that this animation uses.
+ *
+ * @return The timing interpolator for this animation.
+ */
+ public TimeInterpolator getInterpolator() {
+ if (mInterpolatorSet) {
+ return mInterpolator;
+ } else {
+ // Just return the default from ValueAnimator, since that's what we'd get if
+ // the value has not been set otherwise
+ if (mTempValueAnimator == null) {
+ mTempValueAnimator = new ValueAnimator();
+ }
+ return mTempValueAnimator.getInterpolator();
+ }
+ }
+
+ /**
+ * Sets a listener for events in the underlying Animators that run the property
+ * animations.
+ *
+ * @see Animator.AnimatorListener
+ *
+ * @param listener The listener to be called with AnimatorListener events. A value of
+ * <code>null</code> removes any existing listener.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setListener(Animator.AnimatorListener listener) {
+ mListener = listener;
+ return this;
+ }
+
+ Animator.AnimatorListener getListener() {
+ return mListener;
+ }
+
+ /**
+ * Sets a listener for update events in the underlying ValueAnimator that runs
+ * the property animations. Note that the underlying animator is animating between
+ * 0 and 1 (these values are then turned into the actual property values internally
+ * by ViewPropertyAnimator). So the animator cannot give information on the current
+ * values of the properties being animated by this ViewPropertyAnimator, although
+ * the view object itself can be queried to get the current values.
+ *
+ * @see android.animation.ValueAnimator.AnimatorUpdateListener
+ *
+ * @param listener The listener to be called with update events. A value of
+ * <code>null</code> removes any existing listener.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator setUpdateListener(ValueAnimator.AnimatorUpdateListener listener) {
+ mUpdateListener = listener;
+ return this;
+ }
+
+ ValueAnimator.AnimatorUpdateListener getUpdateListener() {
+ return mUpdateListener;
+ }
+
+ /**
+ * Starts the currently pending property animations immediately. Calling <code>start()</code>
+ * is optional because all animations start automatically at the next opportunity. However,
+ * if the animations are needed to start immediately and synchronously (not at the time when
+ * the next event is processed by the hierarchy, which is when the animations would begin
+ * otherwise), then this method can be used.
+ */
+ public void start() {
+ mView.removeCallbacks(mAnimationStarter);
+ startAnimation();
+ }
+
+ /**
+ * Cancels all property animations that are currently running or pending.
+ */
+ public void cancel() {
+ if (mAnimatorMap.size() > 0) {
+ HashMap<Animator, PropertyBundle> mAnimatorMapCopy =
+ (HashMap<Animator, PropertyBundle>)mAnimatorMap.clone();
+ Set<Animator> animatorSet = mAnimatorMapCopy.keySet();
+ for (Animator runningAnim : animatorSet) {
+ runningAnim.cancel();
+ }
+ }
+ mPendingAnimations.clear();
+ mPendingSetupAction = null;
+ mPendingCleanupAction = null;
+ mPendingOnStartAction = null;
+ mPendingOnEndAction = null;
+ mView.removeCallbacks(mAnimationStarter);
+ }
+
+ /**
+ * This method will cause the View's <code>x</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator x(float value) {
+ animateProperty(X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>x</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator xBy(float value) {
+ animatePropertyBy(X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>y</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator y(float value) {
+ animateProperty(Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>y</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator yBy(float value) {
+ animatePropertyBy(Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>z</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setZ(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator z(float value) {
+ animateProperty(Z, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>z</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setZ(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator zBy(float value) {
+ animatePropertyBy(Z, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>rotation</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setRotation(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator rotation(float value) {
+ animateProperty(ROTATION, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>rotation</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setRotation(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator rotationBy(float value) {
+ animatePropertyBy(ROTATION, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>rotationX</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setRotationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator rotationX(float value) {
+ animateProperty(ROTATION_X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>rotationX</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setRotationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator rotationXBy(float value) {
+ animatePropertyBy(ROTATION_X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>rotationY</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setRotationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator rotationY(float value) {
+ animateProperty(ROTATION_Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>rotationY</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setRotationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator rotationYBy(float value) {
+ animatePropertyBy(ROTATION_Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>translationX</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setTranslationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator translationX(float value) {
+ animateProperty(TRANSLATION_X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>translationX</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setTranslationX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator translationXBy(float value) {
+ animatePropertyBy(TRANSLATION_X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>translationY</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setTranslationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator translationY(float value) {
+ animateProperty(TRANSLATION_Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>translationY</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setTranslationY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator translationYBy(float value) {
+ animatePropertyBy(TRANSLATION_Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>translationZ</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setTranslationZ(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator translationZ(float value) {
+ animateProperty(TRANSLATION_Z, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>translationZ</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setTranslationZ(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator translationZBy(float value) {
+ animatePropertyBy(TRANSLATION_Z, value);
+ return this;
+ }
+ /**
+ * This method will cause the View's <code>scaleX</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setScaleX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator scaleX(float value) {
+ animateProperty(SCALE_X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>scaleX</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setScaleX(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator scaleXBy(float value) {
+ animatePropertyBy(SCALE_X, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>scaleY</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setScaleY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator scaleY(float value) {
+ animateProperty(SCALE_Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>scaleY</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setScaleY(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator scaleYBy(float value) {
+ animatePropertyBy(SCALE_Y, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>alpha</code> property to be animated to the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The value to be animated to.
+ * @see View#setAlpha(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator alpha(@FloatRange(from = 0.0f, to = 1.0f) float value) {
+ animateProperty(ALPHA, value);
+ return this;
+ }
+
+ /**
+ * This method will cause the View's <code>alpha</code> property to be animated by the
+ * specified value. Animations already running on the property will be canceled.
+ *
+ * @param value The amount to be animated by, as an offset from the current value.
+ * @see View#setAlpha(float)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator alphaBy(float value) {
+ animatePropertyBy(ALPHA, value);
+ return this;
+ }
+
+ /**
+ * The View associated with this ViewPropertyAnimator will have its
+ * {@link View#setLayerType(int, android.graphics.Paint) layer type} set to
+ * {@link View#LAYER_TYPE_HARDWARE} for the duration of the next animation.
+ * As stated in the documentation for {@link View#LAYER_TYPE_HARDWARE},
+ * the actual type of layer used internally depends on the runtime situation of the
+ * view. If the activity and this view are hardware-accelerated, then the layer will be
+ * accelerated as well. If the activity or the view is not accelerated, then the layer will
+ * effectively be the same as {@link View#LAYER_TYPE_SOFTWARE}.
+ *
+ * <p>This state is not persistent, either on the View or on this ViewPropertyAnimator: the
+ * layer type of the View will be restored when the animation ends to what it was when this
+ * method was called, and this setting on ViewPropertyAnimator is only valid for the next
+ * animation. Note that calling this method and then independently setting the layer type of
+ * the View (by a direct call to {@link View#setLayerType(int, android.graphics.Paint)}) will
+ * result in some inconsistency, including having the layer type restored to its pre-withLayer()
+ * value when the animation ends.</p>
+ *
+ * @see View#setLayerType(int, android.graphics.Paint)
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator withLayer() {
+ mPendingSetupAction= new Runnable() {
+ @Override
+ public void run() {
+ mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ if (mView.isAttachedToWindow()) {
+ mView.buildLayer();
+ }
+ }
+ };
+ final int currentLayerType = mView.getLayerType();
+ mPendingCleanupAction = new Runnable() {
+ @Override
+ public void run() {
+ mView.setLayerType(currentLayerType, null);
+ }
+ };
+ if (mAnimatorSetupMap == null) {
+ mAnimatorSetupMap = new HashMap<Animator, Runnable>();
+ }
+ if (mAnimatorCleanupMap == null) {
+ mAnimatorCleanupMap = new HashMap<Animator, Runnable>();
+ }
+
+ return this;
+ }
+
+ /**
+ * Specifies an action to take place when the next animation runs. If there is a
+ * {@link #setStartDelay(long) startDelay} set on this ViewPropertyAnimator, then the
+ * action will run after that startDelay expires, when the actual animation begins.
+ * This method, along with {@link #withEndAction(Runnable)}, is intended to help facilitate
+ * choreographing ViewPropertyAnimator animations with other animations or actions
+ * in the application.
+ *
+ * @param runnable The action to run when the next animation starts.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator withStartAction(Runnable runnable) {
+ mPendingOnStartAction = runnable;
+ if (runnable != null && mAnimatorOnStartMap == null) {
+ mAnimatorOnStartMap = new HashMap<Animator, Runnable>();
+ }
+ return this;
+ }
+
+ /**
+ * Specifies an action to take place when the next animation ends. The action is only
+ * run if the animation ends normally; if the ViewPropertyAnimator is canceled during
+ * that animation, the runnable will not run.
+ * This method, along with {@link #withStartAction(Runnable)}, is intended to help facilitate
+ * choreographing ViewPropertyAnimator animations with other animations or actions
+ * in the application.
+ *
+ * <p>For example, the following code animates a view to x=200 and then back to 0:</p>
+ * <pre>
+ * Runnable endAction = new Runnable() {
+ * public void run() {
+ * view.animate().x(0);
+ * }
+ * };
+ * view.animate().x(200).withEndAction(endAction);
+ * </pre>
+ *
+ * @param runnable The action to run when the next animation ends.
+ * @return This object, allowing calls to methods in this class to be chained.
+ */
+ public ViewPropertyAnimator withEndAction(Runnable runnable) {
+ mPendingOnEndAction = runnable;
+ if (runnable != null && mAnimatorOnEndMap == null) {
+ mAnimatorOnEndMap = new HashMap<Animator, Runnable>();
+ }
+ return this;
+ }
+
+ boolean hasActions() {
+ return mPendingSetupAction != null
+ || mPendingCleanupAction != null
+ || mPendingOnStartAction != null
+ || mPendingOnEndAction != null;
+ }
+
+ /**
+ * Starts the underlying Animator for a set of properties. We use a single animator that
+ * simply runs from 0 to 1, and then use that fractional value to set each property
+ * value accordingly.
+ */
+ private void startAnimation() {
+ mView.setHasTransientState(true);
+ ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
+ ArrayList<NameValuesHolder> nameValueList =
+ (ArrayList<NameValuesHolder>) mPendingAnimations.clone();
+ mPendingAnimations.clear();
+ int propertyMask = 0;
+ int propertyCount = nameValueList.size();
+ for (int i = 0; i < propertyCount; ++i) {
+ NameValuesHolder nameValuesHolder = nameValueList.get(i);
+ propertyMask |= nameValuesHolder.mNameConstant;
+ }
+ mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList));
+ if (mPendingSetupAction != null) {
+ mAnimatorSetupMap.put(animator, mPendingSetupAction);
+ mPendingSetupAction = null;
+ }
+ if (mPendingCleanupAction != null) {
+ mAnimatorCleanupMap.put(animator, mPendingCleanupAction);
+ mPendingCleanupAction = null;
+ }
+ if (mPendingOnStartAction != null) {
+ mAnimatorOnStartMap.put(animator, mPendingOnStartAction);
+ mPendingOnStartAction = null;
+ }
+ if (mPendingOnEndAction != null) {
+ mAnimatorOnEndMap.put(animator, mPendingOnEndAction);
+ mPendingOnEndAction = null;
+ }
+ animator.addUpdateListener(mAnimatorEventListener);
+ animator.addListener(mAnimatorEventListener);
+ if (mStartDelaySet) {
+ animator.setStartDelay(mStartDelay);
+ }
+ if (mDurationSet) {
+ animator.setDuration(mDuration);
+ }
+ if (mInterpolatorSet) {
+ animator.setInterpolator(mInterpolator);
+ }
+ animator.start();
+ }
+
+ /**
+ * Utility function, called by the various x(), y(), etc. methods. This stores the
+ * constant name for the property along with the from/delta values that will be used to
+ * calculate and set the property during the animation. This structure is added to the
+ * pending animations, awaiting the eventual start() of the underlying animator. A
+ * Runnable is posted to start the animation, and any pending such Runnable is canceled
+ * (which enables us to end up starting just one animator for all of the properties
+ * specified at one time).
+ *
+ * @param constantName The specifier for the property being animated
+ * @param toValue The value to which the property will animate
+ */
+ private void animateProperty(int constantName, float toValue) {
+ float fromValue = getValue(constantName);
+ float deltaValue = toValue - fromValue;
+ animatePropertyBy(constantName, fromValue, deltaValue);
+ }
+
+ /**
+ * Utility function, called by the various xBy(), yBy(), etc. methods. This method is
+ * just like animateProperty(), except the value is an offset from the property's
+ * current value, instead of an absolute "to" value.
+ *
+ * @param constantName The specifier for the property being animated
+ * @param byValue The amount by which the property will change
+ */
+ private void animatePropertyBy(int constantName, float byValue) {
+ float fromValue = getValue(constantName);
+ animatePropertyBy(constantName, fromValue, byValue);
+ }
+
+ /**
+ * Utility function, called by animateProperty() and animatePropertyBy(), which handles the
+ * details of adding a pending animation and posting the request to start the animation.
+ *
+ * @param constantName The specifier for the property being animated
+ * @param startValue The starting value of the property
+ * @param byValue The amount by which the property will change
+ */
+ private void animatePropertyBy(int constantName, float startValue, float byValue) {
+ // First, cancel any existing animations on this property
+ if (mAnimatorMap.size() > 0) {
+ Animator animatorToCancel = null;
+ Set<Animator> animatorSet = mAnimatorMap.keySet();
+ for (Animator runningAnim : animatorSet) {
+ PropertyBundle bundle = mAnimatorMap.get(runningAnim);
+ if (bundle.cancel(constantName)) {
+ // property was canceled - cancel the animation if it's now empty
+ // Note that it's safe to break out here because every new animation
+ // on a property will cancel a previous animation on that property, so
+ // there can only ever be one such animation running.
+ if (bundle.mPropertyMask == NONE) {
+ // the animation is no longer changing anything - cancel it
+ animatorToCancel = runningAnim;
+ break;
+ }
+ }
+ }
+ if (animatorToCancel != null) {
+ animatorToCancel.cancel();
+ }
+ }
+
+ NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue);
+ mPendingAnimations.add(nameValuePair);
+ mView.removeCallbacks(mAnimationStarter);
+ mView.postOnAnimation(mAnimationStarter);
+ }
+
+ /**
+ * This method handles setting the property values directly in the View object's fields.
+ * propertyConstant tells it which property should be set, value is the value to set
+ * the property to.
+ *
+ * @param propertyConstant The property to be set
+ * @param value The value to set the property to
+ */
+ private void setValue(int propertyConstant, float value) {
+ final RenderNode renderNode = mView.mRenderNode;
+ switch (propertyConstant) {
+ case TRANSLATION_X:
+ renderNode.setTranslationX(value);
+ break;
+ case TRANSLATION_Y:
+ renderNode.setTranslationY(value);
+ break;
+ case TRANSLATION_Z:
+ renderNode.setTranslationZ(value);
+ break;
+ case ROTATION:
+ renderNode.setRotationZ(value);
+ break;
+ case ROTATION_X:
+ renderNode.setRotationX(value);
+ break;
+ case ROTATION_Y:
+ renderNode.setRotationY(value);
+ break;
+ case SCALE_X:
+ renderNode.setScaleX(value);
+ break;
+ case SCALE_Y:
+ renderNode.setScaleY(value);
+ break;
+ case X:
+ renderNode.setTranslationX(value - mView.mLeft);
+ break;
+ case Y:
+ renderNode.setTranslationY(value - mView.mTop);
+ break;
+ case Z:
+ renderNode.setTranslationZ(value - renderNode.getElevation());
+ break;
+ case ALPHA:
+ mView.setAlphaInternal(value);
+ renderNode.setAlpha(value);
+ break;
+ }
+ }
+
+ /**
+ * This method gets the value of the named property from the View object.
+ *
+ * @param propertyConstant The property whose value should be returned
+ * @return float The value of the named property
+ */
+ private float getValue(int propertyConstant) {
+ final RenderNode node = mView.mRenderNode;
+ switch (propertyConstant) {
+ case TRANSLATION_X:
+ return node.getTranslationX();
+ case TRANSLATION_Y:
+ return node.getTranslationY();
+ case TRANSLATION_Z:
+ return node.getTranslationZ();
+ case ROTATION:
+ return node.getRotationZ();
+ case ROTATION_X:
+ return node.getRotationX();
+ case ROTATION_Y:
+ return node.getRotationY();
+ case SCALE_X:
+ return node.getScaleX();
+ case SCALE_Y:
+ return node.getScaleY();
+ case X:
+ return mView.mLeft + node.getTranslationX();
+ case Y:
+ return mView.mTop + node.getTranslationY();
+ case Z:
+ return node.getElevation() + node.getTranslationZ();
+ case ALPHA:
+ return mView.getAlpha();
+ }
+ return 0;
+ }
+
+ /**
+ * Utility class that handles the various Animator events. The only ones we care
+ * about are the end event (which we use to clean up the animator map when an animator
+ * finishes) and the update event (which we use to calculate the current value of each
+ * property and then set it on the view object).
+ */
+ private class AnimatorEventListener
+ implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mAnimatorSetupMap != null) {
+ Runnable r = mAnimatorSetupMap.get(animation);
+ if (r != null) {
+ r.run();
+ }
+ mAnimatorSetupMap.remove(animation);
+ }
+ if (mAnimatorOnStartMap != null) {
+ Runnable r = mAnimatorOnStartMap.get(animation);
+ if (r != null) {
+ r.run();
+ }
+ mAnimatorOnStartMap.remove(animation);
+ }
+ if (mListener != null) {
+ mListener.onAnimationStart(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationCancel(animation);
+ }
+ if (mAnimatorOnEndMap != null) {
+ mAnimatorOnEndMap.remove(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ if (mListener != null) {
+ mListener.onAnimationRepeat(animation);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mView.setHasTransientState(false);
+ if (mAnimatorCleanupMap != null) {
+ Runnable r = mAnimatorCleanupMap.get(animation);
+ if (r != null) {
+ r.run();
+ }
+ mAnimatorCleanupMap.remove(animation);
+ }
+ if (mListener != null) {
+ mListener.onAnimationEnd(animation);
+ }
+ if (mAnimatorOnEndMap != null) {
+ Runnable r = mAnimatorOnEndMap.get(animation);
+ if (r != null) {
+ r.run();
+ }
+ mAnimatorOnEndMap.remove(animation);
+ }
+ mAnimatorMap.remove(animation);
+ }
+
+ /**
+ * Calculate the current value for each property and set it on the view. Invalidate
+ * the view object appropriately, depending on which properties are being animated.
+ *
+ * @param animation The animator associated with the properties that need to be
+ * set. This animator holds the animation fraction which we will use to calculate
+ * the current value of each property.
+ */
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ PropertyBundle propertyBundle = mAnimatorMap.get(animation);
+ if (propertyBundle == null) {
+ // Shouldn't happen, but just to play it safe
+ return;
+ }
+
+ boolean hardwareAccelerated = mView.isHardwareAccelerated();
+
+ // alpha requires slightly different treatment than the other (transform) properties.
+ // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation
+ // logic is dependent on how the view handles an internal call to onSetAlpha().
+ // We track what kinds of properties are set, and how alpha is handled when it is
+ // set, and perform the invalidation steps appropriately.
+ boolean alphaHandled = false;
+ if (!hardwareAccelerated) {
+ mView.invalidateParentCaches();
+ }
+ float fraction = animation.getAnimatedFraction();
+ int propertyMask = propertyBundle.mPropertyMask;
+ if ((propertyMask & TRANSFORM_MASK) != 0) {
+ mView.invalidateViewProperty(hardwareAccelerated, false);
+ }
+ ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder;
+ if (valueList != null) {
+ int count = valueList.size();
+ for (int i = 0; i < count; ++i) {
+ NameValuesHolder values = valueList.get(i);
+ float value = values.mFromValue + fraction * values.mDeltaValue;
+ if (values.mNameConstant == ALPHA) {
+ alphaHandled = mView.setAlphaNoInvalidation(value);
+ } else {
+ setValue(values.mNameConstant, value);
+ }
+ }
+ }
+ if ((propertyMask & TRANSFORM_MASK) != 0) {
+ if (!hardwareAccelerated) {
+ mView.mPrivateFlags |= View.PFLAG_DRAWN; // force another invalidation
+ }
+ }
+ // invalidate(false) in all cases except if alphaHandled gets set to true
+ // via the call to setAlphaNoInvalidation(), above
+ if (alphaHandled) {
+ mView.invalidate(true);
+ } else {
+ mView.invalidateViewProperty(false, false);
+ }
+ if (mUpdateListener != null) {
+ mUpdateListener.onAnimationUpdate(animation);
+ }
+ }
+ }
+}
diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java
new file mode 100644
index 0000000..3550a31
--- /dev/null
+++ b/android/view/ViewRootImpl.java
@@ -0,0 +1,10425 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.InputDevice.SOURCE_CLASS_NONE;
+import static android.view.InsetsState.ITYPE_IME;
+import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.InsetsState.SIZE;
+import static android.view.View.PFLAG_DRAW_ANIMATION;
+import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
+import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewRootImplProto.ADDED;
+import static android.view.ViewRootImplProto.APP_VISIBLE;
+import static android.view.ViewRootImplProto.CUR_SCROLL_Y;
+import static android.view.ViewRootImplProto.DISPLAY_ID;
+import static android.view.ViewRootImplProto.HEIGHT;
+import static android.view.ViewRootImplProto.IS_ANIMATING;
+import static android.view.ViewRootImplProto.IS_DRAWING;
+import static android.view.ViewRootImplProto.LAST_WINDOW_INSETS;
+import static android.view.ViewRootImplProto.REMOVED;
+import static android.view.ViewRootImplProto.SCROLL_Y;
+import static android.view.ViewRootImplProto.SOFT_INPUT_MODE;
+import static android.view.ViewRootImplProto.VIEW;
+import static android.view.ViewRootImplProto.VISIBLE_RECT;
+import static android.view.ViewRootImplProto.WIDTH;
+import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES;
+import static android.view.ViewRootImplProto.WIN_FRAME;
+import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER;
+import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
+import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
+import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
+import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
+import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_APPEARANCE_CONTROLLED;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FIT_INSETS_CONTROLLED;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
+import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
+import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
+import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
+
+import android.Manifest;
+import android.animation.LayoutTransition;
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiContext;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.ResourcesManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.BLASTBufferQueue;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.FrameInfo;
+import android.graphics.HardwareRenderer;
+import android.graphics.HardwareRenderer.FrameDrawingCallback;
+import android.graphics.HardwareRendererObserver;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.RenderNode;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.hardware.input.InputManager;
+import android.media.AudioManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.sysprop.DisplayProperties;
+import android.util.AndroidRuntimeException;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.EventLog;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.LongArray;
+import android.util.MergedConfiguration;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+import android.util.TypedValue;
+import android.util.imetracing.ImeTracing;
+import android.util.proto.ProtoOutputStream;
+import android.view.InputDevice.InputSourceClass;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.Surface.OutOfResourcesException;
+import android.view.SurfaceControl.Transaction;
+import android.view.View.AttachInfo;
+import android.view.View.FocusDirection;
+import android.view.View.MeasureSpec;
+import android.view.Window.OnContentApplyWindowInsetsListener;
+import android.view.WindowInsets.Side.InsetsSide;
+import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+import android.view.accessibility.AccessibilityManager.HighTextContrastChangeListener;
+import android.view.accessibility.AccessibilityNodeIdManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.accessibility.IAccessibilityEmbeddedConnection;
+import android.view.accessibility.IAccessibilityInteractionConnection;
+import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
+import android.view.contentcapture.MainContentCaptureSession;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Scroller;
+import android.window.ClientWindowFrames;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.policy.DecorView;
+import com.android.internal.policy.PhoneFallbackEventHandler;
+import com.android.internal.util.Preconditions;
+import com.android.internal.view.BaseSurfaceHolder;
+import com.android.internal.view.RootViewSurfaceTaker;
+import com.android.internal.view.SurfaceCallbackHelper;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * The top of a view hierarchy, implementing the needed protocol between View
+ * and the WindowManager. This is for the most part an internal implementation
+ * detail of {@link WindowManagerGlobal}.
+ *
+ * {@hide}
+ */
+@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
+public final class ViewRootImpl implements ViewParent,
+ View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
+ AttachedSurfaceControl {
+ private static final String TAG = "ViewRootImpl";
+ private static final boolean DBG = false;
+ private static final boolean LOCAL_LOGV = false;
+ /** @noinspection PointlessBooleanExpression*/
+ private static final boolean DEBUG_DRAW = false || LOCAL_LOGV;
+ private static final boolean DEBUG_LAYOUT = false || LOCAL_LOGV;
+ private static final boolean DEBUG_DIALOG = false || LOCAL_LOGV;
+ private static final boolean DEBUG_INPUT_RESIZE = false || LOCAL_LOGV;
+ private static final boolean DEBUG_ORIENTATION = false || LOCAL_LOGV;
+ private static final boolean DEBUG_TRACKBALL = false || LOCAL_LOGV;
+ private static final boolean DEBUG_IMF = false || LOCAL_LOGV;
+ private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV;
+ private static final boolean DEBUG_FPS = false;
+ private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
+ private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV;
+ private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV;
+ private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV;
+ private static final boolean DEBUG_BLAST = false || LOCAL_LOGV;
+
+ /**
+ * Set to false if we do not want to use the multi threaded renderer even though
+ * threaded renderer (aka hardware renderering) is used. Note that by disabling
+ * this, WindowCallbacks will not fire.
+ */
+ private static final boolean MT_RENDERER_AVAILABLE = true;
+
+ /**
+ * Whether or not to report end-to-end input latency. Disabled temporarily as a
+ * risk mitigation against potential jank caused by acquiring a weak reference
+ * per frame
+ */
+ private static final boolean ENABLE_INPUT_LATENCY_TRACKING = false;
+
+ /**
+ * Set this system property to true to force the view hierarchy to render
+ * at 60 Hz. This can be used to measure the potential framerate.
+ */
+ private static final String PROPERTY_PROFILE_RENDERING = "viewroot.profile_rendering";
+
+ /**
+ * Maximum time we allow the user to roll the trackball enough to generate
+ * a key event, before resetting the counters.
+ */
+ static final int MAX_TRACKBALL_DELAY = 250;
+
+ /**
+ * Initial value for {@link #mContentCaptureEnabled}.
+ */
+ private static final int CONTENT_CAPTURE_ENABLED_NOT_CHECKED = 0;
+
+ /**
+ * Value for {@link #mContentCaptureEnabled} when it was checked and set to {@code true}.
+ */
+ private static final int CONTENT_CAPTURE_ENABLED_TRUE = 1;
+
+ /**
+ * Value for {@link #mContentCaptureEnabled} when it was checked and set to {@code false}.
+ */
+ private static final int CONTENT_CAPTURE_ENABLED_FALSE = 2;
+
+ /**
+ * Maximum time to wait for {@link View#dispatchScrollCaptureSearch} to complete.
+ */
+ private static final int SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS = 2500;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
+
+ static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<>();
+ static boolean sFirstDrawComplete = false;
+
+ /**
+ * Callback for notifying about global configuration changes.
+ */
+ public interface ConfigChangedCallback {
+
+ /** Notifies about global config change. */
+ void onConfigurationChanged(Configuration globalConfig);
+ }
+
+ private static final ArrayList<ConfigChangedCallback> sConfigCallbacks = new ArrayList<>();
+
+ /**
+ * Callback for notifying activities about override configuration changes.
+ */
+ public interface ActivityConfigCallback {
+
+ /**
+ * Notifies about override config change and/or move to different display.
+ * @param overrideConfig New override config to apply to activity.
+ * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed.
+ */
+ void onConfigurationChanged(Configuration overrideConfig, int newDisplayId);
+ }
+
+ /**
+ * Callback used to notify corresponding activity about override configuration change and make
+ * sure that all resources are set correctly before updating the ViewRootImpl's internal state.
+ */
+ private ActivityConfigCallback mActivityConfigCallback;
+
+ /**
+ * Used when configuration change first updates the config of corresponding activity.
+ * In that case we receive a call back from {@link ActivityThread} and this flag is used to
+ * preserve the initial value.
+ *
+ * @see #performConfigurationChange(MergedConfiguration, boolean, int)
+ */
+ private boolean mForceNextConfigUpdate;
+
+ private boolean mUseBLASTAdapter;
+ private boolean mForceDisableBLAST;
+
+ private boolean mFastScrollSoundEffectsEnabled;
+
+ /**
+ * Signals that compatibility booleans have been initialized according to
+ * target SDK versions.
+ */
+ private static boolean sCompatibilityDone = false;
+
+ /**
+ * Always assign focus if a focusable View is available.
+ */
+ private static boolean sAlwaysAssignFocus;
+
+ /**
+ * This list must only be modified by the main thread, so a lock is only needed when changing
+ * the list or when accessing the list from a non-main thread.
+ */
+ @GuardedBy("mWindowCallbacks")
+ final ArrayList<WindowCallbacks> mWindowCallbacks = new ArrayList<>();
+ @UnsupportedAppUsage
+ @UiContext
+ public final Context mContext;
+
+ @UnsupportedAppUsage
+ final IWindowSession mWindowSession;
+ @NonNull Display mDisplay;
+ final DisplayManager mDisplayManager;
+ final String mBasePackageName;
+
+ final int[] mTmpLocation = new int[2];
+
+ final TypedValue mTmpValue = new TypedValue();
+
+ final Thread mThread;
+
+ final WindowLeaked mLocation;
+
+ public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
+
+ final W mWindow;
+
+ final IBinder mLeashToken;
+
+ final int mTargetSdkVersion;
+
+ @UnsupportedAppUsage
+ View mView;
+
+ View mAccessibilityFocusedHost;
+ AccessibilityNodeInfo mAccessibilityFocusedVirtualView;
+
+ // True if the window currently has pointer capture enabled.
+ boolean mPointerCapture;
+
+ int mViewVisibility;
+ boolean mAppVisible = true;
+ // For recents to freeform transition we need to keep drawing after the app receives information
+ // that it became invisible. This will ignore that information and depend on the decor view
+ // visibility to control drawing. The decor view visibility will get adjusted when the app get
+ // stopped and that's when the app will stop drawing further frames.
+ private boolean mForceDecorViewVisibility = false;
+ // Used for tracking app visibility updates separately in case we get double change. This will
+ // make sure that we always call relayout for the corresponding window.
+ private boolean mAppVisibilityChanged;
+ int mOrigWindowType = -1;
+
+ /** Whether the window had focus during the most recent traversal. */
+ boolean mHadWindowFocus;
+
+ /**
+ * Whether the window lost focus during a previous traversal and has not
+ * yet gained it back. Used to determine whether a WINDOW_STATE_CHANGE
+ * accessibility events should be sent during traversal.
+ */
+ boolean mLostWindowFocus;
+
+ // Set to true if the owner of this window is in the stopped state,
+ // so the window should no longer be active.
+ @UnsupportedAppUsage
+ boolean mStopped = false;
+
+ // Set to true if the owner of this window is in ambient mode,
+ // which means it won't receive input events.
+ boolean mIsAmbientMode = false;
+
+ // Set to true to stop input during an Activity Transition.
+ boolean mPausedForTransition = false;
+
+ boolean mLastInCompatMode = false;
+
+ SurfaceHolder.Callback2 mSurfaceHolderCallback;
+ BaseSurfaceHolder mSurfaceHolder;
+ boolean mIsCreating;
+ boolean mDrawingAllowed;
+
+ final Region mTransparentRegion;
+ final Region mPreviousTransparentRegion;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ int mWidth;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ int mHeight;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Rect mDirty;
+ public boolean mIsAnimating;
+
+ private boolean mUseMTRenderer;
+ private boolean mDragResizing;
+ private boolean mInvalidateRootRequested;
+ private int mResizeMode;
+ private int mCanvasOffsetX;
+ private int mCanvasOffsetY;
+ private boolean mActivityRelaunched;
+
+ CompatibilityInfo.Translator mTranslator;
+
+ @UnsupportedAppUsage
+ final View.AttachInfo mAttachInfo;
+ final SystemUiVisibilityInfo mCompatibleVisibilityInfo;
+ int mDispatchedSystemUiVisibility;
+ int mDispatchedSystemBarAppearance;
+ InputQueue.Callback mInputQueueCallback;
+ InputQueue mInputQueue;
+ @UnsupportedAppUsage
+ FallbackEventHandler mFallbackEventHandler;
+ final Choreographer mChoreographer;
+ protected final ViewFrameInfo mViewFrameInfo = new ViewFrameInfo();
+ private final InputEventAssigner mInputEventAssigner = new InputEventAssigner();
+
+ // Set to true if mSurfaceControl is used for Webview Overlay
+ private boolean mIsForWebviewOverlay;
+
+ /**
+ * Update the Choreographer's FrameInfo object with the timing information for the current
+ * ViewRootImpl instance. Erase the data in the current ViewFrameInfo to prepare for the next
+ * frame.
+ * @return the updated FrameInfo object
+ */
+ protected @NonNull FrameInfo getUpdatedFrameInfo() {
+ // Since Choreographer is a thread-local singleton while we can have multiple
+ // ViewRootImpl's, populate the frame information from the current viewRootImpl before
+ // starting the draw
+ FrameInfo frameInfo = mChoreographer.mFrameInfo;
+ mViewFrameInfo.populateFrameInfo(frameInfo);
+ mViewFrameInfo.reset();
+ mInputEventAssigner.notifyFrameProcessed();
+ return frameInfo;
+ }
+
+ // used in relayout to get SurfaceControl size
+ // for BLAST adapter surface setup
+ private final Point mSurfaceSize = new Point();
+ private final Point mLastSurfaceSize = new Point();
+
+ final Rect mTempRect; // used in the transaction to not thrash the heap.
+ final Rect mVisRect; // used to retrieve visible rect of focused view.
+ private final Rect mTempBoundsRect = new Rect(); // used to set the size of the bounds surface.
+
+ // This is used to reduce the race between window focus changes being dispatched from
+ // the window manager and input events coming through the input system.
+ @GuardedBy("this")
+ boolean mWindowFocusChanged;
+ @GuardedBy("this")
+ boolean mUpcomingWindowFocus;
+ @GuardedBy("this")
+ boolean mUpcomingInTouchMode;
+
+ public boolean mTraversalScheduled;
+ int mTraversalBarrier;
+ boolean mWillDrawSoon;
+ /** Set to true while in performTraversals for detecting when die(true) is called from internal
+ * callbacks such as onMeasure, onPreDraw, onDraw and deferring doDie() until later. */
+ boolean mIsInTraversal;
+ boolean mApplyInsetsRequested;
+ boolean mLayoutRequested;
+ boolean mFirst;
+
+ @Nullable
+ int mContentCaptureEnabled = CONTENT_CAPTURE_ENABLED_NOT_CHECKED;
+ boolean mPerformContentCapture;
+
+ boolean mReportNextDraw;
+ boolean mFullRedrawNeeded;
+ boolean mNewSurfaceNeeded;
+ boolean mForceNextWindowRelayout;
+ CountDownLatch mWindowDrawCountDown;
+
+ boolean mIsDrawing;
+ int mLastSystemUiVisibility;
+ int mClientWindowLayoutFlags;
+
+ // Pool of queued input events.
+ private static final int MAX_QUEUED_INPUT_EVENT_POOL_SIZE = 10;
+ private QueuedInputEvent mQueuedInputEventPool;
+ private int mQueuedInputEventPoolSize;
+
+ /* Input event queue.
+ * Pending input events are input events waiting to be delivered to the input stages
+ * and handled by the application.
+ */
+ QueuedInputEvent mPendingInputEventHead;
+ QueuedInputEvent mPendingInputEventTail;
+ int mPendingInputEventCount;
+ boolean mProcessInputEventsScheduled;
+ boolean mUnbufferedInputDispatch;
+ @InputSourceClass
+ int mUnbufferedInputSource = SOURCE_CLASS_NONE;
+
+ String mPendingInputEventQueueLengthCounterName = "pq";
+
+ InputStage mFirstInputStage;
+ InputStage mFirstPostImeInputStage;
+ InputStage mSyntheticInputStage;
+
+ private final UnhandledKeyManager mUnhandledKeyManager = new UnhandledKeyManager();
+
+ boolean mWindowAttributesChanged = false;
+
+ // These can be accessed by any thread, must be protected with a lock.
+ // Surface can never be reassigned or cleared (use Surface.clear()).
+ @UnsupportedAppUsage
+ public final Surface mSurface = new Surface();
+ private final SurfaceControl mSurfaceControl = new SurfaceControl();
+
+ private BLASTBufferQueue mBlastBufferQueue;
+
+ /**
+ * Transaction object that can be used to synchronize child SurfaceControl changes with
+ * ViewRootImpl SurfaceControl changes by the server. The object is passed along with
+ * the SurfaceChangedCallback.
+ */
+ private final Transaction mSurfaceChangedTransaction = new Transaction();
+ /**
+ * Child container layer of {@code mSurface} with the same bounds as its parent, and cropped to
+ * the surface insets. This surface is created only if a client requests it via {@link
+ * #getBoundsLayer()}. By parenting to this bounds surface, child surfaces can ensure they do
+ * not draw into the surface inset region set by the parent window.
+ */
+ private SurfaceControl mBoundsLayer;
+ private final SurfaceSession mSurfaceSession = new SurfaceSession();
+ private final Transaction mTransaction = new Transaction();
+
+ @UnsupportedAppUsage
+ boolean mAdded;
+ boolean mAddedTouchMode;
+
+ /**
+ * It usually keeps the latest layout result from {@link IWindow#resized} or
+ * {@link IWindowSession#relayout}.
+ */
+ private final ClientWindowFrames mTmpFrames = new ClientWindowFrames();
+
+ // These are accessed by multiple threads.
+ final Rect mWinFrame; // frame given by window manager.
+
+ final Rect mPendingBackDropFrame = new Rect();
+
+ private boolean mWillMove;
+ private boolean mWillResize;
+
+ boolean mPendingAlwaysConsumeSystemBars;
+ private final InsetsState mTempInsets = new InsetsState();
+ private final InsetsSourceControl[] mTempControls = new InsetsSourceControl[SIZE];
+ final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets
+ = new ViewTreeObserver.InternalInsetsInfo();
+
+ private WindowInsets mLastWindowInsets;
+
+ // Insets types hidden by legacy window flags or system UI flags.
+ private @InsetsType int mTypesHiddenByFlags = 0;
+
+ /** Last applied configuration obtained from resources. */
+ private final Configuration mLastConfigurationFromResources = new Configuration();
+ /** Last configuration reported from WM or via {@link #MSG_UPDATE_CONFIGURATION}. */
+ private final MergedConfiguration mLastReportedMergedConfiguration = new MergedConfiguration();
+ /** Configurations waiting to be applied. */
+ private final MergedConfiguration mPendingMergedConfiguration = new MergedConfiguration();
+
+ boolean mScrollMayChange;
+ @SoftInputModeFlags
+ int mSoftInputMode;
+ @UnsupportedAppUsage
+ WeakReference<View> mLastScrolledFocus;
+ int mScrollY;
+ int mCurScrollY;
+ Scroller mScroller;
+ static final Interpolator mResizeInterpolator = new AccelerateDecelerateInterpolator();
+ private ArrayList<LayoutTransition> mPendingTransitions;
+
+ final ViewConfiguration mViewConfiguration;
+
+ /* Drag/drop */
+ ClipDescription mDragDescription;
+ View mCurrentDragView;
+ volatile Object mLocalDragState;
+ final PointF mDragPoint = new PointF();
+ final PointF mLastTouchPoint = new PointF();
+ int mLastTouchSource;
+
+ private boolean mProfileRendering;
+ private Choreographer.FrameCallback mRenderProfiler;
+ private boolean mRenderProfilingEnabled;
+
+ // Variables to track frames per second, enabled via DEBUG_FPS flag
+ private long mFpsStartTime = -1;
+ private long mFpsPrevTime = -1;
+ private int mFpsNumFrames;
+
+ private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ private PointerIcon mCustomPointerIcon = null;
+
+ /**
+ * see {@link #playSoundEffect(int)}
+ */
+ AudioManager mAudioManager;
+
+ final AccessibilityManager mAccessibilityManager;
+
+ AccessibilityInteractionController mAccessibilityInteractionController;
+
+ final AccessibilityInteractionConnectionManager mAccessibilityInteractionConnectionManager =
+ new AccessibilityInteractionConnectionManager();
+ final HighContrastTextManager mHighContrastTextManager;
+
+ SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
+
+ HashSet<View> mTempHashSet;
+
+ private final int mDensity;
+ private final int mNoncompatDensity;
+
+ private boolean mInLayout = false;
+ ArrayList<View> mLayoutRequesters = new ArrayList<View>();
+ boolean mHandlingLayoutInLayoutRequest = false;
+
+ private int mViewLayoutDirectionInitial;
+
+ /** Set to true once doDie() has been called. */
+ private boolean mRemoved;
+
+ private boolean mNeedsRendererSetup;
+
+ private final InputEventCompatProcessor mInputCompatProcessor;
+
+ /**
+ * Consistency verifier for debugging purposes.
+ */
+ protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
+ InputEventConsistencyVerifier.isInstrumentationEnabled() ?
+ new InputEventConsistencyVerifier(this, 0) : null;
+
+ private final InsetsController mInsetsController;
+ private final ImeFocusController mImeFocusController;
+
+ private boolean mIsSurfaceOpaque;
+
+ private final BackgroundBlurDrawable.Aggregator mBlurRegionAggregator =
+ new BackgroundBlurDrawable.Aggregator(this);
+
+ /**
+ * @return {@link ImeFocusController} for this instance.
+ */
+ @NonNull
+ public ImeFocusController getImeFocusController() {
+ return mImeFocusController;
+ }
+
+ private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker();
+
+ private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
+
+ static final class SystemUiVisibilityInfo {
+ int globalVisibility;
+ int localValue;
+ int localChanges;
+ }
+
+ /**
+ * This is only used when the UI thread is paused due to {@link #mNextDrawUseBlastSync} being
+ * set. Specifically, it's only used when calling
+ * {@link BLASTBufferQueue#setNextTransaction(Transaction)} and then merged with
+ * {@link #mSurfaceChangedTransaction}. It doesn't need to be thread safe since it's only
+ * accessed when the UI thread is paused.
+ */
+ private final SurfaceControl.Transaction mRtBLASTSyncTransaction =
+ new SurfaceControl.Transaction();
+
+ /**
+ * Keeps track of whether the WM requested to use BLAST Sync when calling relayout. When set,
+ * we pause the UI thread to ensure we don't get overlapping requests. We then send a
+ * transaction to {@link BLASTBufferQueue#setNextTransaction(Transaction)}, which is then sent
+ * back to WM to synchronize.
+ *
+ * This flag is set to false only after the synchronized transaction that contains the buffer
+ * has been sent to SurfaceFlinger.
+ */
+ private boolean mNextDrawUseBlastSync = false;
+
+ /**
+ * Wait for the blast sync transaction complete callback before drawing and queuing up more
+ * frames. This will prevent out of order buffers submissions when WM has requested to
+ * synchronize with the client.
+ */
+ private boolean mWaitForBlastSyncComplete = false;
+
+ /**
+ * Keeps track of whether a traverse was triggered while the UI thread was paused. This can
+ * occur when the client is waiting on another process to submit the transaction that
+ * contains the buffer. The UI thread needs to wait on the callback before it can submit
+ * another buffer.
+ */
+ private boolean mRequestedTraverseWhilePaused = false;
+
+ private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks;
+
+ private long mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS;
+
+ /**
+ * Increment this value when the surface has been replaced.
+ */
+ private int mSurfaceSequenceId = 0;
+
+ private String mTag = TAG;
+
+ public ViewRootImpl(Context context, Display display) {
+ this(context, display, WindowManagerGlobal.getWindowSession(),
+ false /* useSfChoreographer */);
+ }
+
+ public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session) {
+ this(context, display, session, false /* useSfChoreographer */);
+ }
+
+ public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
+ boolean useSfChoreographer) {
+ mContext = context;
+ mWindowSession = session;
+ mDisplay = display;
+ mBasePackageName = context.getBasePackageName();
+ mThread = Thread.currentThread();
+ mLocation = new WindowLeaked(null);
+ mLocation.fillInStackTrace();
+ mWidth = -1;
+ mHeight = -1;
+ mDirty = new Rect();
+ mTempRect = new Rect();
+ mVisRect = new Rect();
+ mWinFrame = new Rect();
+ mWindow = new W(this);
+ mLeashToken = new Binder();
+ mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+ mViewVisibility = View.GONE;
+ mTransparentRegion = new Region();
+ mPreviousTransparentRegion = new Region();
+ mFirst = true; // true for the first time the view is added
+ mPerformContentCapture = true; // also true for the first time the view is added
+ mAdded = false;
+ mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
+ context);
+ mCompatibleVisibilityInfo = new SystemUiVisibilityInfo();
+ mAccessibilityManager = AccessibilityManager.getInstance(context);
+ mAccessibilityManager.addAccessibilityStateChangeListener(
+ mAccessibilityInteractionConnectionManager, mHandler);
+ mHighContrastTextManager = new HighContrastTextManager();
+ mAccessibilityManager.addHighTextContrastStateChangeListener(
+ mHighContrastTextManager, mHandler);
+ mViewConfiguration = ViewConfiguration.get(context);
+ mDensity = context.getResources().getDisplayMetrics().densityDpi;
+ mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
+ mFallbackEventHandler = new PhoneFallbackEventHandler(context);
+ mChoreographer = useSfChoreographer
+ ? Choreographer.getSfInstance() : Choreographer.getInstance();
+ mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
+ mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
+
+ String processorOverrideName = context.getResources().getString(
+ R.string.config_inputEventCompatProcessorOverrideClassName);
+ if (processorOverrideName.isEmpty()) {
+ // No compatibility processor override, using default.
+ mInputCompatProcessor = new InputEventCompatProcessor(context);
+ } else {
+ InputEventCompatProcessor compatProcessor = null;
+ try {
+ final Class<? extends InputEventCompatProcessor> klass =
+ (Class<? extends InputEventCompatProcessor>) Class.forName(
+ processorOverrideName);
+ compatProcessor = klass.getConstructor(Context.class).newInstance(context);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to create the InputEventCompatProcessor. ", e);
+ } finally {
+ mInputCompatProcessor = compatProcessor;
+ }
+ }
+
+ if (!sCompatibilityDone) {
+ sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P;
+
+ sCompatibilityDone = true;
+ }
+
+ loadSystemProperties();
+ mImeFocusController = new ImeFocusController(this);
+ AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+ mFastScrollSoundEffectsEnabled = audioManager.areNavigationRepeatSoundEffectsEnabled();
+
+ mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS;
+ }
+
+ public static void addFirstDrawHandler(Runnable callback) {
+ synchronized (sFirstDrawHandlers) {
+ if (!sFirstDrawComplete) {
+ sFirstDrawHandlers.add(callback);
+ }
+ }
+ }
+
+ /** Add static config callback to be notified about global config changes. */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static void addConfigCallback(ConfigChangedCallback callback) {
+ synchronized (sConfigCallbacks) {
+ sConfigCallbacks.add(callback);
+ }
+ }
+
+ /** Add activity config callback to be notified about override config changes. */
+ public void setActivityConfigCallback(ActivityConfigCallback callback) {
+ mActivityConfigCallback = callback;
+ }
+
+ public void setOnContentApplyWindowInsetsListener(OnContentApplyWindowInsetsListener listener) {
+ mAttachInfo.mContentOnApplyWindowInsetsListener = listener;
+
+ // System windows will be fitted on first traversal, so no reason to request additional
+ // (possibly getting executed after the first traversal).
+ if (!mFirst) {
+ requestFitSystemWindows();
+ }
+ }
+
+ public void addWindowCallbacks(WindowCallbacks callback) {
+ synchronized (mWindowCallbacks) {
+ mWindowCallbacks.add(callback);
+ }
+ }
+
+ public void removeWindowCallbacks(WindowCallbacks callback) {
+ synchronized (mWindowCallbacks) {
+ mWindowCallbacks.remove(callback);
+ }
+ }
+
+ public void reportDrawFinish() {
+ if (mWindowDrawCountDown != null) {
+ mWindowDrawCountDown.countDown();
+ }
+ }
+
+ // FIXME for perf testing only
+ private boolean mProfile = false;
+
+ /**
+ * Call this to profile the next traversal call.
+ * FIXME for perf testing only. Remove eventually
+ */
+ public void profile() {
+ mProfile = true;
+ }
+
+ /**
+ * Indicates whether we are in touch mode. Calling this method triggers an IPC
+ * call and should be avoided whenever possible.
+ *
+ * @return True, if the device is in touch mode, false otherwise.
+ *
+ * @hide
+ */
+ static boolean isInTouchMode() {
+ IWindowSession windowSession = WindowManagerGlobal.peekWindowSession();
+ if (windowSession != null) {
+ try {
+ return windowSession.getInTouchMode();
+ } catch (RemoteException e) {
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Notifies us that our child has been rebuilt, following
+ * a window preservation operation. In these cases we
+ * keep the same DecorView, but the activity controlling it
+ * is a different instance, and we need to update our
+ * callbacks.
+ *
+ * @hide
+ */
+ public void notifyChildRebuilt() {
+ if (mView instanceof RootViewSurfaceTaker) {
+ if (mSurfaceHolderCallback != null) {
+ mSurfaceHolder.removeCallback(mSurfaceHolderCallback);
+ }
+
+ mSurfaceHolderCallback =
+ ((RootViewSurfaceTaker)mView).willYouTakeTheSurface();
+
+ if (mSurfaceHolderCallback != null) {
+ mSurfaceHolder = new TakenSurfaceHolder();
+ mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
+ mSurfaceHolder.addCallback(mSurfaceHolderCallback);
+ } else {
+ mSurfaceHolder = null;
+ }
+
+ mInputQueueCallback =
+ ((RootViewSurfaceTaker)mView).willYouTakeTheInputQueue();
+ if (mInputQueueCallback != null) {
+ mInputQueueCallback.onInputQueueCreated(mInputQueue);
+ }
+ }
+ }
+
+ // TODO(b/161810301): Make this private after window layout is moved to the client side.
+ public static void computeWindowBounds(WindowManager.LayoutParams attrs, InsetsState state,
+ Rect displayFrame, Rect outBounds) {
+ final @InsetsType int typesToFit = attrs.getFitInsetsTypes();
+ final @InsetsSide int sidesToFit = attrs.getFitInsetsSides();
+ final ArraySet<Integer> types = InsetsState.toInternalType(typesToFit);
+ final Rect df = displayFrame;
+ Insets insets = Insets.of(0, 0, 0, 0);
+ for (int i = types.size() - 1; i >= 0; i--) {
+ final InsetsSource source = state.peekSource(types.valueAt(i));
+ if (source == null) {
+ continue;
+ }
+ insets = Insets.max(insets, source.calculateInsets(
+ df, attrs.isFitInsetsIgnoringVisibility()));
+ }
+ final int left = (sidesToFit & WindowInsets.Side.LEFT) != 0 ? insets.left : 0;
+ final int top = (sidesToFit & WindowInsets.Side.TOP) != 0 ? insets.top : 0;
+ final int right = (sidesToFit & WindowInsets.Side.RIGHT) != 0 ? insets.right : 0;
+ final int bottom = (sidesToFit & WindowInsets.Side.BOTTOM) != 0 ? insets.bottom : 0;
+ outBounds.set(df.left + left, df.top + top, df.right - right, df.bottom - bottom);
+ }
+
+ private Configuration getConfiguration() {
+ return mContext.getResources().getConfiguration();
+ }
+
+ /**
+ * We have one child
+ */
+ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
+ setView(view, attrs, panelParentView, UserHandle.myUserId());
+ }
+
+ /**
+ * We have one child
+ */
+ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
+ int userId) {
+ synchronized (this) {
+ if (mView == null) {
+ mView = view;
+
+ mAttachInfo.mDisplayState = mDisplay.getState();
+ mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+
+ mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
+ mFallbackEventHandler.setView(view);
+ mWindowAttributes.copyFrom(attrs);
+ if (mWindowAttributes.packageName == null) {
+ mWindowAttributes.packageName = mBasePackageName;
+ }
+ mWindowAttributes.privateFlags |=
+ WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST;
+
+ attrs = mWindowAttributes;
+ setTag();
+
+ if (DEBUG_KEEP_SCREEN_ON && (mClientWindowLayoutFlags
+ & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
+ && (attrs.flags&WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0) {
+ Slog.d(mTag, "setView: FLAG_KEEP_SCREEN_ON changed from true to false!");
+ }
+ // Keep track of the actual window flags supplied by the client.
+ mClientWindowLayoutFlags = attrs.flags;
+
+ setAccessibilityFocus(null, null);
+
+ if (view instanceof RootViewSurfaceTaker) {
+ mSurfaceHolderCallback =
+ ((RootViewSurfaceTaker)view).willYouTakeTheSurface();
+ if (mSurfaceHolderCallback != null) {
+ mSurfaceHolder = new TakenSurfaceHolder();
+ mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
+ mSurfaceHolder.addCallback(mSurfaceHolderCallback);
+ }
+ }
+
+ // Compute surface insets required to draw at specified Z value.
+ // TODO: Use real shadow insets for a constant max Z.
+ if (!attrs.hasManualSurfaceInsets) {
+ attrs.setSurfaceInsets(view, false /*manual*/, true /*preservePrevious*/);
+ }
+
+ CompatibilityInfo compatibilityInfo =
+ mDisplay.getDisplayAdjustments().getCompatibilityInfo();
+ mTranslator = compatibilityInfo.getTranslator();
+
+ // If the application owns the surface, don't enable hardware acceleration
+ if (mSurfaceHolder == null) {
+ // While this is supposed to enable only, it can effectively disable
+ // the acceleration too.
+ enableHardwareAcceleration(attrs);
+ final boolean useMTRenderer = MT_RENDERER_AVAILABLE
+ && mAttachInfo.mThreadedRenderer != null;
+ if (mUseMTRenderer != useMTRenderer) {
+ // Shouldn't be resizing, as it's done only in window setup,
+ // but end just in case.
+ endDragResizing();
+ mUseMTRenderer = useMTRenderer;
+ }
+ }
+
+ boolean restore = false;
+ if (mTranslator != null) {
+ mSurface.setCompatibilityTranslator(mTranslator);
+ restore = true;
+ attrs.backup();
+ mTranslator.translateWindowLayout(attrs);
+ }
+ if (DEBUG_LAYOUT) Log.d(mTag, "WindowLayout in setView:" + attrs);
+
+ if (!compatibilityInfo.supportsScreen()) {
+ attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
+ mLastInCompatMode = true;
+ }
+
+ mSoftInputMode = attrs.softInputMode;
+ mWindowAttributesChanged = true;
+ mAttachInfo.mRootView = view;
+ mAttachInfo.mScalingRequired = mTranslator != null;
+ mAttachInfo.mApplicationScale =
+ mTranslator == null ? 1.0f : mTranslator.applicationScale;
+ if (panelParentView != null) {
+ mAttachInfo.mPanelParentWindowToken
+ = panelParentView.getApplicationWindowToken();
+ }
+ mAdded = true;
+ int res; /* = WindowManagerImpl.ADD_OKAY; */
+
+ // Schedule the first layout -before- adding to the window
+ // manager, to make sure we do the relayout before receiving
+ // any other events from the system.
+ requestLayout();
+ InputChannel inputChannel = null;
+ if ((mWindowAttributes.inputFeatures
+ & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
+ inputChannel = new InputChannel();
+ }
+ mForceDecorViewVisibility = (mWindowAttributes.privateFlags
+ & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
+
+ if (mView instanceof RootViewSurfaceTaker) {
+ PendingInsetsController pendingInsetsController =
+ ((RootViewSurfaceTaker) mView).providePendingInsetsController();
+ if (pendingInsetsController != null) {
+ pendingInsetsController.replayAndAttach(mInsetsController);
+ }
+ }
+
+ try {
+ mOrigWindowType = mWindowAttributes.type;
+ mAttachInfo.mRecomputeGlobalAttributes = true;
+ collectViewAttributes();
+ adjustLayoutParamsForCompatibility(mWindowAttributes);
+ controlInsetsForCompatibility(mWindowAttributes);
+ res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
+ getHostVisibility(), mDisplay.getDisplayId(), userId,
+ mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets,
+ mTempControls);
+ if (mTranslator != null) {
+ mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
+ mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
+ }
+ } catch (RemoteException e) {
+ mAdded = false;
+ mView = null;
+ mAttachInfo.mRootView = null;
+ mFallbackEventHandler.setView(null);
+ unscheduleTraversals();
+ setAccessibilityFocus(null, null);
+ throw new RuntimeException("Adding window failed", e);
+ } finally {
+ if (restore) {
+ attrs.restore();
+ }
+ }
+
+ mAttachInfo.mAlwaysConsumeSystemBars =
+ (res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS) != 0;
+ mPendingAlwaysConsumeSystemBars = mAttachInfo.mAlwaysConsumeSystemBars;
+ mInsetsController.onStateChanged(mTempInsets);
+ mInsetsController.onControlsChanged(mTempControls);
+ computeWindowBounds(mWindowAttributes, mInsetsController.getState(),
+ getConfiguration().windowConfiguration.getBounds(), mTmpFrames.frame);
+ setFrame(mTmpFrames.frame);
+ if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
+ if (res < WindowManagerGlobal.ADD_OKAY) {
+ mAttachInfo.mRootView = null;
+ mAdded = false;
+ mFallbackEventHandler.setView(null);
+ unscheduleTraversals();
+ setAccessibilityFocus(null, null);
+ switch (res) {
+ case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
+ case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
+ throw new WindowManager.BadTokenException(
+ "Unable to add window -- token " + attrs.token
+ + " is not valid; is your activity running?");
+ case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
+ throw new WindowManager.BadTokenException(
+ "Unable to add window -- token " + attrs.token
+ + " is not for an application");
+ case WindowManagerGlobal.ADD_APP_EXITING:
+ throw new WindowManager.BadTokenException(
+ "Unable to add window -- app for token " + attrs.token
+ + " is exiting");
+ case WindowManagerGlobal.ADD_DUPLICATE_ADD:
+ throw new WindowManager.BadTokenException(
+ "Unable to add window -- window " + mWindow
+ + " has already been added");
+ case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
+ // Silently ignore -- we would have just removed it
+ // right away, anyway.
+ return;
+ case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
+ throw new WindowManager.BadTokenException("Unable to add window "
+ + mWindow + " -- another window of type "
+ + mWindowAttributes.type + " already exists");
+ case WindowManagerGlobal.ADD_PERMISSION_DENIED:
+ throw new WindowManager.BadTokenException("Unable to add window "
+ + mWindow + " -- permission denied for window type "
+ + mWindowAttributes.type);
+ case WindowManagerGlobal.ADD_INVALID_DISPLAY:
+ throw new WindowManager.InvalidDisplayException("Unable to add window "
+ + mWindow + " -- the specified display can not be found");
+ case WindowManagerGlobal.ADD_INVALID_TYPE:
+ throw new WindowManager.InvalidDisplayException("Unable to add window "
+ + mWindow + " -- the specified window type "
+ + mWindowAttributes.type + " is not valid");
+ case WindowManagerGlobal.ADD_INVALID_USER:
+ throw new WindowManager.BadTokenException("Unable to add Window "
+ + mWindow + " -- requested userId is not valid");
+ }
+ throw new RuntimeException(
+ "Unable to add window -- unknown error code " + res);
+ }
+
+ if ((res & WindowManagerGlobal.ADD_FLAG_USE_BLAST) != 0) {
+ mUseBLASTAdapter = true;
+ }
+
+ if (view instanceof RootViewSurfaceTaker) {
+ mInputQueueCallback =
+ ((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
+ }
+ if (inputChannel != null) {
+ if (mInputQueueCallback != null) {
+ mInputQueue = new InputQueue();
+ mInputQueueCallback.onInputQueueCreated(mInputQueue);
+ }
+ mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
+ Looper.myLooper());
+
+ if (ENABLE_INPUT_LATENCY_TRACKING && mAttachInfo.mThreadedRenderer != null) {
+ InputMetricsListener listener = new InputMetricsListener();
+ mHardwareRendererObserver = new HardwareRendererObserver(
+ listener, listener.data, mHandler, true /*waitForPresentTime*/);
+ mAttachInfo.mThreadedRenderer.addObserver(mHardwareRendererObserver);
+ }
+ }
+
+ view.assignParent(this);
+ mAddedTouchMode = (res & WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE) != 0;
+ mAppVisible = (res & WindowManagerGlobal.ADD_FLAG_APP_VISIBLE) != 0;
+
+ if (mAccessibilityManager.isEnabled()) {
+ mAccessibilityInteractionConnectionManager.ensureConnection();
+ }
+
+ if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ // Set up the input pipeline.
+ CharSequence counterSuffix = attrs.getTitle();
+ mSyntheticInputStage = new SyntheticInputStage();
+ InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
+ InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
+ "aq:native-post-ime:" + counterSuffix);
+ InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
+ InputStage imeStage = new ImeInputStage(earlyPostImeStage,
+ "aq:ime:" + counterSuffix);
+ InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
+ InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
+ "aq:native-pre-ime:" + counterSuffix);
+
+ mFirstInputStage = nativePreImeStage;
+ mFirstPostImeInputStage = earlyPostImeStage;
+ mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
+ }
+ }
+ }
+
+ private void setTag() {
+ final String[] split = mWindowAttributes.getTitle().toString().split("\\.");
+ if (split.length > 0) {
+ mTag = TAG + "[" + split[split.length - 1] + "]";
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getWindowFlags() {
+ return mWindowAttributes.flags;
+ }
+
+ public int getDisplayId() {
+ return mDisplay.getDisplayId();
+ }
+
+ public CharSequence getTitle() {
+ return mWindowAttributes.getTitle();
+ }
+
+ /**
+ * @return the width of the root view. Note that this will return {@code -1} until the first
+ * layout traversal, when the width is set.
+ *
+ * @hide
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * @return the height of the root view. Note that this will return {@code -1} until the first
+ * layout traversal, when the height is set.
+ *
+ * @hide
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Destroys hardware rendering resources for this ViewRootImpl
+ *
+ * May be called on any thread
+ */
+ @AnyThread
+ void destroyHardwareResources() {
+ final ThreadedRenderer renderer = mAttachInfo.mThreadedRenderer;
+ if (renderer != null) {
+ // This is called by WindowManagerGlobal which may or may not be on the right thread
+ if (Looper.myLooper() != mAttachInfo.mHandler.getLooper()) {
+ mAttachInfo.mHandler.postAtFrontOfQueue(this::destroyHardwareResources);
+ return;
+ }
+ renderer.destroyHardwareResources(mView);
+ renderer.destroy();
+ }
+ }
+
+ /**
+ * Does nothing; Here only because of @UnsupportedAppUsage
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R,
+ publicAlternatives = "Use {@link android.webkit.WebView} instead")
+ public void detachFunctor(long functor) { }
+
+ /**
+ * Does nothing; Here only because of @UnsupportedAppUsage
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R,
+ publicAlternatives = "Use {@link android.webkit.WebView} instead")
+ public static void invokeFunctor(long functor, boolean waitForCompletion) { }
+
+ /**
+ * @param animator animator to register with the hardware renderer
+ */
+ public void registerAnimatingRenderNode(RenderNode animator) {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.registerAnimatingRenderNode(animator);
+ } else {
+ if (mAttachInfo.mPendingAnimatingRenderNodes == null) {
+ mAttachInfo.mPendingAnimatingRenderNodes = new ArrayList<RenderNode>();
+ }
+ mAttachInfo.mPendingAnimatingRenderNodes.add(animator);
+ }
+ }
+
+ /**
+ * @param animator animator to register with the hardware renderer
+ */
+ public void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator) {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.registerVectorDrawableAnimator(animator);
+ }
+ }
+
+ /**
+ * Registers a callback to be executed when the next frame is being drawn on RenderThread. This
+ * callback will be executed on a RenderThread worker thread, and only used for the next frame
+ * and thus it will only fire once.
+ *
+ * @param callback The callback to register.
+ */
+ public void registerRtFrameCallback(@NonNull FrameDrawingCallback callback) {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.registerRtFrameCallback(frame -> {
+ try {
+ callback.onFrameDraw(frame);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while executing onFrameDraw", e);
+ }
+ });
+ }
+ }
+
+ /**
+ * Register a callback to be executed when Webview overlay needs to merge a transaction.
+ * This callback will be executed on RenderThread worker thread, and released inside native code
+ * when CanvasContext is destroyed.
+ */
+ private void addASurfaceTransactionCallback() {
+ HardwareRenderer.ASurfaceTransactionCallback callback = (nativeTransactionObj,
+ nativeSurfaceControlObj,
+ frameNr) -> {
+ if (mBlastBufferQueue == null) {
+ return false;
+ } else {
+ mBlastBufferQueue.mergeWithNextTransaction(nativeTransactionObj, frameNr);
+ return true;
+ }
+ };
+ mAttachInfo.mThreadedRenderer.setASurfaceTransactionCallback(callback);
+ }
+
+ /**
+ * Register a callback to be executed when Webview overlay needs a surface control.
+ * This callback will be executed on RenderThread worker thread, and released inside native code
+ * when CanvasContext is destroyed.
+ */
+ private void addPrepareSurfaceControlForWebviewCallback() {
+ HardwareRenderer.PrepareSurfaceControlForWebviewCallback callback = () -> {
+ // make mSurfaceControl transparent, so child surface controls are visible
+ if (mIsForWebviewOverlay) return;
+ synchronized (ViewRootImpl.this) {
+ mIsForWebviewOverlay = true;
+ }
+ mTransaction.setOpaque(mSurfaceControl, false).apply();
+ };
+ mAttachInfo.mThreadedRenderer.setPrepareSurfaceControlForWebviewCallback(callback);
+ }
+
+ @UnsupportedAppUsage
+ private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {
+ mAttachInfo.mHardwareAccelerated = false;
+ mAttachInfo.mHardwareAccelerationRequested = false;
+
+ // Don't enable hardware acceleration when the application is in compatibility mode
+ if (mTranslator != null) return;
+
+ // Try to enable hardware acceleration if requested
+ final boolean hardwareAccelerated =
+ (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
+
+ if (hardwareAccelerated) {
+ // Persistent processes (including the system) should not do
+ // accelerated rendering on low-end devices. In that case,
+ // sRendererDisabled will be set. In addition, the system process
+ // itself should never do accelerated rendering. In that case, both
+ // sRendererDisabled and sSystemRendererDisabled are set. When
+ // sSystemRendererDisabled is set, PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED
+ // can be used by code on the system process to escape that and enable
+ // HW accelerated drawing. (This is basically for the lock screen.)
+
+ final boolean forceHwAccelerated = (attrs.privateFlags &
+ WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED) != 0;
+
+ if (ThreadedRenderer.sRendererEnabled || forceHwAccelerated) {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.destroy();
+ }
+
+ final Rect insets = attrs.surfaceInsets;
+ final boolean hasSurfaceInsets = insets.left != 0 || insets.right != 0
+ || insets.top != 0 || insets.bottom != 0;
+ final boolean translucent = attrs.format != PixelFormat.OPAQUE || hasSurfaceInsets;
+ mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
+ attrs.getTitle().toString());
+ updateColorModeIfNeeded(attrs.getColorMode());
+ updateForceDarkMode();
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mHardwareAccelerated =
+ mAttachInfo.mHardwareAccelerationRequested = true;
+ if (mHardwareRendererObserver != null) {
+ mAttachInfo.mThreadedRenderer.addObserver(mHardwareRendererObserver);
+ }
+ if (HardwareRenderer.isWebViewOverlaysEnabled()) {
+ addPrepareSurfaceControlForWebviewCallback();
+ addASurfaceTransactionCallback();
+ }
+ mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
+ }
+ }
+ }
+ }
+
+ private int getNightMode() {
+ return getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ }
+
+ private void updateForceDarkMode() {
+ if (mAttachInfo.mThreadedRenderer == null) return;
+
+ boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
+
+ if (useAutoDark) {
+ boolean forceDarkAllowedDefault =
+ SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
+ TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
+ useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
+ && a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
+ a.recycle();
+ }
+
+ if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) {
+ // TODO: Don't require regenerating all display lists to apply this setting
+ invalidateWorld(mView);
+ }
+ }
+
+ @UnsupportedAppUsage
+ public View getView() {
+ return mView;
+ }
+
+ final WindowLeaked getLocation() {
+ return mLocation;
+ }
+
+ @VisibleForTesting
+ public void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
+ synchronized (this) {
+ final int oldInsetLeft = mWindowAttributes.surfaceInsets.left;
+ final int oldInsetTop = mWindowAttributes.surfaceInsets.top;
+ final int oldInsetRight = mWindowAttributes.surfaceInsets.right;
+ final int oldInsetBottom = mWindowAttributes.surfaceInsets.bottom;
+ final int oldSoftInputMode = mWindowAttributes.softInputMode;
+ final boolean oldHasManualSurfaceInsets = mWindowAttributes.hasManualSurfaceInsets;
+
+ if (DEBUG_KEEP_SCREEN_ON && (mClientWindowLayoutFlags
+ & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
+ && (attrs.flags&WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0) {
+ Slog.d(mTag, "setLayoutParams: FLAG_KEEP_SCREEN_ON from true to false!");
+ }
+
+ // Keep track of the actual window flags supplied by the client.
+ mClientWindowLayoutFlags = attrs.flags;
+
+ // Preserve compatible window flag if exists.
+ final int compatibleWindowFlag = mWindowAttributes.privateFlags
+ & WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
+
+ // Preserve system UI visibility.
+ final int systemUiVisibility = mWindowAttributes.systemUiVisibility;
+ final int subtreeSystemUiVisibility = mWindowAttributes.subtreeSystemUiVisibility;
+
+ // Preserve appearance and behavior.
+ final int appearance = mWindowAttributes.insetsFlags.appearance;
+ final int behavior = mWindowAttributes.insetsFlags.behavior;
+ final int appearanceAndBehaviorPrivateFlags = mWindowAttributes.privateFlags
+ & (PRIVATE_FLAG_APPEARANCE_CONTROLLED | PRIVATE_FLAG_BEHAVIOR_CONTROLLED);
+
+ final int changes = mWindowAttributes.copyFrom(attrs);
+ if ((changes & WindowManager.LayoutParams.TRANSLUCENT_FLAGS_CHANGED) != 0) {
+ // Recompute system ui visibility.
+ mAttachInfo.mRecomputeGlobalAttributes = true;
+ }
+ if ((changes & WindowManager.LayoutParams.LAYOUT_CHANGED) != 0) {
+ // Request to update light center.
+ mAttachInfo.mNeedsUpdateLightCenter = true;
+ }
+ if (mWindowAttributes.packageName == null) {
+ mWindowAttributes.packageName = mBasePackageName;
+ }
+
+ // Restore preserved flags.
+ mWindowAttributes.systemUiVisibility = systemUiVisibility;
+ mWindowAttributes.subtreeSystemUiVisibility = subtreeSystemUiVisibility;
+ mWindowAttributes.insetsFlags.appearance = appearance;
+ mWindowAttributes.insetsFlags.behavior = behavior;
+ mWindowAttributes.privateFlags |= compatibleWindowFlag
+ | appearanceAndBehaviorPrivateFlags
+ | WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST;
+
+ if (mWindowAttributes.preservePreviousSurfaceInsets) {
+ // Restore old surface insets.
+ mWindowAttributes.surfaceInsets.set(
+ oldInsetLeft, oldInsetTop, oldInsetRight, oldInsetBottom);
+ mWindowAttributes.hasManualSurfaceInsets = oldHasManualSurfaceInsets;
+ } else if (mWindowAttributes.surfaceInsets.left != oldInsetLeft
+ || mWindowAttributes.surfaceInsets.top != oldInsetTop
+ || mWindowAttributes.surfaceInsets.right != oldInsetRight
+ || mWindowAttributes.surfaceInsets.bottom != oldInsetBottom) {
+ mNeedsRendererSetup = true;
+ }
+
+ applyKeepScreenOnFlag(mWindowAttributes);
+
+ if (newView) {
+ mSoftInputMode = attrs.softInputMode;
+ requestLayout();
+ }
+
+ // Don't lose the mode we last auto-computed.
+ if ((attrs.softInputMode & SOFT_INPUT_MASK_ADJUST)
+ == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) {
+ mWindowAttributes.softInputMode = (mWindowAttributes.softInputMode
+ & ~SOFT_INPUT_MASK_ADJUST) | (oldSoftInputMode & SOFT_INPUT_MASK_ADJUST);
+ }
+
+ if (mWindowAttributes.softInputMode != oldSoftInputMode) {
+ requestFitSystemWindows();
+ }
+
+ mWindowAttributesChanged = true;
+ scheduleTraversals();
+ }
+ }
+
+ void handleAppVisibility(boolean visible) {
+ if (mAppVisible != visible) {
+ mAppVisible = visible;
+ mAppVisibilityChanged = true;
+ scheduleTraversals();
+ if (!mAppVisible) {
+ WindowManagerGlobal.trimForeground();
+ }
+ }
+ }
+
+ void handleGetNewSurface() {
+ mNewSurfaceNeeded = true;
+ mFullRedrawNeeded = true;
+ scheduleTraversals();
+ }
+
+ /** Handles messages {@link #MSG_RESIZED} and {@link #MSG_RESIZED_REPORT}. */
+ private void handleResized(int msg, SomeArgs args) {
+ if (!mAdded) {
+ return;
+ }
+
+ final ClientWindowFrames frames = (ClientWindowFrames) args.arg1;
+ final MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg2;
+ final boolean forceNextWindowRelayout = args.argi1 != 0;
+ final int displayId = args.argi3;
+ final Rect backdropFrame = frames.backdropFrame;
+
+ final boolean frameChanged = !mWinFrame.equals(frames.frame);
+ final boolean backdropFrameChanged = !mPendingBackDropFrame.equals(backdropFrame);
+ final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
+ final boolean displayChanged = mDisplay.getDisplayId() != displayId;
+ if (msg == MSG_RESIZED && !frameChanged && !backdropFrameChanged && !configChanged
+ && !displayChanged && !forceNextWindowRelayout) {
+ return;
+ }
+
+ if (configChanged) {
+ // If configuration changed - notify about that and, maybe, about move to display.
+ performConfigurationChange(mergedConfiguration, false /* force */,
+ displayChanged ? displayId : INVALID_DISPLAY /* same display */);
+ } else if (displayChanged) {
+ // Moved to display without config change - report last applied one.
+ onMovedToDisplay(displayId, mLastConfigurationFromResources);
+ }
+
+ setFrame(frames.frame);
+ mTmpFrames.displayFrame.set(frames.displayFrame);
+ mPendingBackDropFrame.set(backdropFrame);
+ mForceNextWindowRelayout = forceNextWindowRelayout;
+ mPendingAlwaysConsumeSystemBars = args.argi2 != 0;
+
+ if (msg == MSG_RESIZED_REPORT && !mNextDrawUseBlastSync) {
+ reportNextDraw();
+ }
+
+ if (mView != null && (frameChanged || configChanged)) {
+ forceLayout(mView);
+ }
+ requestLayout();
+ }
+
+ private final DisplayListener mDisplayListener = new DisplayListener() {
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (mView != null && mDisplay.getDisplayId() == displayId) {
+ final int oldDisplayState = mAttachInfo.mDisplayState;
+ final int newDisplayState = mDisplay.getState();
+ if (oldDisplayState != newDisplayState) {
+ mAttachInfo.mDisplayState = newDisplayState;
+ pokeDrawLockIfNeeded();
+ if (oldDisplayState != Display.STATE_UNKNOWN) {
+ final int oldScreenState = toViewScreenState(oldDisplayState);
+ final int newScreenState = toViewScreenState(newDisplayState);
+ if (oldScreenState != newScreenState) {
+ mView.dispatchScreenStateChanged(newScreenState);
+ }
+ if (oldDisplayState == Display.STATE_OFF) {
+ // Draw was suppressed so we need to for it to happen here.
+ mFullRedrawNeeded = true;
+ scheduleTraversals();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+
+ private int toViewScreenState(int displayState) {
+ return displayState == Display.STATE_OFF ?
+ View.SCREEN_STATE_OFF : View.SCREEN_STATE_ON;
+ }
+ };
+
+ /**
+ * Notify about move to a different display.
+ * @param displayId The id of the display where this view root is moved to.
+ * @param config Configuration of the resources on new display after move.
+ *
+ * @hide
+ */
+ public void onMovedToDisplay(int displayId, Configuration config) {
+ if (mDisplay.getDisplayId() == displayId) {
+ return;
+ }
+
+ // Get new instance of display based on current display adjustments. It may be updated later
+ // if moving between the displays also involved a configuration change.
+ updateInternalDisplay(displayId, mView.getResources());
+ mImeFocusController.onMovedToDisplay();
+ mAttachInfo.mDisplayState = mDisplay.getState();
+ // Internal state updated, now notify the view hierarchy.
+ mView.dispatchMovedToDisplay(mDisplay, config);
+ }
+
+ /**
+ * Updates {@link #mDisplay} to the display object corresponding to {@param displayId}.
+ * Uses DEFAULT_DISPLAY if there isn't a display object in the system corresponding
+ * to {@param displayId}.
+ */
+ private void updateInternalDisplay(int displayId, Resources resources) {
+ final Display preferredDisplay =
+ ResourcesManager.getInstance().getAdjustedDisplay(displayId, resources);
+ if (preferredDisplay == null) {
+ // Fallback to use default display.
+ Slog.w(TAG, "Cannot get desired display with Id: " + displayId);
+ mDisplay = ResourcesManager.getInstance()
+ .getAdjustedDisplay(DEFAULT_DISPLAY, resources);
+ } else {
+ mDisplay = preferredDisplay;
+ }
+ mContext.updateDisplay(mDisplay.getDisplayId());
+ }
+
+ void pokeDrawLockIfNeeded() {
+ final int displayState = mAttachInfo.mDisplayState;
+ if (mView != null && mAdded && mTraversalScheduled
+ && (displayState == Display.STATE_DOZE
+ || displayState == Display.STATE_DOZE_SUSPEND)) {
+ try {
+ mWindowSession.pokeDrawLock(mWindow);
+ } catch (RemoteException ex) {
+ // System server died, oh well.
+ }
+ }
+ }
+
+ @Override
+ public void requestFitSystemWindows() {
+ checkThread();
+ mApplyInsetsRequested = true;
+ scheduleTraversals();
+ }
+
+ void notifyInsetsChanged() {
+ mApplyInsetsRequested = true;
+ if (mWillMove || mWillResize) {
+ // The window frame will be changed soon. The following logic will be executed then.
+ return;
+ }
+ requestLayout();
+
+ // See comment for View.sForceLayoutWhenInsetsChanged
+ if (View.sForceLayoutWhenInsetsChanged && mView != null
+ && (mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST)
+ == SOFT_INPUT_ADJUST_RESIZE) {
+ forceLayout(mView);
+ }
+
+ // If this changes during traversal, no need to schedule another one as it will dispatch it
+ // during the current traversal.
+ if (!mIsInTraversal) {
+ scheduleTraversals();
+ }
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mHandlingLayoutInLayoutRequest) {
+ checkThread();
+ mLayoutRequested = true;
+ scheduleTraversals();
+ }
+ }
+
+ @Override
+ public boolean isLayoutRequested() {
+ return mLayoutRequested;
+ }
+
+ @Override
+ public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
+ // TODO: Re-enable after camera is fixed or consider targetSdk checking this
+ // checkThread();
+ if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
+ mIsAnimating = true;
+ }
+ invalidate();
+ }
+
+ @UnsupportedAppUsage
+ void invalidate() {
+ mDirty.set(0, 0, mWidth, mHeight);
+ if (!mWillDrawSoon) {
+ scheduleTraversals();
+ }
+ }
+
+ void invalidateWorld(View view) {
+ view.invalidate();
+ if (view instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) view;
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ invalidateWorld(parent.getChildAt(i));
+ }
+ }
+ }
+
+ @Override
+ public void invalidateChild(View child, Rect dirty) {
+ invalidateChildInParent(null, dirty);
+ }
+
+ @Override
+ public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
+ checkThread();
+ if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
+
+ if (dirty == null) {
+ invalidate();
+ return null;
+ } else if (dirty.isEmpty() && !mIsAnimating) {
+ return null;
+ }
+
+ if (mCurScrollY != 0 || mTranslator != null) {
+ mTempRect.set(dirty);
+ dirty = mTempRect;
+ if (mCurScrollY != 0) {
+ dirty.offset(0, -mCurScrollY);
+ }
+ if (mTranslator != null) {
+ mTranslator.translateRectInAppWindowToScreen(dirty);
+ }
+ if (mAttachInfo.mScalingRequired) {
+ dirty.inset(-1, -1);
+ }
+ }
+
+ invalidateRectOnScreen(dirty);
+
+ return null;
+ }
+
+ private void invalidateRectOnScreen(Rect dirty) {
+ final Rect localDirty = mDirty;
+
+ // Add the new dirty rect to the current one
+ localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
+ // Intersect with the bounds of the window to skip
+ // updates that lie outside of the visible region
+ final float appScale = mAttachInfo.mApplicationScale;
+ final boolean intersected = localDirty.intersect(0, 0,
+ (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
+ if (!intersected) {
+ localDirty.setEmpty();
+ }
+ if (!mWillDrawSoon && (intersected || mIsAnimating)) {
+ scheduleTraversals();
+ }
+ }
+
+ public void setIsAmbientMode(boolean ambient) {
+ mIsAmbientMode = ambient;
+ }
+
+ void setWindowStopped(boolean stopped) {
+ checkThread();
+ if (mStopped != stopped) {
+ mStopped = stopped;
+ final ThreadedRenderer renderer = mAttachInfo.mThreadedRenderer;
+ if (renderer != null) {
+ if (DEBUG_DRAW) Log.d(mTag, "WindowStopped on " + getTitle() + " set to " + mStopped);
+ renderer.setStopped(mStopped);
+ }
+ if (!mStopped) {
+ mNewSurfaceNeeded = true;
+ scheduleTraversals();
+ } else {
+ if (renderer != null) {
+ renderer.destroyHardwareResources(mView);
+ }
+
+ if (mSurface.isValid()) {
+ if (mSurfaceHolder != null) {
+ notifyHolderSurfaceDestroyed();
+ }
+ notifySurfaceDestroyed();
+ }
+ destroySurface();
+ }
+ }
+ }
+
+
+ /** Register callbacks to be notified when the ViewRootImpl surface changes. */
+ public interface SurfaceChangedCallback {
+ void surfaceCreated(Transaction t);
+ void surfaceReplaced(Transaction t);
+ void surfaceDestroyed();
+ }
+
+ private final ArrayList<SurfaceChangedCallback> mSurfaceChangedCallbacks = new ArrayList<>();
+ public void addSurfaceChangedCallback(SurfaceChangedCallback c) {
+ mSurfaceChangedCallbacks.add(c);
+ }
+
+ public void removeSurfaceChangedCallback(SurfaceChangedCallback c) {
+ mSurfaceChangedCallbacks.remove(c);
+ }
+
+ private void notifySurfaceCreated() {
+ for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) {
+ mSurfaceChangedCallbacks.get(i).surfaceCreated(mSurfaceChangedTransaction);
+ }
+ }
+
+ /**
+ * Notify listeners when the ViewRootImpl surface has been replaced. This callback will not be
+ * called if a new surface is created, only if the valid surface has been replaced with another
+ * valid surface.
+ */
+ private void notifySurfaceReplaced() {
+ for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) {
+ mSurfaceChangedCallbacks.get(i).surfaceReplaced(mSurfaceChangedTransaction);
+ }
+ }
+
+ private void notifySurfaceDestroyed() {
+ for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) {
+ mSurfaceChangedCallbacks.get(i).surfaceDestroyed();
+ }
+ }
+
+ /**
+ * @return child layer with the same bounds as its parent {@code mSurface} and cropped to the
+ * surface insets. If the layer does not exist, it is created.
+ *
+ * <p>Parenting to this layer will ensure that its children are cropped by the view's surface
+ * insets.
+ */
+ public SurfaceControl getBoundsLayer() {
+ if (mBoundsLayer == null) {
+ mBoundsLayer = new SurfaceControl.Builder(mSurfaceSession)
+ .setContainerLayer()
+ .setName("Bounds for - " + getTitle().toString())
+ .setParent(getSurfaceControl())
+ .setCallsite("ViewRootImpl.getBoundsLayer")
+ .build();
+ setBoundsLayerCrop(mTransaction);
+ mTransaction.show(mBoundsLayer).apply();
+ }
+ return mBoundsLayer;
+ }
+
+ Surface getOrCreateBLASTSurface() {
+ if (!mSurfaceControl.isValid()) {
+ return null;
+ }
+
+ Surface ret = null;
+ if (mBlastBufferQueue == null) {
+ mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
+ mSurfaceSize.x, mSurfaceSize.y,
+ mWindowAttributes.format);
+ // We only return the Surface the first time, as otherwise
+ // it hasn't changed and there is no need to update.
+ ret = mBlastBufferQueue.createSurface();
+ } else {
+ mBlastBufferQueue.update(mSurfaceControl,
+ mSurfaceSize.x, mSurfaceSize.y,
+ mWindowAttributes.format);
+ }
+
+ return ret;
+ }
+
+ private void setBoundsLayerCrop(Transaction t) {
+ // Adjust of insets and update the bounds layer so child surfaces do not draw into
+ // the surface inset region.
+ mTempBoundsRect.set(0, 0, mSurfaceSize.x, mSurfaceSize.y);
+ mTempBoundsRect.inset(mWindowAttributes.surfaceInsets.left,
+ mWindowAttributes.surfaceInsets.top,
+ mWindowAttributes.surfaceInsets.right, mWindowAttributes.surfaceInsets.bottom);
+ t.setWindowCrop(mBoundsLayer, mTempBoundsRect);
+ }
+
+ /**
+ * Called after window layout to update the bounds surface. If the surface insets have changed
+ * or the surface has resized, update the bounds surface.
+ */
+ private boolean updateBoundsLayer(SurfaceControl.Transaction t) {
+ if (mBoundsLayer != null) {
+ setBoundsLayerCrop(t);
+ return true;
+ }
+ return false;
+ }
+
+ private void prepareSurfaces() {
+ final SurfaceControl.Transaction t = mTransaction;
+ final SurfaceControl sc = getSurfaceControl();
+ if (!sc.isValid()) return;
+
+ if (updateBoundsLayer(t)) {
+ mergeWithNextTransaction(t, mSurface.getNextFrameNumber());
+ }
+ }
+
+ private void destroySurface() {
+ if (mBoundsLayer != null) {
+ mBoundsLayer.release();
+ mBoundsLayer = null;
+ }
+ mSurface.release();
+ mSurfaceControl.release();
+
+ if (mBlastBufferQueue != null) {
+ mBlastBufferQueue.destroy();
+ mBlastBufferQueue = null;
+ }
+
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.setSurfaceControl(null);
+ }
+ }
+
+ /**
+ * Block the input events during an Activity Transition. The KEYCODE_BACK event is allowed
+ * through to allow quick reversal of the Activity Transition.
+ *
+ * @param paused true to pause, false to resume.
+ */
+ public void setPausedForTransition(boolean paused) {
+ mPausedForTransition = paused;
+ }
+
+ @Override
+ public ViewParent getParent() {
+ return null;
+ }
+
+ @Override
+ public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
+ if (child != mView) {
+ throw new RuntimeException("child is not mine, honest!");
+ }
+ // Note: don't apply scroll offset, because we want to know its
+ // visibility in the virtual canvas being given to the view hierarchy.
+ return r.intersect(0, 0, mWidth, mHeight);
+ }
+
+ @Override
+ public void bringChildToFront(View child) {
+ }
+
+ int getHostVisibility() {
+ return (mAppVisible || mForceDecorViewVisibility) ? mView.getVisibility() : View.GONE;
+ }
+
+ /**
+ * Add LayoutTransition to the list of transitions to be started in the next traversal.
+ * This list will be cleared after the transitions on the list are start()'ed. These
+ * transitionsa re added by LayoutTransition itself when it sets up animations. The setup
+ * happens during the layout phase of traversal, which we want to complete before any of the
+ * animations are started (because those animations may side-effect properties that layout
+ * depends upon, like the bounding rectangles of the affected views). So we add the transition
+ * to the list and it is started just prior to starting the drawing phase of traversal.
+ *
+ * @param transition The LayoutTransition to be started on the next traversal.
+ *
+ * @hide
+ */
+ public void requestTransitionStart(LayoutTransition transition) {
+ if (mPendingTransitions == null || !mPendingTransitions.contains(transition)) {
+ if (mPendingTransitions == null) {
+ mPendingTransitions = new ArrayList<LayoutTransition>();
+ }
+ mPendingTransitions.add(transition);
+ }
+ }
+
+ /**
+ * Notifies the HardwareRenderer that a new frame will be coming soon.
+ * Currently only {@link ThreadedRenderer} cares about this, and uses
+ * this knowledge to adjust the scheduling of off-thread animations
+ */
+ void notifyRendererOfFramePending() {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.notifyFramePending();
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ void scheduleTraversals() {
+ if (!mTraversalScheduled) {
+ mTraversalScheduled = true;
+ mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
+ mChoreographer.postCallback(
+ Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
+ notifyRendererOfFramePending();
+ pokeDrawLockIfNeeded();
+ }
+ }
+
+ void unscheduleTraversals() {
+ if (mTraversalScheduled) {
+ mTraversalScheduled = false;
+ mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
+ mChoreographer.removeCallbacks(
+ Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
+ }
+ }
+
+ void doTraversal() {
+ if (mTraversalScheduled) {
+ mTraversalScheduled = false;
+ mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
+
+ if (mProfile) {
+ Debug.startMethodTracing("ViewAncestor");
+ }
+
+ performTraversals();
+
+ if (mProfile) {
+ Debug.stopMethodTracing();
+ mProfile = false;
+ }
+ }
+ }
+
+ private void applyKeepScreenOnFlag(WindowManager.LayoutParams params) {
+ // Update window's global keep screen on flag: if a view has requested
+ // that the screen be kept on, then it is always set; otherwise, it is
+ // set to whatever the client last requested for the global state.
+ if (mAttachInfo.mKeepScreenOn) {
+ params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ } else {
+ params.flags = (params.flags&~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ | (mClientWindowLayoutFlags&WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ private boolean collectViewAttributes() {
+ if (mAttachInfo.mRecomputeGlobalAttributes) {
+ //Log.i(mTag, "Computing view hierarchy attributes!");
+ mAttachInfo.mRecomputeGlobalAttributes = false;
+ boolean oldScreenOn = mAttachInfo.mKeepScreenOn;
+ mAttachInfo.mKeepScreenOn = false;
+ mAttachInfo.mSystemUiVisibility = 0;
+ mAttachInfo.mHasSystemUiListeners = false;
+ mView.dispatchCollectViewAttributes(mAttachInfo, 0);
+ mAttachInfo.mSystemUiVisibility &= ~mAttachInfo.mDisabledSystemUiVisibility;
+ WindowManager.LayoutParams params = mWindowAttributes;
+ mAttachInfo.mSystemUiVisibility |= getImpliedSystemUiVisibility(params);
+ mCompatibleVisibilityInfo.globalVisibility =
+ (mCompatibleVisibilityInfo.globalVisibility & ~View.SYSTEM_UI_FLAG_LOW_PROFILE)
+ | (mAttachInfo.mSystemUiVisibility & View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ dispatchDispatchSystemUiVisibilityChanged(mCompatibleVisibilityInfo);
+ if (mAttachInfo.mKeepScreenOn != oldScreenOn
+ || mAttachInfo.mSystemUiVisibility != params.subtreeSystemUiVisibility
+ || mAttachInfo.mHasSystemUiListeners != params.hasSystemUiListeners) {
+ applyKeepScreenOnFlag(params);
+ params.subtreeSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
+ params.hasSystemUiListeners = mAttachInfo.mHasSystemUiListeners;
+ mView.dispatchWindowSystemUiVisiblityChanged(mAttachInfo.mSystemUiVisibility);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int getImpliedSystemUiVisibility(WindowManager.LayoutParams params) {
+ int vis = 0;
+ // Translucent decor window flags imply stable system ui visibility.
+ if ((params.flags & FLAG_TRANSLUCENT_STATUS) != 0) {
+ vis |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ }
+ if ((params.flags & FLAG_TRANSLUCENT_NAVIGATION) != 0) {
+ vis |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+ }
+ return vis;
+ }
+
+ /**
+ * Update the compatible system UI visibility for dispatching it to the legacy app.
+ *
+ * @param type Indicates which type of the insets source we are handling.
+ * @param visible True if the insets source is visible.
+ * @param hasControl True if we can control the insets source.
+ */
+ void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible,
+ boolean hasControl) {
+ if (type != ITYPE_STATUS_BAR && type != ITYPE_NAVIGATION_BAR) {
+ return;
+ }
+ final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo;
+ final int systemUiFlag = type == ITYPE_STATUS_BAR
+ ? View.SYSTEM_UI_FLAG_FULLSCREEN
+ : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ final boolean wasVisible = (info.globalVisibility & systemUiFlag) == 0;
+ if (visible) {
+ info.globalVisibility &= ~systemUiFlag;
+ if (!wasVisible && hasControl) {
+ // The local system UI visibility can only be cleared while we have the control.
+ info.localChanges |= systemUiFlag;
+ }
+ } else {
+ info.globalVisibility |= systemUiFlag;
+ info.localChanges &= ~systemUiFlag;
+ }
+ dispatchDispatchSystemUiVisibilityChanged(info);
+ }
+
+ /**
+ * If the system is forcing showing any system bar, the legacy low profile flag should be
+ * cleared for compatibility.
+ *
+ * @param showTypes {@link InsetsType types} shown by the system.
+ * @param fromIme {@code true} if the invocation is from IME.
+ */
+ private void clearLowProfileModeIfNeeded(@InsetsType int showTypes, boolean fromIme) {
+ final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo;
+ if ((showTypes & Type.systemBars()) != 0 && !fromIme
+ && (info.globalVisibility & SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+ info.globalVisibility &= ~SYSTEM_UI_FLAG_LOW_PROFILE;
+ info.localChanges |= SYSTEM_UI_FLAG_LOW_PROFILE;
+ dispatchDispatchSystemUiVisibilityChanged(info);
+ }
+ }
+
+ private void dispatchDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) {
+ if (mDispatchedSystemUiVisibility != args.globalVisibility) {
+ mHandler.removeMessages(MSG_DISPATCH_SYSTEM_UI_VISIBILITY);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY, args));
+ }
+ }
+
+ private void handleDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) {
+ if (mView == null) return;
+ if (args.localChanges != 0) {
+ mView.updateLocalSystemUiVisibility(args.localValue, args.localChanges);
+ args.localChanges = 0;
+ }
+
+ final int visibility = args.globalVisibility & View.SYSTEM_UI_CLEARABLE_FLAGS;
+ if (mDispatchedSystemUiVisibility != visibility) {
+ mDispatchedSystemUiVisibility = visibility;
+ mView.dispatchSystemUiVisibilityChanged(visibility);
+ }
+ }
+
+ @VisibleForTesting
+ public static void adjustLayoutParamsForCompatibility(WindowManager.LayoutParams inOutParams) {
+ final int sysUiVis = inOutParams.systemUiVisibility | inOutParams.subtreeSystemUiVisibility;
+ final int flags = inOutParams.flags;
+ final int type = inOutParams.type;
+ final int adjust = inOutParams.softInputMode & SOFT_INPUT_MASK_ADJUST;
+
+ if ((inOutParams.privateFlags & PRIVATE_FLAG_APPEARANCE_CONTROLLED) == 0) {
+ inOutParams.insetsFlags.appearance = 0;
+ if ((sysUiVis & SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+ inOutParams.insetsFlags.appearance |= APPEARANCE_LOW_PROFILE_BARS;
+ }
+ if ((sysUiVis & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0) {
+ inOutParams.insetsFlags.appearance |= APPEARANCE_LIGHT_STATUS_BARS;
+ }
+ if ((sysUiVis & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0) {
+ inOutParams.insetsFlags.appearance |= APPEARANCE_LIGHT_NAVIGATION_BARS;
+ }
+ }
+
+ if ((inOutParams.privateFlags & PRIVATE_FLAG_BEHAVIOR_CONTROLLED) == 0) {
+ if ((sysUiVis & SYSTEM_UI_FLAG_IMMERSIVE_STICKY) != 0
+ || (flags & FLAG_FULLSCREEN) != 0) {
+ inOutParams.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+ } else {
+ inOutParams.insetsFlags.behavior = BEHAVIOR_DEFAULT;
+ }
+ }
+
+ inOutParams.privateFlags &= ~PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
+
+ if ((inOutParams.privateFlags & PRIVATE_FLAG_FIT_INSETS_CONTROLLED) != 0) {
+ return;
+ }
+
+ int types = inOutParams.getFitInsetsTypes();
+ boolean ignoreVis = inOutParams.isFitInsetsIgnoringVisibility();
+
+ if (((sysUiVis & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0
+ || (flags & FLAG_LAYOUT_IN_SCREEN) != 0)
+ || (flags & FLAG_TRANSLUCENT_STATUS) != 0) {
+ types &= ~Type.statusBars();
+ }
+ if ((sysUiVis & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0
+ || (flags & FLAG_TRANSLUCENT_NAVIGATION) != 0) {
+ types &= ~Type.systemBars();
+ }
+ if (type == TYPE_TOAST || type == TYPE_SYSTEM_ALERT) {
+ ignoreVis = true;
+ } else if ((types & Type.systemBars()) == Type.systemBars()) {
+ if (adjust == SOFT_INPUT_ADJUST_RESIZE) {
+ types |= Type.ime();
+ } else {
+ inOutParams.privateFlags |= PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
+ }
+ }
+ inOutParams.setFitInsetsTypes(types);
+ inOutParams.setFitInsetsIgnoringVisibility(ignoreVis);
+
+ // The fitting of insets are not really controlled by the clients, so we remove the flag.
+ inOutParams.privateFlags &= ~PRIVATE_FLAG_FIT_INSETS_CONTROLLED;
+ }
+
+ private void controlInsetsForCompatibility(WindowManager.LayoutParams params) {
+ final int sysUiVis = params.systemUiVisibility | params.subtreeSystemUiVisibility;
+ final int flags = params.flags;
+ final boolean matchParent = params.width == MATCH_PARENT && params.height == MATCH_PARENT;
+ final boolean nonAttachedAppWindow = params.type >= FIRST_APPLICATION_WINDOW
+ && params.type <= LAST_APPLICATION_WINDOW;
+ final boolean statusWasHiddenByFlags = (mTypesHiddenByFlags & Type.statusBars()) != 0;
+ final boolean statusIsHiddenByFlags = (sysUiVis & SYSTEM_UI_FLAG_FULLSCREEN) != 0
+ || ((flags & FLAG_FULLSCREEN) != 0 && matchParent && nonAttachedAppWindow);
+ final boolean navWasHiddenByFlags = (mTypesHiddenByFlags & Type.navigationBars()) != 0;
+ final boolean navIsHiddenByFlags = (sysUiVis & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0;
+
+ @InsetsType int typesToHide = 0;
+ @InsetsType int typesToShow = 0;
+ if (statusIsHiddenByFlags && !statusWasHiddenByFlags) {
+ typesToHide |= Type.statusBars();
+ } else if (!statusIsHiddenByFlags && statusWasHiddenByFlags) {
+ typesToShow |= Type.statusBars();
+ }
+ if (navIsHiddenByFlags && !navWasHiddenByFlags) {
+ typesToHide |= Type.navigationBars();
+ } else if (!navIsHiddenByFlags && navWasHiddenByFlags) {
+ typesToShow |= Type.navigationBars();
+ }
+ if (typesToHide != 0) {
+ getInsetsController().hide(typesToHide);
+ }
+ if (typesToShow != 0) {
+ getInsetsController().show(typesToShow);
+ }
+ mTypesHiddenByFlags |= typesToHide;
+ mTypesHiddenByFlags &= ~typesToShow;
+ }
+
+ private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
+ final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+ boolean windowSizeMayChange = false;
+
+ if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag,
+ "Measuring " + host + " in display " + desiredWindowWidth
+ + "x" + desiredWindowHeight + "...");
+
+ boolean goodMeasure = false;
+ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ // On large screens, we don't want to allow dialogs to just
+ // stretch to fill the entire width of the screen to display
+ // one line of text. First try doing the layout at a smaller
+ // size to see if it will fit.
+ final DisplayMetrics packageMetrics = res.getDisplayMetrics();
+ res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
+ int baseSize = 0;
+ if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
+ baseSize = (int)mTmpValue.getDimension(packageMetrics);
+ }
+ if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize
+ + ", desiredWindowWidth=" + desiredWindowWidth);
+ if (baseSize != 0 && desiredWindowWidth > baseSize) {
+ childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
+ childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
+ performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+ if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
+ + host.getMeasuredWidth() + "," + host.getMeasuredHeight()
+ + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
+ + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
+ if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
+ goodMeasure = true;
+ } else {
+ // Didn't fit in that size... try expanding a bit.
+ baseSize = (baseSize+desiredWindowWidth)/2;
+ if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="
+ + baseSize);
+ childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
+ performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+ if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
+ + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
+ if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
+ if (DEBUG_DIALOG) Log.v(mTag, "Good!");
+ goodMeasure = true;
+ }
+ }
+ }
+ }
+
+ if (!goodMeasure) {
+ childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
+ childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
+ performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+ if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
+ windowSizeMayChange = true;
+ }
+ }
+
+ if (DBG) {
+ System.out.println("======================================");
+ System.out.println("performTraversals -- after measure");
+ host.debug();
+ }
+
+ return windowSizeMayChange;
+ }
+
+ /**
+ * Modifies the input matrix such that it maps view-local coordinates to
+ * on-screen coordinates.
+ *
+ * @param m input matrix to modify
+ */
+ void transformMatrixToGlobal(Matrix m) {
+ m.preTranslate(mAttachInfo.mWindowLeft, mAttachInfo.mWindowTop);
+ }
+
+ /**
+ * Modifies the input matrix such that it maps on-screen coordinates to
+ * view-local coordinates.
+ *
+ * @param m input matrix to modify
+ */
+ void transformMatrixToLocal(Matrix m) {
+ m.postTranslate(-mAttachInfo.mWindowLeft, -mAttachInfo.mWindowTop);
+ }
+
+ /* package */ WindowInsets getWindowInsets(boolean forceConstruct) {
+ if (mLastWindowInsets == null || forceConstruct) {
+ final Configuration config = getConfiguration();
+ mLastWindowInsets = mInsetsController.calculateInsets(
+ config.isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars,
+ mWindowAttributes.type, config.windowConfiguration.getWindowingMode(),
+ mWindowAttributes.softInputMode, mWindowAttributes.flags,
+ (mWindowAttributes.systemUiVisibility
+ | mWindowAttributes.subtreeSystemUiVisibility));
+
+ mAttachInfo.mContentInsets.set(mLastWindowInsets.getSystemWindowInsets().toRect());
+ mAttachInfo.mStableInsets.set(mLastWindowInsets.getStableInsets().toRect());
+ mAttachInfo.mVisibleInsets.set(mInsetsController.calculateVisibleInsets(
+ mWindowAttributes.softInputMode));
+ }
+ return mLastWindowInsets;
+ }
+
+ public void dispatchApplyInsets(View host) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");
+ mApplyInsetsRequested = false;
+ WindowInsets insets = getWindowInsets(true /* forceConstruct */);
+ if (!shouldDispatchCutout()) {
+ // Window is either not laid out in cutout or the status bar inset takes care of
+ // clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.
+ insets = insets.consumeDisplayCutout();
+ }
+ host.dispatchApplyWindowInsets(insets);
+ mAttachInfo.delayNotifyContentCaptureInsetsEvent(insets.getInsets(Type.all()));
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ private boolean updateCaptionInsets() {
+ if (!(mView instanceof DecorView)) return false;
+ final int captionInsetsHeight = ((DecorView) mView).getCaptionInsetsHeight();
+ final Rect captionFrame = new Rect();
+ if (captionInsetsHeight != 0) {
+ captionFrame.set(mWinFrame.left, mWinFrame.top, mWinFrame.right,
+ mWinFrame.top + captionInsetsHeight);
+ }
+ if (mAttachInfo.mCaptionInsets.equals(captionFrame)) return false;
+ mAttachInfo.mCaptionInsets.set(captionFrame);
+ return true;
+ }
+
+ private boolean shouldDispatchCutout() {
+ return mWindowAttributes.layoutInDisplayCutoutMode
+ == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ || mWindowAttributes.layoutInDisplayCutoutMode
+ == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ }
+
+ @VisibleForTesting
+ public InsetsController getInsetsController() {
+ return mInsetsController;
+ }
+
+ private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
+ return lp.type == TYPE_STATUS_BAR_ADDITIONAL
+ || lp.type == TYPE_INPUT_METHOD
+ || lp.type == TYPE_VOLUME_OVERLAY;
+ }
+
+ int dipToPx(int dip) {
+ final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
+ return (int) (displayMetrics.density * dip + 0.5f);
+ }
+
+ private void performTraversals() {
+ // cache mView since it is used so much below...
+ final View host = mView;
+
+ if (DBG) {
+ System.out.println("======================================");
+ System.out.println("performTraversals");
+ host.debug();
+ }
+
+ if (host == null || !mAdded) {
+ return;
+ }
+
+ // This is to ensure we don't end up queueing new frames while waiting on a previous frame
+ // to get latched. This can happen when there's been a sync request for this window. The
+ // frame could be in a transaction that's passed to different processes to ensure
+ // synchronization. It continues to block until ViewRootImpl receives a callback that the
+ // transaction containing the buffer has been sent to SurfaceFlinger. Once we receive, that
+ // signal, we know it's safe to start queuing new buffers.
+ //
+ // When the callback is invoked, it will trigger a traversal request if
+ // mRequestedTraverseWhilePaused is set so there's no need to attempt a retry here.
+ if (mWaitForBlastSyncComplete) {
+ if (DEBUG_BLAST) {
+ Log.w(mTag, "Can't perform draw while waiting for a transaction complete");
+ }
+ mRequestedTraverseWhilePaused = true;
+ return;
+ }
+
+ mIsInTraversal = true;
+ mWillDrawSoon = true;
+ boolean windowSizeMayChange = false;
+ WindowManager.LayoutParams lp = mWindowAttributes;
+
+ int desiredWindowWidth;
+ int desiredWindowHeight;
+
+ final int viewVisibility = getHostVisibility();
+ final boolean viewVisibilityChanged = !mFirst
+ && (mViewVisibility != viewVisibility || mNewSurfaceNeeded
+ // Also check for possible double visibility update, which will make current
+ // viewVisibility value equal to mViewVisibility and we may miss it.
+ || mAppVisibilityChanged);
+ mAppVisibilityChanged = false;
+ final boolean viewUserVisibilityChanged = !mFirst &&
+ ((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE));
+
+ WindowManager.LayoutParams params = null;
+ CompatibilityInfo compatibilityInfo =
+ mDisplay.getDisplayAdjustments().getCompatibilityInfo();
+ if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
+ params = lp;
+ mFullRedrawNeeded = true;
+ mLayoutRequested = true;
+ if (mLastInCompatMode) {
+ params.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
+ mLastInCompatMode = false;
+ } else {
+ params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
+ mLastInCompatMode = true;
+ }
+ }
+
+ Rect frame = mWinFrame;
+ if (mFirst) {
+ mFullRedrawNeeded = true;
+ mLayoutRequested = true;
+
+ final Configuration config = getConfiguration();
+ if (shouldUseDisplaySize(lp)) {
+ // NOTE -- system code, won't try to do compat mode.
+ Point size = new Point();
+ mDisplay.getRealSize(size);
+ desiredWindowWidth = size.x;
+ desiredWindowHeight = size.y;
+ } else if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
+ || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ // For wrap content, we have to remeasure later on anyways. Use size consistent with
+ // below so we get best use of the measure cache.
+ desiredWindowWidth = dipToPx(config.screenWidthDp);
+ desiredWindowHeight = dipToPx(config.screenHeightDp);
+ } else {
+ // After addToDisplay, the frame contains the frameHint from window manager, which
+ // for most windows is going to be the same size as the result of relayoutWindow.
+ // Using this here allows us to avoid remeasuring after relayoutWindow
+ desiredWindowWidth = frame.width();
+ desiredWindowHeight = frame.height();
+ }
+
+ // We used to use the following condition to choose 32 bits drawing caches:
+ // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
+ // However, windows are now always 32 bits by default, so choose 32 bits
+ mAttachInfo.mUse32BitDrawingCache = true;
+ mAttachInfo.mWindowVisibility = viewVisibility;
+ mAttachInfo.mRecomputeGlobalAttributes = false;
+ mLastConfigurationFromResources.setTo(config);
+ mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
+ // Set the layout direction if it has not been set before (inherit is the default)
+ if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
+ host.setLayoutDirection(config.getLayoutDirection());
+ }
+ host.dispatchAttachedToWindow(mAttachInfo, 0);
+ mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
+ dispatchApplyInsets(host);
+ } else {
+ desiredWindowWidth = frame.width();
+ desiredWindowHeight = frame.height();
+ if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
+ if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
+ mFullRedrawNeeded = true;
+ mLayoutRequested = true;
+ windowSizeMayChange = true;
+ }
+ }
+
+ if (viewVisibilityChanged) {
+ mAttachInfo.mWindowVisibility = viewVisibility;
+ host.dispatchWindowVisibilityChanged(viewVisibility);
+ if (viewUserVisibilityChanged) {
+ host.dispatchVisibilityAggregated(viewVisibility == View.VISIBLE);
+ }
+ if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
+ endDragResizing();
+ destroyHardwareResources();
+ }
+ }
+
+ // Non-visible windows can't hold accessibility focus.
+ if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
+ host.clearAccessibilityFocus();
+ }
+
+ // Execute enqueued actions on every traversal in case a detached view enqueued an action
+ getRunQueue().executeActions(mAttachInfo.mHandler);
+
+ boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
+ if (layoutRequested) {
+
+ final Resources res = mView.getContext().getResources();
+
+ if (mFirst) {
+ // make sure touch mode code executes by setting cached value
+ // to opposite of the added touch mode.
+ mAttachInfo.mInTouchMode = !mAddedTouchMode;
+ ensureTouchModeLocally(mAddedTouchMode);
+ } else {
+ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
+ || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ windowSizeMayChange = true;
+
+ if (shouldUseDisplaySize(lp)) {
+ // NOTE -- system code, won't try to do compat mode.
+ Point size = new Point();
+ mDisplay.getRealSize(size);
+ desiredWindowWidth = size.x;
+ desiredWindowHeight = size.y;
+ } else {
+ Configuration config = res.getConfiguration();
+ desiredWindowWidth = dipToPx(config.screenWidthDp);
+ desiredWindowHeight = dipToPx(config.screenHeightDp);
+ }
+ }
+ }
+
+ // Ask host how big it wants to be
+ windowSizeMayChange |= measureHierarchy(host, lp, res,
+ desiredWindowWidth, desiredWindowHeight);
+ }
+
+ if (collectViewAttributes()) {
+ params = lp;
+ }
+ if (mAttachInfo.mForceReportNewAttributes) {
+ mAttachInfo.mForceReportNewAttributes = false;
+ params = lp;
+ }
+
+ if (mFirst || mAttachInfo.mViewVisibilityChanged) {
+ mAttachInfo.mViewVisibilityChanged = false;
+ int resizeMode = mSoftInputMode & SOFT_INPUT_MASK_ADJUST;
+ // If we are in auto resize mode, then we need to determine
+ // what mode to use now.
+ if (resizeMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) {
+ final int N = mAttachInfo.mScrollContainers.size();
+ for (int i=0; i<N; i++) {
+ if (mAttachInfo.mScrollContainers.get(i).isShown()) {
+ resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+ }
+ }
+ if (resizeMode == 0) {
+ resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+ }
+ if ((lp.softInputMode & SOFT_INPUT_MASK_ADJUST) != resizeMode) {
+ lp.softInputMode = (lp.softInputMode & ~SOFT_INPUT_MASK_ADJUST) | resizeMode;
+ params = lp;
+ }
+ }
+ }
+
+ if (mApplyInsetsRequested && !(mWillMove || mWillResize)) {
+ dispatchApplyInsets(host);
+ if (mLayoutRequested) {
+ // Short-circuit catching a new layout request here, so
+ // we don't need to go through two layout passes when things
+ // change due to fitting system windows, which can happen a lot.
+ windowSizeMayChange |= measureHierarchy(host, lp,
+ mView.getContext().getResources(),
+ desiredWindowWidth, desiredWindowHeight);
+ }
+ }
+
+ if (layoutRequested) {
+ // Clear this now, so that if anything requests a layout in the
+ // rest of this function we will catch it and re-run a full
+ // layout pass.
+ mLayoutRequested = false;
+ }
+
+ boolean windowShouldResize = layoutRequested && windowSizeMayChange
+ && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
+ || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
+ frame.width() < desiredWindowWidth && frame.width() != mWidth)
+ || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
+ frame.height() < desiredWindowHeight && frame.height() != mHeight));
+ windowShouldResize |= mDragResizing && mResizeMode == RESIZE_MODE_FREEFORM;
+
+ // If the activity was just relaunched, it might have unfrozen the task bounds (while
+ // relaunching), so we need to force a call into window manager to pick up the latest
+ // bounds.
+ windowShouldResize |= mActivityRelaunched;
+
+ // Determine whether to compute insets.
+ // If there are no inset listeners remaining then we may still need to compute
+ // insets in case the old insets were non-empty and must be reset.
+ final boolean computesInternalInsets =
+ mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()
+ || mAttachInfo.mHasNonEmptyGivenInternalInsets;
+
+ boolean insetsPending = false;
+ int relayoutResult = 0;
+ boolean updatedConfiguration = false;
+
+ final int surfaceGenerationId = mSurface.getGenerationId();
+
+ final boolean isViewVisible = viewVisibility == View.VISIBLE;
+ final boolean windowRelayoutWasForced = mForceNextWindowRelayout;
+ boolean surfaceSizeChanged = false;
+ boolean surfaceCreated = false;
+ boolean surfaceDestroyed = false;
+ // True if surface generation id changes or relayout result is RELAYOUT_RES_SURFACE_CHANGED.
+ boolean surfaceReplaced = false;
+
+ final boolean windowAttributesChanged = mWindowAttributesChanged;
+ if (windowAttributesChanged) {
+ mWindowAttributesChanged = false;
+ params = lp;
+ }
+
+ if (params != null) {
+ if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0
+ && !PixelFormat.formatHasAlpha(params.format)) {
+ params.format = PixelFormat.TRANSLUCENT;
+ }
+ adjustLayoutParamsForCompatibility(params);
+ controlInsetsForCompatibility(params);
+ if (mDispatchedSystemBarAppearance != params.insetsFlags.appearance) {
+ mDispatchedSystemBarAppearance = params.insetsFlags.appearance;
+ mView.onSystemBarAppearanceChanged(mDispatchedSystemBarAppearance);
+ }
+ }
+ final boolean wasReportNextDraw = mReportNextDraw;
+
+ if (mFirst || windowShouldResize || viewVisibilityChanged || params != null
+ || mForceNextWindowRelayout) {
+ mForceNextWindowRelayout = false;
+
+ // If this window is giving internal insets to the window manager, then we want to first
+ // make the provided insets unchanged during layout. This avoids it briefly causing
+ // other windows to resize/move based on the raw frame of the window, waiting until we
+ // can finish laying out this window and get back to the window manager with the
+ // ultimately computed insets.
+ insetsPending = computesInternalInsets;
+
+ if (mSurfaceHolder != null) {
+ mSurfaceHolder.mSurfaceLock.lock();
+ mDrawingAllowed = true;
+ }
+
+ boolean hwInitialized = false;
+ boolean dispatchApplyInsets = false;
+ boolean hadSurface = mSurface.isValid();
+
+ try {
+ if (DEBUG_LAYOUT) {
+ Log.i(mTag, "host=w:" + host.getMeasuredWidth() + ", h:" +
+ host.getMeasuredHeight() + ", params=" + params);
+ }
+
+ if (mAttachInfo.mThreadedRenderer != null) {
+ // relayoutWindow may decide to destroy mSurface. As that decision
+ // happens in WindowManager service, we need to be defensive here
+ // and stop using the surface in case it gets destroyed.
+ if (mAttachInfo.mThreadedRenderer.pause()) {
+ // Animations were running so we need to push a frame
+ // to resume them
+ mDirty.set(0, 0, mWidth, mHeight);
+ }
+ }
+ if (mFirst || viewVisibilityChanged) {
+ mViewFrameInfo.flags |= FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED;
+ }
+ relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
+ final boolean freeformResizing = (relayoutResult
+ & WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING_FREEFORM) != 0;
+ final boolean dockedResizing = (relayoutResult
+ & WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING_DOCKED) != 0;
+ final boolean dragResizing = freeformResizing || dockedResizing;
+ if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_BLAST_SYNC) != 0) {
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Relayout called with blastSync");
+ }
+ reportNextDraw();
+ if (isHardwareEnabled()) {
+ mNextDrawUseBlastSync = true;
+ }
+ }
+
+ if (mSurfaceControl.isValid()) {
+ updateOpacity(mWindowAttributes, dragResizing);
+ }
+
+ if (DEBUG_LAYOUT) Log.v(mTag, "relayout: frame=" + frame.toShortString()
+ + " surface=" + mSurface);
+
+ // If the pending {@link MergedConfiguration} handed back from
+ // {@link #relayoutWindow} does not match the one last reported,
+ // WindowManagerService has reported back a frame from a configuration not yet
+ // handled by the client. In this case, we need to accept the configuration so we
+ // do not lay out and draw with the wrong configuration.
+ if (!mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
+ if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: "
+ + mPendingMergedConfiguration.getMergedConfiguration());
+ performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
+ !mFirst, INVALID_DISPLAY /* same display */);
+ updatedConfiguration = true;
+ }
+
+ surfaceSizeChanged = false;
+ if (!mLastSurfaceSize.equals(mSurfaceSize)) {
+ surfaceSizeChanged = true;
+ mLastSurfaceSize.set(mSurfaceSize.x, mSurfaceSize.y);
+ }
+ final boolean alwaysConsumeSystemBarsChanged =
+ mPendingAlwaysConsumeSystemBars != mAttachInfo.mAlwaysConsumeSystemBars;
+ updateColorModeIfNeeded(lp.getColorMode());
+ surfaceCreated = !hadSurface && mSurface.isValid();
+ surfaceDestroyed = hadSurface && !mSurface.isValid();
+
+ // When using Blast, the surface generation id may not change when there's a new
+ // SurfaceControl. In that case, we also check relayout flag
+ // RELAYOUT_RES_SURFACE_CHANGED since it should indicate that WMS created a new
+ // SurfaceControl.
+ surfaceReplaced = (surfaceGenerationId != mSurface.getGenerationId()
+ || (relayoutResult & RELAYOUT_RES_SURFACE_CHANGED)
+ == RELAYOUT_RES_SURFACE_CHANGED)
+ && mSurface.isValid();
+ if (surfaceReplaced) {
+ mSurfaceSequenceId++;
+ }
+
+ if (alwaysConsumeSystemBarsChanged) {
+ mAttachInfo.mAlwaysConsumeSystemBars = mPendingAlwaysConsumeSystemBars;
+ dispatchApplyInsets = true;
+ }
+ if (updateCaptionInsets()) {
+ dispatchApplyInsets = true;
+ }
+ if (dispatchApplyInsets || mLastSystemUiVisibility !=
+ mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested) {
+ mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
+ dispatchApplyInsets(host);
+ // We applied insets so force contentInsetsChanged to ensure the
+ // hierarchy is measured below.
+ dispatchApplyInsets = true;
+ }
+
+ if (surfaceCreated) {
+ // If we are creating a new surface, then we need to
+ // completely redraw it.
+ mFullRedrawNeeded = true;
+ mPreviousTransparentRegion.setEmpty();
+
+ // Only initialize up-front if transparent regions are not
+ // requested, otherwise defer to see if the entire window
+ // will be transparent
+ if (mAttachInfo.mThreadedRenderer != null) {
+ try {
+ hwInitialized = mAttachInfo.mThreadedRenderer.initialize(mSurface);
+ if (hwInitialized && (host.mPrivateFlags
+ & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0) {
+ // Don't pre-allocate if transparent regions
+ // are requested as they may not be needed
+ mAttachInfo.mThreadedRenderer.allocateBuffers();
+ }
+ } catch (OutOfResourcesException e) {
+ handleOutOfResourcesException(e);
+ return;
+ }
+ }
+ } else if (surfaceDestroyed) {
+ // If the surface has been removed, then reset the scroll
+ // positions.
+ if (mLastScrolledFocus != null) {
+ mLastScrolledFocus.clear();
+ }
+ mScrollY = mCurScrollY = 0;
+ if (mView instanceof RootViewSurfaceTaker) {
+ ((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY);
+ }
+ if (mScroller != null) {
+ mScroller.abortAnimation();
+ }
+ // Our surface is gone
+ if (isHardwareEnabled()) {
+ mAttachInfo.mThreadedRenderer.destroy();
+ }
+ } else if ((surfaceReplaced
+ || surfaceSizeChanged || windowRelayoutWasForced)
+ && mSurfaceHolder == null
+ && mAttachInfo.mThreadedRenderer != null
+ && mSurface.isValid()) {
+ mFullRedrawNeeded = true;
+ try {
+ // Need to do updateSurface (which leads to CanvasContext::setSurface and
+ // re-create the EGLSurface) if either the Surface changed (as indicated by
+ // generation id), or WindowManager changed the surface size. The latter is
+ // because on some chips, changing the consumer side's BufferQueue size may
+ // not take effect immediately unless we create a new EGLSurface.
+ // Note that frame size change doesn't always imply surface size change (eg.
+ // drag resizing uses fullscreen surface), need to check surfaceSizeChanged
+ // flag from WindowManager.
+ mAttachInfo.mThreadedRenderer.updateSurface(mSurface);
+ } catch (OutOfResourcesException e) {
+ handleOutOfResourcesException(e);
+ return;
+ }
+ }
+
+ if (mDragResizing != dragResizing) {
+ if (dragResizing) {
+ mResizeMode = freeformResizing
+ ? RESIZE_MODE_FREEFORM
+ : RESIZE_MODE_DOCKED_DIVIDER;
+ final boolean backdropSizeMatchesFrame =
+ mWinFrame.width() == mPendingBackDropFrame.width()
+ && mWinFrame.height() == mPendingBackDropFrame.height();
+ // TODO: Need cutout?
+ startDragResizing(mPendingBackDropFrame, !backdropSizeMatchesFrame,
+ mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mResizeMode);
+ } else {
+ // We shouldn't come here, but if we come we should end the resize.
+ endDragResizing();
+ }
+ }
+ if (!mUseMTRenderer) {
+ if (dragResizing) {
+ mCanvasOffsetX = mWinFrame.left;
+ mCanvasOffsetY = mWinFrame.top;
+ } else {
+ mCanvasOffsetX = mCanvasOffsetY = 0;
+ }
+ }
+ } catch (RemoteException e) {
+ }
+
+ if (DEBUG_ORIENTATION) Log.v(
+ TAG, "Relayout returned: frame=" + frame + ", surface=" + mSurface);
+
+ mAttachInfo.mWindowLeft = frame.left;
+ mAttachInfo.mWindowTop = frame.top;
+
+ // !!FIXME!! This next section handles the case where we did not get the
+ // window size we asked for. We should avoid this by getting a maximum size from
+ // the window session beforehand.
+ if (mWidth != frame.width() || mHeight != frame.height()) {
+ mWidth = frame.width();
+ mHeight = frame.height();
+ }
+
+ if (mSurfaceHolder != null) {
+ // The app owns the surface; tell it about what is going on.
+ if (mSurface.isValid()) {
+ // XXX .copyFrom() doesn't work!
+ //mSurfaceHolder.mSurface.copyFrom(mSurface);
+ mSurfaceHolder.mSurface = mSurface;
+ }
+ mSurfaceHolder.setSurfaceFrameSize(mWidth, mHeight);
+ mSurfaceHolder.mSurfaceLock.unlock();
+ if (surfaceCreated) {
+ mSurfaceHolder.ungetCallbacks();
+
+ mIsCreating = true;
+ SurfaceHolder.Callback[] callbacks = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceCreated(mSurfaceHolder);
+ }
+ }
+ }
+
+ if ((surfaceCreated || surfaceReplaced || surfaceSizeChanged
+ || windowAttributesChanged) && mSurface.isValid()) {
+ SurfaceHolder.Callback[] callbacks = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceChanged(mSurfaceHolder, lp.format,
+ mWidth, mHeight);
+ }
+ }
+ mIsCreating = false;
+ }
+
+ if (surfaceDestroyed) {
+ notifyHolderSurfaceDestroyed();
+ mSurfaceHolder.mSurfaceLock.lock();
+ try {
+ mSurfaceHolder.mSurface = new Surface();
+ } finally {
+ mSurfaceHolder.mSurfaceLock.unlock();
+ }
+ }
+ }
+
+ final ThreadedRenderer threadedRenderer = mAttachInfo.mThreadedRenderer;
+ if (threadedRenderer != null && threadedRenderer.isEnabled()) {
+ if (hwInitialized
+ || mWidth != threadedRenderer.getWidth()
+ || mHeight != threadedRenderer.getHeight()
+ || mNeedsRendererSetup) {
+ threadedRenderer.setup(mWidth, mHeight, mAttachInfo,
+ mWindowAttributes.surfaceInsets);
+ mNeedsRendererSetup = false;
+ }
+ }
+
+ // TODO: In the CL "ViewRootImpl: Fix issue with early draw report in
+ // seamless rotation". We moved processing of RELAYOUT_RES_BLAST_SYNC
+ // earlier in the function, potentially triggering a call to
+ // reportNextDraw(). That same CL changed this and the next reference
+ // to wasReportNextDraw, such that this logic would remain undisturbed
+ // (it continues to operate as if the code was never moved). This was
+ // done to achieve a more hermetic fix for S, but it's entirely
+ // possible that checking the most recent value is actually more
+ // correct here.
+ if (!mStopped || wasReportNextDraw) {
+ boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
+ (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
+ if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
+ || mHeight != host.getMeasuredHeight() || dispatchApplyInsets ||
+ updatedConfiguration) {
+ int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
+ int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
+
+ if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed! mWidth="
+ + mWidth + " measuredWidth=" + host.getMeasuredWidth()
+ + " mHeight=" + mHeight
+ + " measuredHeight=" + host.getMeasuredHeight()
+ + " dispatchApplyInsets=" + dispatchApplyInsets);
+
+ // Ask host how big it wants to be
+ performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ // Implementation of weights from WindowManager.LayoutParams
+ // We just grow the dimensions as needed and re-measure if
+ // needs be
+ int width = host.getMeasuredWidth();
+ int height = host.getMeasuredHeight();
+ boolean measureAgain = false;
+
+ if (lp.horizontalWeight > 0.0f) {
+ width += (int) ((mWidth - width) * lp.horizontalWeight);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
+ MeasureSpec.EXACTLY);
+ measureAgain = true;
+ }
+ if (lp.verticalWeight > 0.0f) {
+ height += (int) ((mHeight - height) * lp.verticalWeight);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
+ MeasureSpec.EXACTLY);
+ measureAgain = true;
+ }
+
+ if (measureAgain) {
+ if (DEBUG_LAYOUT) Log.v(mTag,
+ "And hey let's measure once more: width=" + width
+ + " height=" + height);
+ performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ layoutRequested = true;
+ }
+ }
+ } else {
+ // Not the first pass and no window/insets/visibility change but the window
+ // may have moved and we need check that and if so to update the left and right
+ // in the attach info. We translate only the window frame since on window move
+ // the window manager tells us only for the new frame but the insets are the
+ // same and we do not want to translate them more than once.
+ maybeHandleWindowMove(frame);
+ }
+
+ if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || windowAttributesChanged) {
+ // If the surface has been replaced, there's a chance the bounds layer is not parented
+ // to the new layer. When updating bounds layer, also reparent to the main VRI
+ // SurfaceControl to ensure it's correctly placed in the hierarchy.
+ //
+ // This needs to be done on the client side since WMS won't reparent the children to the
+ // new surface if it thinks the app is closing. WMS gets the signal that the app is
+ // stopping, but on the client side it doesn't get stopped since it's restarted quick
+ // enough. WMS doesn't want to keep around old children since they will leak when the
+ // client creates new children.
+ prepareSurfaces();
+ }
+
+ final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
+ boolean triggerGlobalLayoutListener = didLayout
+ || mAttachInfo.mRecomputeGlobalAttributes;
+ if (didLayout) {
+ performLayout(lp, mWidth, mHeight);
+
+ // By this point all views have been sized and positioned
+ // We can compute the transparent area
+
+ if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
+ // start out transparent
+ // TODO: AVOID THAT CALL BY CACHING THE RESULT?
+ host.getLocationInWindow(mTmpLocation);
+ mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
+ mTmpLocation[0] + host.mRight - host.mLeft,
+ mTmpLocation[1] + host.mBottom - host.mTop);
+
+ host.gatherTransparentRegion(mTransparentRegion);
+ if (mTranslator != null) {
+ mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
+ }
+
+ if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
+ mPreviousTransparentRegion.set(mTransparentRegion);
+ mFullRedrawNeeded = true;
+ // TODO: Ideally we would do this in prepareSurfaces,
+ // but prepareSurfaces is currently working under
+ // the assumption that we paused the render thread
+ // via the WM relayout code path. We probably eventually
+ // want to synchronize transparent region hint changes
+ // with draws.
+ SurfaceControl sc = getSurfaceControl();
+ if (sc.isValid()) {
+ mTransaction.setTransparentRegionHint(sc, mTransparentRegion).apply();
+ }
+ }
+ }
+
+ if (DBG) {
+ System.out.println("======================================");
+ System.out.println("performTraversals -- after setFrame");
+ host.debug();
+ }
+ }
+
+ // These callbacks will trigger SurfaceView SurfaceHolder.Callbacks and must be invoked
+ // after the measure pass. If its invoked before the measure pass and the app modifies
+ // the view hierarchy in the callbacks, we could leave the views in a broken state.
+ if (surfaceCreated) {
+ notifySurfaceCreated();
+ } else if (surfaceReplaced) {
+ notifySurfaceReplaced();
+ } else if (surfaceDestroyed) {
+ notifySurfaceDestroyed();
+ }
+
+ if (triggerGlobalLayoutListener) {
+ mAttachInfo.mRecomputeGlobalAttributes = false;
+ mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
+ }
+
+ if (computesInternalInsets) {
+ // Clear the original insets.
+ final ViewTreeObserver.InternalInsetsInfo insets = mAttachInfo.mGivenInternalInsets;
+ insets.reset();
+
+ // Compute new insets in place.
+ mAttachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);
+ mAttachInfo.mHasNonEmptyGivenInternalInsets = !insets.isEmpty();
+
+ // Tell the window manager.
+ if (insetsPending || !mLastGivenInsets.equals(insets)) {
+ mLastGivenInsets.set(insets);
+
+ // Translate insets to screen coordinates if needed.
+ final Rect contentInsets;
+ final Rect visibleInsets;
+ final Region touchableRegion;
+ if (mTranslator != null) {
+ contentInsets = mTranslator.getTranslatedContentInsets(insets.contentInsets);
+ visibleInsets = mTranslator.getTranslatedVisibleInsets(insets.visibleInsets);
+ touchableRegion = mTranslator.getTranslatedTouchableArea(insets.touchableRegion);
+ } else {
+ contentInsets = insets.contentInsets;
+ visibleInsets = insets.visibleInsets;
+ touchableRegion = insets.touchableRegion;
+ }
+
+ try {
+ mWindowSession.setInsets(mWindow, insets.mTouchableInsets,
+ contentInsets, visibleInsets, touchableRegion);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ if (mFirst) {
+ if (sAlwaysAssignFocus || !isInTouchMode()) {
+ // handle first focus request
+ if (DEBUG_INPUT_RESIZE) {
+ Log.v(mTag, "First: mView.hasFocus()=" + mView.hasFocus());
+ }
+ if (mView != null) {
+ if (!mView.hasFocus()) {
+ mView.restoreDefaultFocus();
+ if (DEBUG_INPUT_RESIZE) {
+ Log.v(mTag, "First: requested focused view=" + mView.findFocus());
+ }
+ } else {
+ if (DEBUG_INPUT_RESIZE) {
+ Log.v(mTag, "First: existing focused view=" + mView.findFocus());
+ }
+ }
+ }
+ } else {
+ // Some views (like ScrollView) won't hand focus to descendants that aren't within
+ // their viewport. Before layout, there's a good change these views are size 0
+ // which means no children can get focus. After layout, this view now has size, but
+ // is not guaranteed to hand-off focus to a focusable child (specifically, the edge-
+ // case where the child has a size prior to layout and thus won't trigger
+ // focusableViewAvailable).
+ View focused = mView.findFocus();
+ if (focused instanceof ViewGroup
+ && ((ViewGroup) focused).getDescendantFocusability()
+ == ViewGroup.FOCUS_AFTER_DESCENDANTS) {
+ focused.restoreDefaultFocus();
+ }
+ }
+ }
+
+ final boolean changedVisibility = (viewVisibilityChanged || mFirst) && isViewVisible;
+ final boolean hasWindowFocus = mAttachInfo.mHasWindowFocus && isViewVisible;
+ final boolean regainedFocus = hasWindowFocus && mLostWindowFocus;
+ if (regainedFocus) {
+ mLostWindowFocus = false;
+ } else if (!hasWindowFocus && mHadWindowFocus) {
+ mLostWindowFocus = true;
+ }
+
+ if (changedVisibility || regainedFocus) {
+ // Toasts are presented as notifications - don't present them as windows as well
+ boolean isToast = mWindowAttributes.type == TYPE_TOAST;
+ if (!isToast) {
+ host.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+ }
+
+ mFirst = false;
+ mWillDrawSoon = false;
+ mNewSurfaceNeeded = false;
+ mActivityRelaunched = false;
+ mViewVisibility = viewVisibility;
+ mHadWindowFocus = hasWindowFocus;
+
+ mImeFocusController.onTraversal(hasWindowFocus, mWindowAttributes);
+
+ // Remember if we must report the next draw.
+ if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
+ reportNextDraw();
+ }
+
+ boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
+
+ if (!cancelDraw) {
+ if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
+ for (int i = 0; i < mPendingTransitions.size(); ++i) {
+ mPendingTransitions.get(i).startChangingAnimations();
+ }
+ mPendingTransitions.clear();
+ }
+ performDraw();
+ } else {
+ if (isViewVisible) {
+ // Try again
+ scheduleTraversals();
+ } else {
+ if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
+ for (int i = 0; i < mPendingTransitions.size(); ++i) {
+ mPendingTransitions.get(i).endChangingAnimations();
+ }
+ mPendingTransitions.clear();
+ }
+
+ // We may never draw since it's not visible. Report back that we're finished
+ // drawing.
+ if (!wasReportNextDraw && mReportNextDraw) {
+ mReportNextDraw = false;
+ pendingDrawFinished();
+ }
+ }
+ }
+
+ if (mAttachInfo.mContentCaptureEvents != null) {
+ notifyContentCatpureEvents();
+ }
+
+ mIsInTraversal = false;
+ }
+
+ private void notifyContentCatpureEvents() {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
+ try {
+ MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
+ .getMainContentCaptureSession();
+ for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) {
+ int sessionId = mAttachInfo.mContentCaptureEvents.keyAt(i);
+ mainSession.notifyViewTreeEvent(sessionId, /* started= */ true);
+ ArrayList<Object> events = mAttachInfo.mContentCaptureEvents
+ .valueAt(i);
+ for_each_event: for (int j = 0; j < events.size(); j++) {
+ Object event = events.get(j);
+ if (event instanceof AutofillId) {
+ mainSession.notifyViewDisappeared(sessionId, (AutofillId) event);
+ } else if (event instanceof View) {
+ View view = (View) event;
+ ContentCaptureSession session = view.getContentCaptureSession();
+ if (session == null) {
+ Log.w(mTag, "no content capture session on view: " + view);
+ continue for_each_event;
+ }
+ int actualId = session.getId();
+ if (actualId != sessionId) {
+ Log.w(mTag, "content capture session mismatch for view (" + view
+ + "): was " + sessionId + " before, it's " + actualId + " now");
+ continue for_each_event;
+ }
+ ViewStructure structure = session.newViewStructure(view);
+ view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
+ session.notifyViewAppeared(structure);
+ } else if (event instanceof Insets) {
+ mainSession.notifyViewInsetsChanged(sessionId, (Insets) event);
+ } else {
+ Log.w(mTag, "invalid content capture event: " + event);
+ }
+ }
+ mainSession.notifyViewTreeEvent(sessionId, /* started= */ false);
+ }
+ mAttachInfo.mContentCaptureEvents = null;
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ private void notifyHolderSurfaceDestroyed() {
+ mSurfaceHolder.ungetCallbacks();
+ SurfaceHolder.Callback[] callbacks = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceDestroyed(mSurfaceHolder);
+ }
+ }
+ }
+
+ private void maybeHandleWindowMove(Rect frame) {
+ // TODO: Well, we are checking whether the frame has changed similarly
+ // to how this is done for the insets. This is however incorrect since
+ // the insets and the frame are translated. For example, the old frame
+ // was (1, 1 - 1, 1) and was translated to say (2, 2 - 2, 2), now the new
+ // reported frame is (2, 2 - 2, 2) which implies no change but this is not
+ // true since we are comparing a not translated value to a translated one.
+ // This scenario is rare but we may want to fix that.
+
+ final boolean windowMoved = mAttachInfo.mWindowLeft != frame.left
+ || mAttachInfo.mWindowTop != frame.top;
+ if (windowMoved) {
+ mAttachInfo.mWindowLeft = frame.left;
+ mAttachInfo.mWindowTop = frame.top;
+ }
+ if (windowMoved || mAttachInfo.mNeedsUpdateLightCenter) {
+ // Update the light position for the new offsets.
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.setLightCenter(mAttachInfo);
+ }
+ mAttachInfo.mNeedsUpdateLightCenter = false;
+ }
+ }
+
+ private void handleWindowFocusChanged() {
+ final boolean hasWindowFocus;
+ final boolean inTouchMode;
+ synchronized (this) {
+ if (!mWindowFocusChanged) {
+ return;
+ }
+ mWindowFocusChanged = false;
+ hasWindowFocus = mUpcomingWindowFocus;
+ inTouchMode = mUpcomingInTouchMode;
+ }
+ // TODO (b/131181940): Make sure this doesn't leak Activity with mActivityConfigCallback
+ // config changes.
+ if (hasWindowFocus) {
+ mInsetsController.onWindowFocusGained(
+ getFocusedViewOrNull() != null /* hasViewFocused */);
+ } else {
+ mInsetsController.onWindowFocusLost();
+ }
+
+ if (mAdded) {
+ profileRendering(hasWindowFocus);
+
+ if (hasWindowFocus) {
+ ensureTouchModeLocally(inTouchMode);
+ if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) {
+ mFullRedrawNeeded = true;
+ try {
+ final Rect surfaceInsets = mWindowAttributes.surfaceInsets;
+ mAttachInfo.mThreadedRenderer.initializeIfNeeded(
+ mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
+ } catch (OutOfResourcesException e) {
+ Log.e(mTag, "OutOfResourcesException locking surface", e);
+ try {
+ if (!mWindowSession.outOfMemory(mWindow)) {
+ Slog.w(mTag, "No processes killed for memory;"
+ + " killing self");
+ Process.killProcess(Process.myPid());
+ }
+ } catch (RemoteException ex) {
+ }
+ // Retry in a bit.
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ MSG_WINDOW_FOCUS_CHANGED), 500);
+ return;
+ }
+ }
+ }
+
+ mAttachInfo.mHasWindowFocus = hasWindowFocus;
+ mImeFocusController.updateImeFocusable(mWindowAttributes, true /* force */);
+ mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes);
+
+ if (mView != null) {
+ mAttachInfo.mKeyDispatchState.reset();
+ mView.dispatchWindowFocusChanged(hasWindowFocus);
+ mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus);
+ if (mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.hideTooltip();
+ }
+ }
+
+ // Note: must be done after the focus change callbacks,
+ // so all of the view state is set up correctly.
+ mImeFocusController.onPostWindowFocus(
+ getFocusedViewOrNull(), hasWindowFocus, mWindowAttributes);
+
+ if (hasWindowFocus) {
+ // Clear the forward bit. We can just do this directly, since
+ // the window manager doesn't care about it.
+ mWindowAttributes.softInputMode &=
+ ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ ((WindowManager.LayoutParams) mView.getLayoutParams())
+ .softInputMode &=
+ ~WindowManager.LayoutParams
+ .SOFT_INPUT_IS_FORWARD_NAVIGATION;
+
+ // Refocusing a window that has a focused view should fire a
+ // focus event for the view since the global focused view changed.
+ fireAccessibilityFocusEventIfHasFocusedNode();
+ } else {
+ if (mPointerCapture) {
+ handlePointerCaptureChanged(false);
+ }
+ }
+ }
+ mFirstInputStage.onWindowFocusChanged(hasWindowFocus);
+
+ // NOTE: there's no view visibility (appeared / disapparead) events when the windows focus
+ // is lost, so we don't need to to force a flush - there might be other events such as
+ // text changes, but these should be flushed independently.
+ if (hasWindowFocus) {
+ handleContentCaptureFlush();
+ }
+ }
+
+ private void fireAccessibilityFocusEventIfHasFocusedNode() {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return;
+ }
+ final View focusedView = mView.findFocus();
+ if (focusedView == null) {
+ return;
+ }
+ final AccessibilityNodeProvider provider = focusedView.getAccessibilityNodeProvider();
+ if (provider == null) {
+ focusedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ } else {
+ final AccessibilityNodeInfo focusedNode = findFocusedVirtualNode(provider);
+ if (focusedNode != null) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(
+ focusedNode.getSourceNodeId());
+ // This is a best effort since clearing and setting the focus via the
+ // provider APIs could have side effects. We don't have a provider API
+ // similar to that on View to ask a given event to be fired.
+ final AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ event.setSource(focusedView, virtualId);
+ event.setPackageName(focusedNode.getPackageName());
+ event.setChecked(focusedNode.isChecked());
+ event.setContentDescription(focusedNode.getContentDescription());
+ event.setPassword(focusedNode.isPassword());
+ event.getText().add(focusedNode.getText());
+ event.setEnabled(focusedNode.isEnabled());
+ focusedView.getParent().requestSendAccessibilityEvent(focusedView, event);
+ focusedNode.recycle();
+ }
+ }
+ }
+
+ private AccessibilityNodeInfo findFocusedVirtualNode(AccessibilityNodeProvider provider) {
+ AccessibilityNodeInfo focusedNode = provider.findFocus(
+ AccessibilityNodeInfo.FOCUS_INPUT);
+ if (focusedNode != null) {
+ return focusedNode;
+ }
+
+ if (!mContext.isAutofillCompatibilityEnabled()) {
+ return null;
+ }
+
+ // Unfortunately some provider implementations don't properly
+ // implement AccessibilityNodeProvider#findFocus
+ AccessibilityNodeInfo current = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+ if (current.isFocused()) {
+ return current;
+ }
+
+ final Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
+ fringe.offer(current);
+
+ while (!fringe.isEmpty()) {
+ current = fringe.poll();
+ final LongArray childNodeIds = current.getChildNodeIds();
+ if (childNodeIds== null || childNodeIds.size() <= 0) {
+ continue;
+ }
+ final int childCount = childNodeIds.size();
+ for (int i = 0; i < childCount; i++) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(
+ childNodeIds.get(i));
+ final AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(virtualId);
+ if (child != null) {
+ if (child.isFocused()) {
+ return child;
+ }
+ fringe.offer(child);
+ }
+ }
+ current.recycle();
+ }
+
+ return null;
+ }
+
+ private void handleOutOfResourcesException(Surface.OutOfResourcesException e) {
+ Log.e(mTag, "OutOfResourcesException initializing HW surface", e);
+ try {
+ if (!mWindowSession.outOfMemory(mWindow) &&
+ Process.myUid() != Process.SYSTEM_UID) {
+ Slog.w(mTag, "No processes killed for memory; killing self");
+ Process.killProcess(Process.myPid());
+ }
+ } catch (RemoteException ex) {
+ }
+ mLayoutRequested = true; // ask wm for a new surface next time.
+ }
+
+ private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
+ if (mView == null) {
+ return;
+ }
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
+ try {
+ mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ /**
+ * Called by {@link android.view.View#isInLayout()} to determine whether the view hierarchy
+ * is currently undergoing a layout pass.
+ *
+ * @return whether the view hierarchy is currently undergoing a layout pass
+ */
+ boolean isInLayout() {
+ return mInLayout;
+ }
+
+ /**
+ * Called by {@link android.view.View#requestLayout()} if the view hierarchy is currently
+ * undergoing a layout pass. requestLayout() should not generally be called during layout,
+ * unless the container hierarchy knows what it is doing (i.e., it is fine as long as
+ * all children in that container hierarchy are measured and laid out at the end of the layout
+ * pass for that container). If requestLayout() is called anyway, we handle it correctly
+ * by registering all requesters during a frame as it proceeds. At the end of the frame,
+ * we check all of those views to see if any still have pending layout requests, which
+ * indicates that they were not correctly handled by their container hierarchy. If that is
+ * the case, we clear all such flags in the tree, to remove the buggy flag state that leads
+ * to blank containers, and force a second request/measure/layout pass in this frame. If
+ * more requestLayout() calls are received during that second layout pass, we post those
+ * requests to the next frame to avoid possible infinite loops.
+ *
+ * <p>The return value from this method indicates whether the request should proceed
+ * (if it is a request during the first layout pass) or should be skipped and posted to the
+ * next frame (if it is a request during the second layout pass).</p>
+ *
+ * @param view the view that requested the layout.
+ *
+ * @return true if request should proceed, false otherwise.
+ */
+ boolean requestLayoutDuringLayout(final View view) {
+ if (view.mParent == null || view.mAttachInfo == null) {
+ // Would not normally trigger another layout, so just let it pass through as usual
+ return true;
+ }
+ if (!mLayoutRequesters.contains(view)) {
+ mLayoutRequesters.add(view);
+ }
+ if (!mHandlingLayoutInLayoutRequest) {
+ // Let the request proceed normally; it will be processed in a second layout pass
+ // if necessary
+ return true;
+ } else {
+ // Don't let the request proceed during the second layout pass.
+ // It will post to the next frame instead.
+ return false;
+ }
+ }
+
+ private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
+ int desiredWindowHeight) {
+ mScrollMayChange = true;
+ mInLayout = true;
+
+ final View host = mView;
+ if (host == null) {
+ return;
+ }
+ if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
+ Log.v(mTag, "Laying out " + host + " to (" +
+ host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
+ try {
+ host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
+
+ mInLayout = false;
+ int numViewsRequestingLayout = mLayoutRequesters.size();
+ if (numViewsRequestingLayout > 0) {
+ // requestLayout() was called during layout.
+ // If no layout-request flags are set on the requesting views, there is no problem.
+ // If some requests are still pending, then we need to clear those flags and do
+ // a full request/measure/layout pass to handle this situation.
+ ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
+ false);
+ if (validLayoutRequesters != null) {
+ // Set this flag to indicate that any further requests are happening during
+ // the second pass, which may result in posting those requests to the next
+ // frame instead
+ mHandlingLayoutInLayoutRequest = true;
+
+ // Process fresh layout requests, then measure and layout
+ int numValidRequests = validLayoutRequesters.size();
+ for (int i = 0; i < numValidRequests; ++i) {
+ final View view = validLayoutRequesters.get(i);
+ Log.w("View", "requestLayout() improperly called by " + view +
+ " during layout: running second layout pass");
+ view.requestLayout();
+ }
+ measureHierarchy(host, lp, mView.getContext().getResources(),
+ desiredWindowWidth, desiredWindowHeight);
+ mInLayout = true;
+ host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
+
+ mHandlingLayoutInLayoutRequest = false;
+
+ // Check the valid requests again, this time without checking/clearing the
+ // layout flags, since requests happening during the second pass get noop'd
+ validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
+ if (validLayoutRequesters != null) {
+ final ArrayList<View> finalRequesters = validLayoutRequesters;
+ // Post second-pass requests to the next frame
+ getRunQueue().post(new Runnable() {
+ @Override
+ public void run() {
+ int numValidRequests = finalRequesters.size();
+ for (int i = 0; i < numValidRequests; ++i) {
+ final View view = finalRequesters.get(i);
+ Log.w("View", "requestLayout() improperly called by " + view +
+ " during second layout pass: posting in next frame");
+ view.requestLayout();
+ }
+ }
+ });
+ }
+ }
+
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ mInLayout = false;
+ }
+
+ /**
+ * This method is called during layout when there have been calls to requestLayout() during
+ * layout. It walks through the list of views that requested layout to determine which ones
+ * still need it, based on visibility in the hierarchy and whether they have already been
+ * handled (as is usually the case with ListView children).
+ *
+ * @param layoutRequesters The list of views that requested layout during layout
+ * @param secondLayoutRequests Whether the requests were issued during the second layout pass.
+ * If so, the FORCE_LAYOUT flag was not set on requesters.
+ * @return A list of the actual views that still need to be laid out.
+ */
+ private ArrayList<View> getValidLayoutRequesters(ArrayList<View> layoutRequesters,
+ boolean secondLayoutRequests) {
+
+ int numViewsRequestingLayout = layoutRequesters.size();
+ ArrayList<View> validLayoutRequesters = null;
+ for (int i = 0; i < numViewsRequestingLayout; ++i) {
+ View view = layoutRequesters.get(i);
+ if (view != null && view.mAttachInfo != null && view.mParent != null &&
+ (secondLayoutRequests || (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) ==
+ View.PFLAG_FORCE_LAYOUT)) {
+ boolean gone = false;
+ View parent = view;
+ // Only trigger new requests for views in a non-GONE hierarchy
+ while (parent != null) {
+ if ((parent.mViewFlags & View.VISIBILITY_MASK) == View.GONE) {
+ gone = true;
+ break;
+ }
+ if (parent.mParent instanceof View) {
+ parent = (View) parent.mParent;
+ } else {
+ parent = null;
+ }
+ }
+ if (!gone) {
+ if (validLayoutRequesters == null) {
+ validLayoutRequesters = new ArrayList<View>();
+ }
+ validLayoutRequesters.add(view);
+ }
+ }
+ }
+ if (!secondLayoutRequests) {
+ // If we're checking the layout flags, then we need to clean them up also
+ for (int i = 0; i < numViewsRequestingLayout; ++i) {
+ View view = layoutRequesters.get(i);
+ while (view != null &&
+ (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) != 0) {
+ view.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT;
+ if (view.mParent instanceof View) {
+ view = (View) view.mParent;
+ } else {
+ view = null;
+ }
+ }
+ }
+ }
+ layoutRequesters.clear();
+ return validLayoutRequesters;
+ }
+
+ @Override
+ public void requestTransparentRegion(View child) {
+ // the test below should not fail unless someone is messing with us
+ checkThread();
+ if (mView != child) {
+ return;
+ }
+
+ if ((mView.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0) {
+ mView.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
+ // Need to make sure we re-evaluate the window attributes next
+ // time around, to ensure the window has the correct format.
+ mWindowAttributesChanged = true;
+ }
+
+ // Always request layout to apply the latest transparent region.
+ requestLayout();
+ }
+
+ /**
+ * Figures out the measure spec for the root view in a window based on it's
+ * layout params.
+ *
+ * @param windowSize
+ * The available width or height of the window
+ *
+ * @param rootDimension
+ * The layout params for one dimension (width or height) of the
+ * window.
+ *
+ * @return The measure spec to use to measure the root view.
+ */
+ private static int getRootMeasureSpec(int windowSize, int rootDimension) {
+ int measureSpec;
+ switch (rootDimension) {
+
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ // Window can't resize. Force root view to be windowSize.
+ measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
+ break;
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ // Window can resize. Set max size for root view.
+ measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
+ break;
+ default:
+ // Window wants to be an exact size. Force root view to be that size.
+ measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
+ break;
+ }
+ return measureSpec;
+ }
+
+ int mHardwareXOffset;
+ int mHardwareYOffset;
+
+ @Override
+ public void onPreDraw(RecordingCanvas canvas) {
+ // If mCurScrollY is not 0 then this influences the hardwareYOffset. The end result is we
+ // can apply offsets that are not handled by anything else, resulting in underdraw as
+ // the View is shifted (thus shifting the window background) exposing unpainted
+ // content. To handle this with minimal glitches we just clear to BLACK if the window
+ // is opaque. If it's not opaque then HWUI already internally does a glClear to
+ // transparent, so there's no risk of underdraw on non-opaque surfaces.
+ if (mCurScrollY != 0 && mHardwareYOffset != 0 && mAttachInfo.mThreadedRenderer.isOpaque()) {
+ canvas.drawColor(Color.BLACK);
+ }
+ canvas.translate(-mHardwareXOffset, -mHardwareYOffset);
+ }
+
+ @Override
+ public void onPostDraw(RecordingCanvas canvas) {
+ drawAccessibilityFocusedDrawableIfNeeded(canvas);
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onPostDraw(canvas);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ void outputDisplayList(View view) {
+ view.mRenderNode.output();
+ }
+
+ /**
+ * @see #PROPERTY_PROFILE_RENDERING
+ */
+ private void profileRendering(boolean enabled) {
+ if (mProfileRendering) {
+ mRenderProfilingEnabled = enabled;
+
+ if (mRenderProfiler != null) {
+ mChoreographer.removeFrameCallback(mRenderProfiler);
+ }
+ if (mRenderProfilingEnabled) {
+ if (mRenderProfiler == null) {
+ mRenderProfiler = new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ mDirty.set(0, 0, mWidth, mHeight);
+ scheduleTraversals();
+ if (mRenderProfilingEnabled) {
+ mChoreographer.postFrameCallback(mRenderProfiler);
+ }
+ }
+ };
+ }
+ mChoreographer.postFrameCallback(mRenderProfiler);
+ } else {
+ mRenderProfiler = null;
+ }
+ }
+ }
+
+ /**
+ * Called from draw() when DEBUG_FPS is enabled
+ */
+ private void trackFPS() {
+ // Tracks frames per second drawn. First value in a series of draws may be bogus
+ // because it down not account for the intervening idle time
+ long nowTime = System.currentTimeMillis();
+ if (mFpsStartTime < 0) {
+ mFpsStartTime = mFpsPrevTime = nowTime;
+ mFpsNumFrames = 0;
+ } else {
+ ++mFpsNumFrames;
+ String thisHash = Integer.toHexString(System.identityHashCode(this));
+ long frameTime = nowTime - mFpsPrevTime;
+ long totalTime = nowTime - mFpsStartTime;
+ Log.v(mTag, "0x" + thisHash + "\tFrame time:\t" + frameTime);
+ mFpsPrevTime = nowTime;
+ if (totalTime > 1000) {
+ float fps = (float) mFpsNumFrames * 1000 / totalTime;
+ Log.v(mTag, "0x" + thisHash + "\tFPS:\t" + fps);
+ mFpsStartTime = nowTime;
+ mFpsNumFrames = 0;
+ }
+ }
+ }
+
+ /**
+ * A count of the number of calls to pendingDrawFinished we
+ * require to notify the WM drawing is complete.
+ */
+ int mDrawsNeededToReport = 0;
+
+ /**
+ * Delay notifying WM of draw finished until
+ * a balanced call to pendingDrawFinished.
+ */
+ void drawPending() {
+ mDrawsNeededToReport++;
+ }
+
+ void pendingDrawFinished() {
+ if (mDrawsNeededToReport == 0) {
+ throw new RuntimeException("Unbalanced drawPending/pendingDrawFinished calls");
+ }
+ mDrawsNeededToReport--;
+ if (mDrawsNeededToReport == 0) {
+ reportDrawFinished();
+ } else if (DEBUG_BLAST) {
+ Log.d(mTag, "pendingDrawFinished. Waiting on draw reported mDrawsNeededToReport="
+ + mDrawsNeededToReport);
+ }
+ }
+
+ private void postDrawFinished() {
+ mHandler.sendEmptyMessage(MSG_DRAW_FINISHED);
+ }
+
+ private void reportDrawFinished() {
+ try {
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "reportDrawFinished");
+ }
+ mDrawsNeededToReport = 0;
+ mWindowSession.finishDrawing(mWindow, mSurfaceChangedTransaction);
+ } catch (RemoteException e) {
+ Log.e(mTag, "Unable to report draw finished", e);
+ mSurfaceChangedTransaction.apply();
+ } finally {
+ mSurfaceChangedTransaction.clear();
+ }
+ }
+
+ /**
+ * The callback will run on the render thread.
+ */
+ private HardwareRenderer.FrameCompleteCallback createFrameCompleteCallback(Handler handler,
+ boolean reportNextDraw, ArrayList<Runnable> commitCallbacks) {
+ return frameNr -> {
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Received frameCompleteCallback frameNum=" + frameNr);
+ }
+
+ handler.postAtFrontOfQueue(() -> {
+ if (mNextDrawUseBlastSync) {
+ // We don't need to synchronize mRtBLASTSyncTransaction here since we're
+ // guaranteed that this is called after onFrameDraw and mNextDrawUseBlastSync
+ // is only true when the UI thread is paused. Therefore, no one should be
+ // modifying this object until the next vsync.
+ mSurfaceChangedTransaction.merge(mRtBLASTSyncTransaction);
+ }
+
+ if (reportNextDraw) {
+ // TODO: Use the frame number
+ pendingDrawFinished();
+ }
+ if (commitCallbacks != null) {
+ for (int i = 0; i < commitCallbacks.size(); i++) {
+ commitCallbacks.get(i).run();
+ }
+ }
+ });
+ };
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isHardwareEnabled() {
+ return mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled();
+ }
+
+ private boolean addFrameCompleteCallbackIfNeeded() {
+ if (!isHardwareEnabled()) {
+ return false;
+ }
+
+ ArrayList<Runnable> commitCallbacks = mAttachInfo.mTreeObserver
+ .captureFrameCommitCallbacks();
+ final boolean needFrameCompleteCallback =
+ mNextDrawUseBlastSync || mReportNextDraw
+ || (commitCallbacks != null && commitCallbacks.size() > 0);
+ if (needFrameCompleteCallback) {
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Creating frameCompleteCallback"
+ + " mNextDrawUseBlastSync=" + mNextDrawUseBlastSync
+ + " mReportNextDraw=" + mReportNextDraw
+ + " commitCallbacks size="
+ + (commitCallbacks == null ? 0 : commitCallbacks.size()));
+ }
+ mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(
+ createFrameCompleteCallback(mAttachInfo.mHandler, mReportNextDraw,
+ commitCallbacks));
+ return true;
+ }
+ return false;
+ }
+
+ private void addFrameCallbackIfNeeded() {
+ final boolean nextDrawUseBlastSync = mNextDrawUseBlastSync;
+ final boolean reportNextDraw = mReportNextDraw;
+ final boolean hasBlurUpdates = mBlurRegionAggregator.hasUpdates();
+ final boolean needsCallbackForBlur = hasBlurUpdates || mBlurRegionAggregator.hasRegions();
+
+ if (!nextDrawUseBlastSync && !reportNextDraw && !needsCallbackForBlur) {
+ return;
+ }
+
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Creating frameDrawingCallback"
+ + " nextDrawUseBlastSync=" + nextDrawUseBlastSync
+ + " reportNextDraw=" + reportNextDraw
+ + " hasBlurUpdates=" + hasBlurUpdates);
+ }
+ mWaitForBlastSyncComplete = nextDrawUseBlastSync;
+ final BackgroundBlurDrawable.BlurRegion[] blurRegionsForFrame =
+ needsCallbackForBlur ? mBlurRegionAggregator.getBlurRegionsCopyForRT() : null;
+
+ // The callback will run on the render thread.
+ HardwareRenderer.FrameDrawingCallback frameDrawingCallback = frame -> {
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Received frameDrawingCallback frameNum=" + frame + "."
+ + " Creating transactionCompleteCallback=" + nextDrawUseBlastSync);
+ }
+
+ if (needsCallbackForBlur) {
+ mBlurRegionAggregator
+ .dispatchBlurTransactionIfNeeded(frame, blurRegionsForFrame, hasBlurUpdates);
+ }
+
+ if (mBlastBufferQueue == null) {
+ return;
+ }
+
+ if (nextDrawUseBlastSync) {
+ // Frame callbacks will always occur after submitting draw requests and before
+ // the draw actually occurs. This will ensure that we set the next transaction
+ // for the frame that's about to get drawn and not on a previous frame that.
+
+ // We don't need to synchronize mRtBLASTSyncTransaction here since it's not
+ // being modified and only sent to BlastBufferQueue.
+ mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction);
+
+ mBlastBufferQueue.setTransactionCompleteCallback(frame, frameNumber -> {
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Received transactionCompleteCallback frameNum=" + frame);
+ }
+ mHandler.postAtFrontOfQueue(() -> {
+ mNextDrawUseBlastSync = false;
+ mWaitForBlastSyncComplete = false;
+ if (DEBUG_BLAST) {
+ Log.d(mTag, "Scheduling a traversal=" + mRequestedTraverseWhilePaused
+ + " due to a previous skipped traversal.");
+ }
+ if (mRequestedTraverseWhilePaused) {
+ mRequestedTraverseWhilePaused = false;
+ scheduleTraversals();
+ }
+ });
+ });
+ } else if (reportNextDraw) {
+ // If we need to report next draw, wait for adapter to flush its shadow queue
+ // by processing previously queued buffers so that we can submit the
+ // transaction a timely manner.
+ mBlastBufferQueue.flushShadowQueue();
+ }
+ };
+ registerRtFrameCallback(frameDrawingCallback);
+ }
+
+ private void performDraw() {
+ if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
+ return;
+ } else if (mView == null) {
+ return;
+ }
+
+ final boolean fullRedrawNeeded =
+ mFullRedrawNeeded || mReportNextDraw || mNextDrawUseBlastSync;
+ mFullRedrawNeeded = false;
+
+ mIsDrawing = true;
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
+
+ boolean usingAsyncReport = addFrameCompleteCallbackIfNeeded();
+ addFrameCallbackIfNeeded();
+
+ try {
+ boolean canUseAsync = draw(fullRedrawNeeded);
+ if (usingAsyncReport && !canUseAsync) {
+ mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
+ usingAsyncReport = false;
+ }
+ } finally {
+ mIsDrawing = false;
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ // For whatever reason we didn't create a HardwareRenderer, end any
+ // hardware animations that are now dangling
+ if (mAttachInfo.mPendingAnimatingRenderNodes != null) {
+ final int count = mAttachInfo.mPendingAnimatingRenderNodes.size();
+ for (int i = 0; i < count; i++) {
+ mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators();
+ }
+ mAttachInfo.mPendingAnimatingRenderNodes.clear();
+ }
+
+ if (mReportNextDraw) {
+ mReportNextDraw = false;
+
+ // if we're using multi-thread renderer, wait for the window frame draws
+ if (mWindowDrawCountDown != null) {
+ try {
+ mWindowDrawCountDown.await();
+ } catch (InterruptedException e) {
+ Log.e(mTag, "Window redraw count down interrupted!");
+ }
+ mWindowDrawCountDown = null;
+ }
+
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.setStopped(mStopped);
+ }
+
+ if (LOCAL_LOGV) {
+ Log.v(mTag, "FINISHED DRAWING: " + mWindowAttributes.getTitle());
+ }
+
+ if (mSurfaceHolder != null && mSurface.isValid()) {
+ SurfaceCallbackHelper sch = new SurfaceCallbackHelper(this::postDrawFinished);
+ SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
+
+ sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
+ } else if (!usingAsyncReport) {
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.fence();
+ }
+ pendingDrawFinished();
+ }
+ }
+ if (mPerformContentCapture) {
+ performContentCaptureInitialReport();
+ }
+ }
+
+ /**
+ * Checks (and caches) if content capture is enabled for this context.
+ */
+ private boolean isContentCaptureEnabled() {
+ switch (mContentCaptureEnabled) {
+ case CONTENT_CAPTURE_ENABLED_TRUE:
+ return true;
+ case CONTENT_CAPTURE_ENABLED_FALSE:
+ return false;
+ case CONTENT_CAPTURE_ENABLED_NOT_CHECKED:
+ final boolean reallyEnabled = isContentCaptureReallyEnabled();
+ mContentCaptureEnabled = reallyEnabled ? CONTENT_CAPTURE_ENABLED_TRUE
+ : CONTENT_CAPTURE_ENABLED_FALSE;
+ return reallyEnabled;
+ default:
+ Log.w(TAG, "isContentCaptureEnabled(): invalid state " + mContentCaptureEnabled);
+ return false;
+ }
+
+ }
+
+ /**
+ * Checks (without caching) if content capture is enabled for this context.
+ */
+ private boolean isContentCaptureReallyEnabled() {
+ // First check if context supports it, so it saves a service lookup when it doesn't
+ if (mContext.getContentCaptureOptions() == null) return false;
+
+ final ContentCaptureManager ccm = mAttachInfo.getContentCaptureManager(mContext);
+ // Then check if it's enabled in the contex itself.
+ if (ccm == null || !ccm.isContentCaptureEnabled()) return false;
+
+ return true;
+ }
+
+ private void performContentCaptureInitialReport() {
+ mPerformContentCapture = false; // One-time offer!
+ final View rootView = mView;
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.v(mTag, "performContentCaptureInitialReport() on " + rootView);
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchContentCapture() for "
+ + getClass().getSimpleName());
+ }
+ try {
+ if (!isContentCaptureEnabled()) return;
+
+ // Content capture is a go!
+ rootView.dispatchInitialProvideContentCaptureStructure();
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ private void handleContentCaptureFlush() {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.v(mTag, "handleContentCaptureFlush()");
+ }
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "flushContentCapture for "
+ + getClass().getSimpleName());
+ }
+ try {
+ if (!isContentCaptureEnabled()) return;
+
+ final ContentCaptureManager ccm = mAttachInfo.mContentCaptureManager;
+ if (ccm == null) {
+ Log.w(TAG, "No ContentCapture on AttachInfo");
+ return;
+ }
+ ccm.flush(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ private boolean draw(boolean fullRedrawNeeded) {
+ Surface surface = mSurface;
+ if (!surface.isValid()) {
+ return false;
+ }
+
+ if (DEBUG_FPS) {
+ trackFPS();
+ }
+
+ if (!sFirstDrawComplete) {
+ synchronized (sFirstDrawHandlers) {
+ sFirstDrawComplete = true;
+ final int count = sFirstDrawHandlers.size();
+ for (int i = 0; i< count; i++) {
+ mHandler.post(sFirstDrawHandlers.get(i));
+ }
+ }
+ }
+
+ scrollToRectOrFocus(null, false);
+
+ if (mAttachInfo.mViewScrollChanged) {
+ mAttachInfo.mViewScrollChanged = false;
+ mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
+ }
+
+ boolean animating = mScroller != null && mScroller.computeScrollOffset();
+ final int curScrollY;
+ if (animating) {
+ curScrollY = mScroller.getCurrY();
+ } else {
+ curScrollY = mScrollY;
+ }
+ if (mCurScrollY != curScrollY) {
+ mCurScrollY = curScrollY;
+ fullRedrawNeeded = true;
+ if (mView instanceof RootViewSurfaceTaker) {
+ ((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY);
+ }
+ }
+
+ final float appScale = mAttachInfo.mApplicationScale;
+ final boolean scalingRequired = mAttachInfo.mScalingRequired;
+
+ final Rect dirty = mDirty;
+ if (mSurfaceHolder != null) {
+ // The app owns the surface, we won't draw.
+ dirty.setEmpty();
+ if (animating && mScroller != null) {
+ mScroller.abortAnimation();
+ }
+ return false;
+ }
+
+ if (fullRedrawNeeded) {
+ dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
+ }
+
+ if (DEBUG_ORIENTATION || DEBUG_DRAW) {
+ Log.v(mTag, "Draw " + mView + "/"
+ + mWindowAttributes.getTitle()
+ + ": dirty={" + dirty.left + "," + dirty.top
+ + "," + dirty.right + "," + dirty.bottom + "} surface="
+ + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
+ appScale + ", width=" + mWidth + ", height=" + mHeight);
+ }
+
+ mAttachInfo.mTreeObserver.dispatchOnDraw();
+
+ int xOffset = -mCanvasOffsetX;
+ int yOffset = -mCanvasOffsetY + curScrollY;
+ final WindowManager.LayoutParams params = mWindowAttributes;
+ final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
+ if (surfaceInsets != null) {
+ xOffset -= surfaceInsets.left;
+ yOffset -= surfaceInsets.top;
+
+ // Offset dirty rect for surface insets.
+ dirty.offset(surfaceInsets.left, surfaceInsets.top);
+ }
+
+ boolean accessibilityFocusDirty = false;
+ final Drawable drawable = mAttachInfo.mAccessibilityFocusDrawable;
+ if (drawable != null) {
+ final Rect bounds = mAttachInfo.mTmpInvalRect;
+ final boolean hasFocus = getAccessibilityFocusedRect(bounds);
+ if (!hasFocus) {
+ bounds.setEmpty();
+ }
+ if (!bounds.equals(drawable.getBounds())) {
+ accessibilityFocusDirty = true;
+ }
+ }
+
+ mAttachInfo.mDrawingTime =
+ mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
+
+ boolean useAsyncReport = false;
+ if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty || mNextDrawUseBlastSync) {
+ if (isHardwareEnabled()) {
+ // If accessibility focus moved, always invalidate the root.
+ boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested;
+ mInvalidateRootRequested = false;
+
+ // Draw with hardware renderer.
+ mIsAnimating = false;
+
+ if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
+ mHardwareYOffset = yOffset;
+ mHardwareXOffset = xOffset;
+ invalidateRoot = true;
+ }
+
+ if (invalidateRoot) {
+ mAttachInfo.mThreadedRenderer.invalidateRoot();
+ }
+
+ dirty.setEmpty();
+
+ // Stage the content drawn size now. It will be transferred to the renderer
+ // shortly before the draw commands get send to the renderer.
+ final boolean updated = updateContentDrawBounds();
+
+ if (mReportNextDraw) {
+ // report next draw overrides setStopped()
+ // This value is re-sync'd to the value of mStopped
+ // in the handling of mReportNextDraw post-draw.
+ mAttachInfo.mThreadedRenderer.setStopped(false);
+ }
+
+ if (updated) {
+ requestDrawWindow();
+ }
+
+ useAsyncReport = true;
+
+ mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
+ } else {
+ // If we get here with a disabled & requested hardware renderer, something went
+ // wrong (an invalidate posted right before we destroyed the hardware surface
+ // for instance) so we should just bail out. Locking the surface with software
+ // rendering at this point would lock it forever and prevent hardware renderer
+ // from doing its job when it comes back.
+ // Before we request a new frame we must however attempt to reinitiliaze the
+ // hardware renderer if it's in requested state. This would happen after an
+ // eglTerminate() for instance.
+ if (mAttachInfo.mThreadedRenderer != null &&
+ !mAttachInfo.mThreadedRenderer.isEnabled() &&
+ mAttachInfo.mThreadedRenderer.isRequested() &&
+ mSurface.isValid()) {
+
+ try {
+ mAttachInfo.mThreadedRenderer.initializeIfNeeded(
+ mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
+ } catch (OutOfResourcesException e) {
+ handleOutOfResourcesException(e);
+ return false;
+ }
+
+ mFullRedrawNeeded = true;
+ scheduleTraversals();
+ return false;
+ }
+
+ if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
+ scalingRequired, dirty, surfaceInsets)) {
+ return false;
+ }
+ }
+ }
+
+ if (animating) {
+ mFullRedrawNeeded = true;
+ scheduleTraversals();
+ }
+ return useAsyncReport;
+ }
+
+ /**
+ * @return true if drawing was successful, false if an error occurred
+ */
+ private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
+ boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
+
+ // Draw with software renderer.
+ final Canvas canvas;
+
+ // We already have the offset of surfaceInsets in xoff, yoff and dirty region,
+ // therefore we need to add it back when moving the dirty region.
+ int dirtyXOffset = xoff;
+ int dirtyYOffset = yoff;
+ if (surfaceInsets != null) {
+ dirtyXOffset += surfaceInsets.left;
+ dirtyYOffset += surfaceInsets.top;
+ }
+
+ try {
+ dirty.offset(-dirtyXOffset, -dirtyYOffset);
+ final int left = dirty.left;
+ final int top = dirty.top;
+ final int right = dirty.right;
+ final int bottom = dirty.bottom;
+
+ canvas = mSurface.lockCanvas(dirty);
+
+ // TODO: Do this in native
+ canvas.setDensity(mDensity);
+ } catch (Surface.OutOfResourcesException e) {
+ handleOutOfResourcesException(e);
+ return false;
+ } catch (IllegalArgumentException e) {
+ Log.e(mTag, "Could not lock surface", e);
+ // Don't assume this is due to out of memory, it could be
+ // something else, and if it is something else then we could
+ // kill stuff (or ourself) for no reason.
+ mLayoutRequested = true; // ask wm for a new surface next time.
+ return false;
+ } finally {
+ dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value.
+ }
+
+ try {
+ if (DEBUG_ORIENTATION || DEBUG_DRAW) {
+ Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
+ + canvas.getWidth() + ", h=" + canvas.getHeight());
+ //canvas.drawARGB(255, 255, 0, 0);
+ }
+
+ // If this bitmap's format includes an alpha channel, we
+ // need to clear it before drawing so that the child will
+ // properly re-composite its drawing on a transparent
+ // background. This automatically respects the clip/dirty region
+ // or
+ // If we are applying an offset, we need to clear the area
+ // where the offset doesn't appear to avoid having garbage
+ // left in the blank areas.
+ if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+
+ dirty.setEmpty();
+ mIsAnimating = false;
+ mView.mPrivateFlags |= View.PFLAG_DRAWN;
+
+ if (DEBUG_DRAW) {
+ Context cxt = mView.getContext();
+ Log.i(mTag, "Drawing: package:" + cxt.getPackageName() +
+ ", metrics=" + cxt.getResources().getDisplayMetrics() +
+ ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
+ }
+ canvas.translate(-xoff, -yoff);
+ if (mTranslator != null) {
+ mTranslator.translateCanvas(canvas);
+ }
+ canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
+
+ mView.draw(canvas);
+
+ drawAccessibilityFocusedDrawableIfNeeded(canvas);
+ } finally {
+ try {
+ surface.unlockCanvasAndPost(canvas);
+ } catch (IllegalArgumentException e) {
+ Log.e(mTag, "Could not unlock surface", e);
+ mLayoutRequested = true; // ask wm for a new surface next time.
+ //noinspection ReturnInsideFinallyBlock
+ return false;
+ }
+
+ if (LOCAL_LOGV) {
+ Log.v(mTag, "Surface " + surface + " unlockCanvasAndPost");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * We want to draw a highlight around the current accessibility focused.
+ * Since adding a style for all possible view is not a viable option we
+ * have this specialized drawing method.
+ *
+ * Note: We are doing this here to be able to draw the highlight for
+ * virtual views in addition to real ones.
+ *
+ * @param canvas The canvas on which to draw.
+ */
+ private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) {
+ final Rect bounds = mAttachInfo.mTmpInvalRect;
+ if (getAccessibilityFocusedRect(bounds)) {
+ final Drawable drawable = getAccessibilityFocusedDrawable();
+ if (drawable != null) {
+ drawable.setBounds(bounds);
+ drawable.draw(canvas);
+ }
+ } else if (mAttachInfo.mAccessibilityFocusDrawable != null) {
+ mAttachInfo.mAccessibilityFocusDrawable.setBounds(0, 0, 0, 0);
+ }
+ }
+
+ private boolean getAccessibilityFocusedRect(Rect bounds) {
+ final AccessibilityManager manager = AccessibilityManager.getInstance(mView.mContext);
+ if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) {
+ return false;
+ }
+
+ final View host = mAccessibilityFocusedHost;
+ if (host == null || host.mAttachInfo == null) {
+ return false;
+ }
+
+ final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();
+ if (provider == null) {
+ host.getBoundsOnScreen(bounds, true);
+ } else if (mAccessibilityFocusedVirtualView != null) {
+ mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds);
+ } else {
+ return false;
+ }
+
+ // Transform the rect into window-relative coordinates.
+ final AttachInfo attachInfo = mAttachInfo;
+ bounds.offset(0, attachInfo.mViewRootImpl.mScrollY);
+ bounds.offset(-attachInfo.mWindowLeft, -attachInfo.mWindowTop);
+ if (!bounds.intersect(0, 0, attachInfo.mViewRootImpl.mWidth,
+ attachInfo.mViewRootImpl.mHeight)) {
+ // If no intersection, set bounds to empty.
+ bounds.setEmpty();
+ }
+ return !bounds.isEmpty();
+ }
+
+ private Drawable getAccessibilityFocusedDrawable() {
+ // Lazily load the accessibility focus drawable.
+ if (mAttachInfo.mAccessibilityFocusDrawable == null) {
+ final TypedValue value = new TypedValue();
+ final boolean resolved = mView.mContext.getTheme().resolveAttribute(
+ R.attr.accessibilityFocusedDrawable, value, true);
+ if (resolved) {
+ mAttachInfo.mAccessibilityFocusDrawable =
+ mView.mContext.getDrawable(value.resourceId);
+ }
+ }
+ // Sets the focus appearance data into the accessibility focus drawable.
+ if (mAttachInfo.mAccessibilityFocusDrawable instanceof GradientDrawable) {
+ final GradientDrawable drawable =
+ (GradientDrawable) mAttachInfo.mAccessibilityFocusDrawable;
+ drawable.setStroke(mAccessibilityManager.getAccessibilityFocusStrokeWidth(),
+ mAccessibilityManager.getAccessibilityFocusColor());
+ }
+
+ return mAttachInfo.mAccessibilityFocusDrawable;
+ }
+
+ void updateSystemGestureExclusionRectsForView(View view) {
+ mGestureExclusionTracker.updateRectsForView(view);
+ mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED);
+ }
+
+ void systemGestureExclusionChanged() {
+ final List<Rect> rectsForWindowManager = mGestureExclusionTracker.computeChangedRects();
+ if (rectsForWindowManager != null && mView != null) {
+ try {
+ mWindowSession.reportSystemGestureExclusionChanged(mWindow, rectsForWindowManager);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mAttachInfo.mTreeObserver
+ .dispatchOnSystemGestureExclusionRectsChanged(rectsForWindowManager);
+ }
+ }
+
+ void updateLocationInParentDisplay(int x, int y) {
+ if (mAttachInfo != null
+ && !mAttachInfo.mLocationInParentDisplay.equals(x, y)) {
+ mAttachInfo.mLocationInParentDisplay.set(x, y);
+ }
+ }
+
+ /**
+ * Set the root-level system gesture exclusion rects. These are added to those provided by
+ * the root's view hierarchy.
+ */
+ public void setRootSystemGestureExclusionRects(@NonNull List<Rect> rects) {
+ mGestureExclusionTracker.setRootSystemGestureExclusionRects(rects);
+ mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED);
+ }
+
+ /**
+ * Returns the root-level system gesture exclusion rects. These do not include those provided by
+ * the root's view hierarchy.
+ */
+ @NonNull
+ public List<Rect> getRootSystemGestureExclusionRects() {
+ return mGestureExclusionTracker.getRootSystemGestureExclusionRects();
+ }
+
+ /**
+ * Requests that the root render node is invalidated next time we perform a draw, such that
+ * {@link WindowCallbacks#onPostDraw} gets called.
+ */
+ public void requestInvalidateRootRenderNode() {
+ mInvalidateRootRequested = true;
+ }
+
+ boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
+ final Rect ci = mAttachInfo.mContentInsets;
+ final Rect vi = mAttachInfo.mVisibleInsets;
+ int scrollY = 0;
+ boolean handled = false;
+
+ if (vi.left > ci.left || vi.top > ci.top
+ || vi.right > ci.right || vi.bottom > ci.bottom) {
+ // We'll assume that we aren't going to change the scroll
+ // offset, since we want to avoid that unless it is actually
+ // going to make the focus visible... otherwise we scroll
+ // all over the place.
+ scrollY = mScrollY;
+ // We can be called for two different situations: during a draw,
+ // to update the scroll position if the focus has changed (in which
+ // case 'rectangle' is null), or in response to a
+ // requestChildRectangleOnScreen() call (in which case 'rectangle'
+ // is non-null and we just want to scroll to whatever that
+ // rectangle is).
+ final View focus = mView.findFocus();
+ if (focus == null) {
+ return false;
+ }
+ View lastScrolledFocus = (mLastScrolledFocus != null) ? mLastScrolledFocus.get() : null;
+ if (focus != lastScrolledFocus) {
+ // If the focus has changed, then ignore any requests to scroll
+ // to a rectangle; first we want to make sure the entire focus
+ // view is visible.
+ rectangle = null;
+ }
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Eval scroll: focus=" + focus
+ + " rectangle=" + rectangle + " ci=" + ci
+ + " vi=" + vi);
+ if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) {
+ // Optimization: if the focus hasn't changed since last
+ // time, and no layout has happened, then just leave things
+ // as they are.
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Keeping scroll y="
+ + mScrollY + " vi=" + vi.toShortString());
+ } else {
+ // We need to determine if the currently focused view is
+ // within the visible part of the window and, if not, apply
+ // a pan so it can be seen.
+ mLastScrolledFocus = new WeakReference<View>(focus);
+ mScrollMayChange = false;
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Need to scroll?");
+ // Try to find the rectangle from the focus view.
+ if (focus.getGlobalVisibleRect(mVisRect, null)) {
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Root w="
+ + mView.getWidth() + " h=" + mView.getHeight()
+ + " ci=" + ci.toShortString()
+ + " vi=" + vi.toShortString());
+ if (rectangle == null) {
+ focus.getFocusedRect(mTempRect);
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Focus " + focus
+ + ": focusRect=" + mTempRect.toShortString());
+ if (mView instanceof ViewGroup) {
+ ((ViewGroup) mView).offsetDescendantRectToMyCoords(
+ focus, mTempRect);
+ }
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag,
+ "Focus in window: focusRect="
+ + mTempRect.toShortString()
+ + " visRect=" + mVisRect.toShortString());
+ } else {
+ mTempRect.set(rectangle);
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag,
+ "Request scroll to rect: "
+ + mTempRect.toShortString()
+ + " visRect=" + mVisRect.toShortString());
+ }
+ if (mTempRect.intersect(mVisRect)) {
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag,
+ "Focus window visible rect: "
+ + mTempRect.toShortString());
+ if (mTempRect.height() >
+ (mView.getHeight()-vi.top-vi.bottom)) {
+ // If the focus simply is not going to fit, then
+ // best is probably just to leave things as-is.
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag,
+ "Too tall; leaving scrollY=" + scrollY);
+ }
+ // Next, check whether top or bottom is covered based on the non-scrolled
+ // position, and calculate new scrollY (or set it to 0).
+ // We can't keep using mScrollY here. For example mScrollY is non-zero
+ // due to IME, then IME goes away. The current value of mScrollY leaves top
+ // and bottom both visible, but we still need to scroll it back to 0.
+ else if (mTempRect.top < vi.top) {
+ scrollY = mTempRect.top - vi.top;
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag,
+ "Top covered; scrollY=" + scrollY);
+ } else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {
+ scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag,
+ "Bottom covered; scrollY=" + scrollY);
+ } else {
+ scrollY = 0;
+ }
+ handled = true;
+ }
+ }
+ }
+ }
+
+ if (scrollY != mScrollY) {
+ if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Pan scroll changed: old="
+ + mScrollY + " , new=" + scrollY);
+ if (!immediate) {
+ if (mScroller == null) {
+ mScroller = new Scroller(mView.getContext());
+ }
+ mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);
+ } else if (mScroller != null) {
+ mScroller.abortAnimation();
+ }
+ mScrollY = scrollY;
+ }
+
+ return handled;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public View getAccessibilityFocusedHost() {
+ return mAccessibilityFocusedHost;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public AccessibilityNodeInfo getAccessibilityFocusedVirtualView() {
+ return mAccessibilityFocusedVirtualView;
+ }
+
+ void setAccessibilityFocus(View view, AccessibilityNodeInfo node) {
+ // If we have a virtual view with accessibility focus we need
+ // to clear the focus and invalidate the virtual view bounds.
+ if (mAccessibilityFocusedVirtualView != null) {
+
+ AccessibilityNodeInfo focusNode = mAccessibilityFocusedVirtualView;
+ View focusHost = mAccessibilityFocusedHost;
+
+ // Wipe the state of the current accessibility focus since
+ // the call into the provider to clear accessibility focus
+ // will fire an accessibility event which will end up calling
+ // this method and we want to have clean state when this
+ // invocation happens.
+ mAccessibilityFocusedHost = null;
+ mAccessibilityFocusedVirtualView = null;
+
+ // Clear accessibility focus on the host after clearing state since
+ // this method may be reentrant.
+ focusHost.clearAccessibilityFocusNoCallbacks(
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+
+ AccessibilityNodeProvider provider = focusHost.getAccessibilityNodeProvider();
+ if (provider != null) {
+ // Invalidate the area of the cleared accessibility focus.
+ focusNode.getBoundsInParent(mTempRect);
+ focusHost.invalidate(mTempRect);
+ // Clear accessibility focus in the virtual node.
+ final int virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId(
+ focusNode.getSourceNodeId());
+ provider.performAction(virtualNodeId,
+ AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
+ }
+ focusNode.recycle();
+ }
+ if ((mAccessibilityFocusedHost != null) && (mAccessibilityFocusedHost != view)) {
+ // Clear accessibility focus in the view.
+ mAccessibilityFocusedHost.clearAccessibilityFocusNoCallbacks(
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ // Set the new focus host and node.
+ mAccessibilityFocusedHost = view;
+ mAccessibilityFocusedVirtualView = node;
+
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.invalidateRoot();
+ }
+ }
+
+ boolean hasPointerCapture() {
+ return mPointerCapture;
+ }
+
+ void requestPointerCapture(boolean enabled) {
+ if (mPointerCapture == enabled) {
+ return;
+ }
+ final IBinder inputToken = getInputToken();
+ if (inputToken == null) {
+ Log.e(mTag, "No input channel to request Pointer Capture.");
+ return;
+ }
+ InputManager.getInstance().requestPointerCapture(inputToken, enabled);
+ }
+
+ private void handlePointerCaptureChanged(boolean hasCapture) {
+ if (mPointerCapture == hasCapture) {
+ return;
+ }
+ mPointerCapture = hasCapture;
+ if (mView != null) {
+ mView.dispatchPointerCaptureChanged(hasCapture);
+ }
+ }
+
+ private void updateColorModeIfNeeded(int colorMode) {
+ if (mAttachInfo.mThreadedRenderer == null) {
+ return;
+ }
+ // TODO: Centralize this sanitization? Why do we let setting bad modes?
+ // Alternatively, can we just let HWUI figure it out? Do we need to care here?
+ if (!getConfiguration().isScreenWideColorGamut()) {
+ colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
+ }
+ mAttachInfo.mThreadedRenderer.setColorMode(colorMode);
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (DEBUG_INPUT_RESIZE) {
+ Log.v(mTag, "Request child focus: focus now " + focused);
+ }
+ checkThread();
+ scheduleTraversals();
+ }
+
+ @Override
+ public void clearChildFocus(View child) {
+ if (DEBUG_INPUT_RESIZE) {
+ Log.v(mTag, "Clearing child focus");
+ }
+ checkThread();
+ scheduleTraversals();
+ }
+
+ @Override
+ public ViewParent getParentForAccessibility() {
+ return null;
+ }
+
+ @Override
+ public void focusableViewAvailable(View v) {
+ checkThread();
+ if (mView != null) {
+ if (!mView.hasFocus()) {
+ if (sAlwaysAssignFocus || !mAttachInfo.mInTouchMode) {
+ v.requestFocus();
+ }
+ } else {
+ // the one case where will transfer focus away from the current one
+ // is if the current view is a view group that prefers to give focus
+ // to its children first AND the view is a descendant of it.
+ View focused = mView.findFocus();
+ if (focused instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) focused;
+ if (group.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
+ && isViewDescendantOf(v, focused)) {
+ v.requestFocus();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void recomputeViewAttributes(View child) {
+ checkThread();
+ if (mView == child) {
+ mAttachInfo.mRecomputeGlobalAttributes = true;
+ if (!mWillDrawSoon) {
+ scheduleTraversals();
+ }
+ }
+ }
+
+ void dispatchDetachedFromWindow() {
+ // Make sure we free-up insets resources if view never received onWindowFocusLost()
+ // because of a die-signal
+ mInsetsController.onWindowFocusLost();
+ mFirstInputStage.onDetachedFromWindow();
+ if (mView != null && mView.mAttachInfo != null) {
+ mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
+ mView.dispatchDetachedFromWindow();
+ }
+
+ mAccessibilityInteractionConnectionManager.ensureNoConnection();
+ mAccessibilityManager.removeAccessibilityStateChangeListener(
+ mAccessibilityInteractionConnectionManager);
+ mAccessibilityManager.removeHighTextContrastStateChangeListener(
+ mHighContrastTextManager);
+ removeSendWindowContentChangedCallback();
+
+ destroyHardwareRenderer();
+
+ setAccessibilityFocus(null, null);
+
+ mInsetsController.cancelExistingAnimations();
+
+ mView.assignParent(null);
+ mView = null;
+ mAttachInfo.mRootView = null;
+
+ destroySurface();
+
+ if (mInputQueueCallback != null && mInputQueue != null) {
+ mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
+ mInputQueue.dispose();
+ mInputQueueCallback = null;
+ mInputQueue = null;
+ }
+ try {
+ mWindowSession.remove(mWindow);
+ } catch (RemoteException e) {
+ }
+ // Dispose receiver would dispose client InputChannel, too. That could send out a socket
+ // broken event, so we need to unregister the server InputChannel when removing window to
+ // prevent server side receive the event and prompt error.
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+
+ mDisplayManager.unregisterDisplayListener(mDisplayListener);
+
+ unscheduleTraversals();
+ }
+
+ /**
+ * Notifies all callbacks that configuration and/or display has changed and updates internal
+ * state.
+ * @param mergedConfiguration New global and override config in {@link MergedConfiguration}
+ * container.
+ * @param force Flag indicating if we should force apply the config.
+ * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} if not
+ * changed.
+ */
+ private void performConfigurationChange(MergedConfiguration mergedConfiguration, boolean force,
+ int newDisplayId) {
+ if (mergedConfiguration == null) {
+ throw new IllegalArgumentException("No merged config provided.");
+ }
+
+ Configuration globalConfig = mergedConfiguration.getGlobalConfiguration();
+ final Configuration overrideConfig = mergedConfiguration.getOverrideConfiguration();
+ if (DEBUG_CONFIGURATION) Log.v(mTag,
+ "Applying new config to window " + mWindowAttributes.getTitle()
+ + ", globalConfig: " + globalConfig
+ + ", overrideConfig: " + overrideConfig);
+
+ final CompatibilityInfo ci = mDisplay.getDisplayAdjustments().getCompatibilityInfo();
+ if (!ci.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) {
+ globalConfig = new Configuration(globalConfig);
+ ci.applyToConfiguration(mNoncompatDensity, globalConfig);
+ }
+
+ synchronized (sConfigCallbacks) {
+ for (int i=sConfigCallbacks.size()-1; i>=0; i--) {
+ sConfigCallbacks.get(i).onConfigurationChanged(globalConfig);
+ }
+ }
+
+ mLastReportedMergedConfiguration.setConfiguration(globalConfig, overrideConfig);
+
+ mForceNextConfigUpdate = force;
+ if (mActivityConfigCallback != null) {
+ // An activity callback is set - notify it about override configuration update.
+ // This basically initiates a round trip to ActivityThread and back, which will ensure
+ // that corresponding activity and resources are updated before updating inner state of
+ // ViewRootImpl. Eventually it will call #updateConfiguration().
+ mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId);
+ } else {
+ // There is no activity callback - update the configuration right away.
+ updateConfiguration(newDisplayId);
+ }
+ mForceNextConfigUpdate = false;
+ }
+
+ /**
+ * Update display and views if last applied merged configuration changed.
+ * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise.
+ */
+ public void updateConfiguration(int newDisplayId) {
+ if (mView == null) {
+ return;
+ }
+
+ // At this point the resources have been updated to
+ // have the most recent config, whatever that is. Use
+ // the one in them which may be newer.
+ final Resources localResources = mView.getResources();
+ final Configuration config = localResources.getConfiguration();
+
+ // Handle move to display.
+ if (newDisplayId != INVALID_DISPLAY) {
+ onMovedToDisplay(newDisplayId, config);
+ }
+
+ // Handle configuration change.
+ if (mForceNextConfigUpdate || mLastConfigurationFromResources.diff(config) != 0) {
+ // Update the display with new DisplayAdjustments.
+ updateInternalDisplay(mDisplay.getDisplayId(), localResources);
+
+ final int lastLayoutDirection = mLastConfigurationFromResources.getLayoutDirection();
+ final int currentLayoutDirection = config.getLayoutDirection();
+ mLastConfigurationFromResources.setTo(config);
+ if (lastLayoutDirection != currentLayoutDirection
+ && mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
+ mView.setLayoutDirection(currentLayoutDirection);
+ }
+ mView.dispatchConfigurationChanged(config);
+
+ // We could have gotten this {@link Configuration} update after we called
+ // {@link #performTraversals} with an older {@link Configuration}. As a result, our
+ // window frame may be stale. We must ensure the next pass of {@link #performTraversals}
+ // catches this.
+ mForceNextWindowRelayout = true;
+ requestLayout();
+ }
+
+ updateForceDarkMode();
+ }
+
+ /**
+ * Return true if child is an ancestor of parent, (or equal to the parent).
+ */
+ public static boolean isViewDescendantOf(View child, View parent) {
+ if (child == parent) {
+ return true;
+ }
+
+ final ViewParent theParent = child.getParent();
+ return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+ }
+
+ private static void forceLayout(View view) {
+ view.forceLayout();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ forceLayout(group.getChildAt(i));
+ }
+ }
+ }
+
+ private static final int MSG_INVALIDATE = 1;
+ private static final int MSG_INVALIDATE_RECT = 2;
+ private static final int MSG_DIE = 3;
+ private static final int MSG_RESIZED = 4;
+ private static final int MSG_RESIZED_REPORT = 5;
+ private static final int MSG_WINDOW_FOCUS_CHANGED = 6;
+ private static final int MSG_DISPATCH_INPUT_EVENT = 7;
+ private static final int MSG_DISPATCH_APP_VISIBILITY = 8;
+ private static final int MSG_DISPATCH_GET_NEW_SURFACE = 9;
+ private static final int MSG_DISPATCH_KEY_FROM_IME = 11;
+ private static final int MSG_DISPATCH_KEY_FROM_AUTOFILL = 12;
+ private static final int MSG_CHECK_FOCUS = 13;
+ private static final int MSG_CLOSE_SYSTEM_DIALOGS = 14;
+ private static final int MSG_DISPATCH_DRAG_EVENT = 15;
+ private static final int MSG_DISPATCH_DRAG_LOCATION_EVENT = 16;
+ private static final int MSG_DISPATCH_SYSTEM_UI_VISIBILITY = 17;
+ private static final int MSG_UPDATE_CONFIGURATION = 18;
+ private static final int MSG_PROCESS_INPUT_EVENTS = 19;
+ private static final int MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST = 21;
+ private static final int MSG_INVALIDATE_WORLD = 22;
+ private static final int MSG_WINDOW_MOVED = 23;
+ private static final int MSG_SYNTHESIZE_INPUT_EVENT = 24;
+ private static final int MSG_DISPATCH_WINDOW_SHOWN = 25;
+ private static final int MSG_REQUEST_KEYBOARD_SHORTCUTS = 26;
+ private static final int MSG_UPDATE_POINTER_ICON = 27;
+ private static final int MSG_POINTER_CAPTURE_CHANGED = 28;
+ private static final int MSG_DRAW_FINISHED = 29;
+ private static final int MSG_INSETS_CHANGED = 30;
+ private static final int MSG_INSETS_CONTROL_CHANGED = 31;
+ private static final int MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED = 32;
+ private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33;
+ private static final int MSG_SHOW_INSETS = 34;
+ private static final int MSG_HIDE_INSETS = 35;
+ private static final int MSG_REQUEST_SCROLL_CAPTURE = 36;
+
+
+ final class ViewRootHandler extends Handler {
+ @Override
+ public String getMessageName(Message message) {
+ switch (message.what) {
+ case MSG_INVALIDATE:
+ return "MSG_INVALIDATE";
+ case MSG_INVALIDATE_RECT:
+ return "MSG_INVALIDATE_RECT";
+ case MSG_DIE:
+ return "MSG_DIE";
+ case MSG_RESIZED:
+ return "MSG_RESIZED";
+ case MSG_RESIZED_REPORT:
+ return "MSG_RESIZED_REPORT";
+ case MSG_WINDOW_FOCUS_CHANGED:
+ return "MSG_WINDOW_FOCUS_CHANGED";
+ case MSG_DISPATCH_INPUT_EVENT:
+ return "MSG_DISPATCH_INPUT_EVENT";
+ case MSG_DISPATCH_APP_VISIBILITY:
+ return "MSG_DISPATCH_APP_VISIBILITY";
+ case MSG_DISPATCH_GET_NEW_SURFACE:
+ return "MSG_DISPATCH_GET_NEW_SURFACE";
+ case MSG_DISPATCH_KEY_FROM_IME:
+ return "MSG_DISPATCH_KEY_FROM_IME";
+ case MSG_DISPATCH_KEY_FROM_AUTOFILL:
+ return "MSG_DISPATCH_KEY_FROM_AUTOFILL";
+ case MSG_CHECK_FOCUS:
+ return "MSG_CHECK_FOCUS";
+ case MSG_CLOSE_SYSTEM_DIALOGS:
+ return "MSG_CLOSE_SYSTEM_DIALOGS";
+ case MSG_DISPATCH_DRAG_EVENT:
+ return "MSG_DISPATCH_DRAG_EVENT";
+ case MSG_DISPATCH_DRAG_LOCATION_EVENT:
+ return "MSG_DISPATCH_DRAG_LOCATION_EVENT";
+ case MSG_DISPATCH_SYSTEM_UI_VISIBILITY:
+ return "MSG_DISPATCH_SYSTEM_UI_VISIBILITY";
+ case MSG_UPDATE_CONFIGURATION:
+ return "MSG_UPDATE_CONFIGURATION";
+ case MSG_PROCESS_INPUT_EVENTS:
+ return "MSG_PROCESS_INPUT_EVENTS";
+ case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST:
+ return "MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST";
+ case MSG_WINDOW_MOVED:
+ return "MSG_WINDOW_MOVED";
+ case MSG_SYNTHESIZE_INPUT_EVENT:
+ return "MSG_SYNTHESIZE_INPUT_EVENT";
+ case MSG_DISPATCH_WINDOW_SHOWN:
+ return "MSG_DISPATCH_WINDOW_SHOWN";
+ case MSG_UPDATE_POINTER_ICON:
+ return "MSG_UPDATE_POINTER_ICON";
+ case MSG_POINTER_CAPTURE_CHANGED:
+ return "MSG_POINTER_CAPTURE_CHANGED";
+ case MSG_DRAW_FINISHED:
+ return "MSG_DRAW_FINISHED";
+ case MSG_INSETS_CHANGED:
+ return "MSG_INSETS_CHANGED";
+ case MSG_INSETS_CONTROL_CHANGED:
+ return "MSG_INSETS_CONTROL_CHANGED";
+ case MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED:
+ return "MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED";
+ case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED:
+ return "MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED";
+ case MSG_SHOW_INSETS:
+ return "MSG_SHOW_INSETS";
+ case MSG_HIDE_INSETS:
+ return "MSG_HIDE_INSETS";
+ }
+ return super.getMessageName(message);
+ }
+
+ @Override
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
+ if (msg.what == MSG_REQUEST_KEYBOARD_SHORTCUTS && msg.obj == null) {
+ // Debugging for b/27963013
+ throw new NullPointerException(
+ "Attempted to call MSG_REQUEST_KEYBOARD_SHORTCUTS with null receiver:");
+ }
+ return super.sendMessageAtTime(msg, uptimeMillis);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, getMessageName(msg));
+ }
+ try {
+ handleMessageImpl(msg);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ private void handleMessageImpl(Message msg) {
+ switch (msg.what) {
+ case MSG_INVALIDATE:
+ ((View) msg.obj).invalidate();
+ break;
+ case MSG_INVALIDATE_RECT:
+ final View.AttachInfo.InvalidateInfo info =
+ (View.AttachInfo.InvalidateInfo) msg.obj;
+ info.target.invalidate(info.left, info.top, info.right, info.bottom);
+ info.recycle();
+ break;
+ case MSG_PROCESS_INPUT_EVENTS:
+ mProcessInputEventsScheduled = false;
+ doProcessInputEvents();
+ break;
+ case MSG_DISPATCH_APP_VISIBILITY:
+ handleAppVisibility(msg.arg1 != 0);
+ break;
+ case MSG_DISPATCH_GET_NEW_SURFACE:
+ handleGetNewSurface();
+ break;
+ case MSG_RESIZED:
+ case MSG_RESIZED_REPORT: {
+ mWillMove = false;
+ mWillResize = false;
+ final SomeArgs args = (SomeArgs) msg.obj;
+ handleResized(msg.what, args);
+ args.recycle();
+ break;
+ }
+ case MSG_INSETS_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mWillMove = args.argi1 == 1;
+ mWillResize = args.argi2 == 1;
+ mInsetsController.onStateChanged((InsetsState) args.arg1);
+ args.recycle();
+ break;
+ }
+ case MSG_INSETS_CONTROL_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mWillMove = args.argi1 == 1;
+ mWillResize = args.argi2 == 1;
+
+ // Deliver state change before control change, such that:
+ // a) When gaining control, controller can compare with server state to evaluate
+ // whether it needs to run animation.
+ // b) When loosing control, controller can restore server state by taking last
+ // dispatched state as truth.
+ mInsetsController.onStateChanged((InsetsState) args.arg1);
+ InsetsSourceControl[] controls = (InsetsSourceControl[]) args.arg2;
+ if (mAdded) {
+ mInsetsController.onControlsChanged(controls);
+ } else if (controls != null) {
+ for (InsetsSourceControl control : controls) {
+ if (control != null) {
+ control.release(SurfaceControl::release);
+ }
+ }
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_SHOW_INSETS: {
+ if (mView == null) {
+ Log.e(TAG,
+ String.format("Calling showInsets(%d,%b) on window that no longer"
+ + " has views.", msg.arg1, msg.arg2 == 1));
+ }
+ clearLowProfileModeIfNeeded(msg.arg1, msg.arg2 == 1);
+ mInsetsController.show(msg.arg1, msg.arg2 == 1);
+ break;
+ }
+ case MSG_HIDE_INSETS: {
+ mInsetsController.hide(msg.arg1, msg.arg2 == 1);
+ break;
+ }
+ case MSG_WINDOW_MOVED:
+ mWillMove = false;
+ if (mAdded) {
+ final int w = mWinFrame.width();
+ final int h = mWinFrame.height();
+ final int l = msg.arg1;
+ final int t = msg.arg2;
+ mTmpFrames.frame.left = l;
+ mTmpFrames.frame.right = l + w;
+ mTmpFrames.frame.top = t;
+ mTmpFrames.frame.bottom = t + h;
+ setFrame(mTmpFrames.frame);
+
+ mPendingBackDropFrame.set(mWinFrame);
+ maybeHandleWindowMove(mWinFrame);
+ }
+ break;
+ case MSG_WINDOW_FOCUS_CHANGED: {
+ handleWindowFocusChanged();
+ } break;
+ case MSG_DIE:
+ doDie();
+ break;
+ case MSG_DISPATCH_INPUT_EVENT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ InputEvent event = (InputEvent) args.arg1;
+ InputEventReceiver receiver = (InputEventReceiver) args.arg2;
+ enqueueInputEvent(event, receiver, 0, true);
+ args.recycle();
+ } break;
+ case MSG_SYNTHESIZE_INPUT_EVENT: {
+ InputEvent event = (InputEvent) msg.obj;
+ enqueueInputEvent(event, null, QueuedInputEvent.FLAG_UNHANDLED, true);
+ } break;
+ case MSG_DISPATCH_KEY_FROM_IME: {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "Dispatching key " + msg.obj + " from IME to " + mView);
+ }
+ KeyEvent event = (KeyEvent) msg.obj;
+ if ((event.getFlags() & KeyEvent.FLAG_FROM_SYSTEM) != 0) {
+ // The IME is trying to say this event is from the
+ // system! Bad bad bad!
+ //noinspection UnusedAssignment
+ event = KeyEvent.changeFlags(event,
+ event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM);
+ }
+ enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME, true);
+ } break;
+ case MSG_DISPATCH_KEY_FROM_AUTOFILL: {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "Dispatching key " + msg.obj + " from Autofill to " + mView);
+ }
+ KeyEvent event = (KeyEvent) msg.obj;
+ enqueueInputEvent(event, null, 0, true);
+ } break;
+ case MSG_CHECK_FOCUS: {
+ getImeFocusController().checkFocus(false, true);
+ } break;
+ case MSG_CLOSE_SYSTEM_DIALOGS: {
+ if (mView != null) {
+ mView.onCloseSystemDialogs((String) msg.obj);
+ }
+ } break;
+ case MSG_DISPATCH_DRAG_EVENT: {
+ } // fall through
+ case MSG_DISPATCH_DRAG_LOCATION_EVENT: {
+ DragEvent event = (DragEvent) msg.obj;
+ // only present when this app called startDrag()
+ event.mLocalState = mLocalDragState;
+ handleDragEvent(event);
+ } break;
+ case MSG_DISPATCH_SYSTEM_UI_VISIBILITY: {
+ handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo) msg.obj);
+ } break;
+ case MSG_UPDATE_CONFIGURATION: {
+ Configuration config = (Configuration) msg.obj;
+ if (config.isOtherSeqNewer(
+ mLastReportedMergedConfiguration.getMergedConfiguration())) {
+ // If we already have a newer merged config applied - use its global part.
+ config = mLastReportedMergedConfiguration.getGlobalConfiguration();
+ }
+
+ // Use the newer global config and last reported override config.
+ mPendingMergedConfiguration.setConfiguration(config,
+ mLastReportedMergedConfiguration.getOverrideConfiguration());
+
+ performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
+ false /* force */, INVALID_DISPLAY /* same display */);
+ } break;
+ case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: {
+ setAccessibilityFocus(null, null);
+ } break;
+ case MSG_INVALIDATE_WORLD: {
+ if (mView != null) {
+ invalidateWorld(mView);
+ }
+ } break;
+ case MSG_DISPATCH_WINDOW_SHOWN: {
+ handleDispatchWindowShown();
+ } break;
+ case MSG_REQUEST_KEYBOARD_SHORTCUTS: {
+ final IResultReceiver receiver = (IResultReceiver) msg.obj;
+ final int deviceId = msg.arg1;
+ handleRequestKeyboardShortcuts(receiver, deviceId);
+ } break;
+ case MSG_UPDATE_POINTER_ICON: {
+ MotionEvent event = (MotionEvent) msg.obj;
+ resetPointerIcon(event);
+ } break;
+ case MSG_POINTER_CAPTURE_CHANGED: {
+ final boolean hasCapture = msg.arg1 != 0;
+ handlePointerCaptureChanged(hasCapture);
+ } break;
+ case MSG_DRAW_FINISHED: {
+ pendingDrawFinished();
+ } break;
+ case MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED: {
+ systemGestureExclusionChanged();
+ } break;
+ case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: {
+ updateLocationInParentDisplay(msg.arg1, msg.arg2);
+ } break;
+ case MSG_REQUEST_SCROLL_CAPTURE:
+ handleScrollCaptureRequest((IScrollCaptureResponseListener) msg.obj);
+ break;
+ }
+ }
+ }
+
+ final ViewRootHandler mHandler = new ViewRootHandler();
+
+ /**
+ * Something in the current window tells us we need to change the touch mode. For
+ * example, we are not in touch mode, and the user touches the screen.
+ *
+ * If the touch mode has changed, tell the window manager, and handle it locally.
+ *
+ * @param inTouchMode Whether we want to be in touch mode.
+ * @return True if the touch mode changed and focus changed was changed as a result
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ boolean ensureTouchMode(boolean inTouchMode) {
+ if (DBG) Log.d("touchmode", "ensureTouchMode(" + inTouchMode + "), current "
+ + "touch mode is " + mAttachInfo.mInTouchMode);
+ if (mAttachInfo.mInTouchMode == inTouchMode) return false;
+
+ // tell the window manager
+ try {
+ mWindowSession.setInTouchMode(inTouchMode);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+
+ // handle the change
+ return ensureTouchModeLocally(inTouchMode);
+ }
+
+ /**
+ * Ensure that the touch mode for this window is set, and if it is changing,
+ * take the appropriate action.
+ * @param inTouchMode Whether we want to be in touch mode.
+ * @return True if the touch mode changed and focus changed was changed as a result
+ */
+ private boolean ensureTouchModeLocally(boolean inTouchMode) {
+ if (DBG) Log.d("touchmode", "ensureTouchModeLocally(" + inTouchMode + "), current "
+ + "touch mode is " + mAttachInfo.mInTouchMode);
+
+ if (mAttachInfo.mInTouchMode == inTouchMode) return false;
+
+ mAttachInfo.mInTouchMode = inTouchMode;
+ mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);
+
+ return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
+ }
+
+ private boolean enterTouchMode() {
+ if (mView != null && mView.hasFocus()) {
+ // note: not relying on mFocusedView here because this could
+ // be when the window is first being added, and mFocused isn't
+ // set yet.
+ final View focused = mView.findFocus();
+ if (focused != null && !focused.isFocusableInTouchMode()) {
+ final ViewGroup ancestorToTakeFocus = findAncestorToTakeFocusInTouchMode(focused);
+ if (ancestorToTakeFocus != null) {
+ // there is an ancestor that wants focus after its
+ // descendants that is focusable in touch mode.. give it
+ // focus
+ return ancestorToTakeFocus.requestFocus();
+ } else {
+ // There's nothing to focus. Clear and propagate through the
+ // hierarchy, but don't attempt to place new focus.
+ focused.clearFocusInternal(null, true, false);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Find an ancestor of focused that wants focus after its descendants and is
+ * focusable in touch mode.
+ * @param focused The currently focused view.
+ * @return An appropriate view, or null if no such view exists.
+ */
+ private static ViewGroup findAncestorToTakeFocusInTouchMode(View focused) {
+ ViewParent parent = focused.getParent();
+ while (parent instanceof ViewGroup) {
+ final ViewGroup vgParent = (ViewGroup) parent;
+ if (vgParent.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
+ && vgParent.isFocusableInTouchMode()) {
+ return vgParent;
+ }
+ if (vgParent.isRootNamespace()) {
+ return null;
+ } else {
+ parent = vgParent.getParent();
+ }
+ }
+ return null;
+ }
+
+ private boolean leaveTouchMode() {
+ if (mView != null) {
+ if (mView.hasFocus()) {
+ View focusedView = mView.findFocus();
+ if (!(focusedView instanceof ViewGroup)) {
+ // some view has focus, let it keep it
+ return false;
+ } else if (((ViewGroup) focusedView).getDescendantFocusability() !=
+ ViewGroup.FOCUS_AFTER_DESCENDANTS) {
+ // some view group has focus, and doesn't prefer its children
+ // over itself for focus, so let them keep it.
+ return false;
+ }
+ }
+
+ // find the best view to give focus to in this brave new non-touch-mode
+ // world
+ return mView.restoreDefaultFocus();
+ }
+ return false;
+ }
+
+ /**
+ * Base class for implementing a stage in the chain of responsibility
+ * for processing input events.
+ * <p>
+ * Events are delivered to the stage by the {@link #deliver} method. The stage
+ * then has the choice of finishing the event or forwarding it to the next stage.
+ * </p>
+ */
+ abstract class InputStage {
+ private final InputStage mNext;
+
+ protected static final int FORWARD = 0;
+ protected static final int FINISH_HANDLED = 1;
+ protected static final int FINISH_NOT_HANDLED = 2;
+
+ private String mTracePrefix;
+
+ /**
+ * Creates an input stage.
+ * @param next The next stage to which events should be forwarded.
+ */
+ public InputStage(InputStage next) {
+ mNext = next;
+ }
+
+ /**
+ * Delivers an event to be processed.
+ */
+ public final void deliver(QueuedInputEvent q) {
+ if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
+ forward(q);
+ } else if (shouldDropInputEvent(q)) {
+ finish(q, false);
+ } else {
+ traceEvent(q, Trace.TRACE_TAG_VIEW);
+ final int result;
+ try {
+ result = onProcess(q);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ apply(q, result);
+ }
+ }
+
+ /**
+ * Marks the the input event as finished then forwards it to the next stage.
+ */
+ protected void finish(QueuedInputEvent q, boolean handled) {
+ q.mFlags |= QueuedInputEvent.FLAG_FINISHED;
+ if (handled) {
+ q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED;
+ }
+ forward(q);
+ }
+
+ /**
+ * Forwards the event to the next stage.
+ */
+ protected void forward(QueuedInputEvent q) {
+ onDeliverToNext(q);
+ }
+
+ /**
+ * Applies a result code from {@link #onProcess} to the specified event.
+ */
+ protected void apply(QueuedInputEvent q, int result) {
+ if (result == FORWARD) {
+ forward(q);
+ } else if (result == FINISH_HANDLED) {
+ finish(q, true);
+ } else if (result == FINISH_NOT_HANDLED) {
+ finish(q, false);
+ } else {
+ throw new IllegalArgumentException("Invalid result: " + result);
+ }
+ }
+
+ /**
+ * Called when an event is ready to be processed.
+ * @return A result code indicating how the event was handled.
+ */
+ protected int onProcess(QueuedInputEvent q) {
+ return FORWARD;
+ }
+
+ /**
+ * Called when an event is being delivered to the next stage.
+ */
+ protected void onDeliverToNext(QueuedInputEvent q) {
+ if (DEBUG_INPUT_STAGES) {
+ Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
+ }
+ if (mNext != null) {
+ mNext.deliver(q);
+ } else {
+ finishInputEvent(q);
+ }
+ }
+
+ protected void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (mNext != null) {
+ mNext.onWindowFocusChanged(hasWindowFocus);
+ }
+ }
+
+ protected void onDetachedFromWindow() {
+ if (mNext != null) {
+ mNext.onDetachedFromWindow();
+ }
+ }
+
+ protected boolean shouldDropInputEvent(QueuedInputEvent q) {
+ if (mView == null || !mAdded) {
+ Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
+ return true;
+ }
+
+ // Find a reason for dropping or canceling the event.
+ final String reason;
+ if (!mAttachInfo.mHasWindowFocus
+ && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
+ && !isAutofillUiShowing()) {
+ // This is a non-pointer event and the window doesn't currently have input focus
+ // This could be an event that came back from the previous stage
+ // but the window has lost focus or stopped in the meantime.
+ reason = "no window focus";
+ } else if (mStopped) {
+ reason = "window is stopped";
+ } else if (mIsAmbientMode
+ && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) {
+ reason = "non-button event in ambient mode";
+ } else if (mPausedForTransition && !isBack(q.mEvent)) {
+ reason = "paused for transition";
+ } else {
+ // Most common path: no reason to drop or cancel the event
+ return false;
+ }
+
+ if (isTerminalInputEvent(q.mEvent)) {
+ // Don't drop terminal input events, however mark them as canceled.
+ q.mEvent.cancel();
+ Slog.w(mTag, "Cancelling event (" + reason + "):" + q.mEvent);
+ return false;
+ }
+
+ // Drop non-terminal input events.
+ Slog.w(mTag, "Dropping event (" + reason + "):" + q.mEvent);
+ return true;
+ }
+
+ void dump(String prefix, PrintWriter writer) {
+ if (mNext != null) {
+ mNext.dump(prefix, writer);
+ }
+ }
+
+ private boolean isBack(InputEvent event) {
+ if (event instanceof KeyEvent) {
+ return ((KeyEvent) event).getKeyCode() == KeyEvent.KEYCODE_BACK;
+ } else {
+ return false;
+ }
+ }
+
+ private void traceEvent(QueuedInputEvent q, long traceTag) {
+ if (!Trace.isTagEnabled(traceTag)) {
+ return;
+ }
+
+ if (mTracePrefix == null) {
+ mTracePrefix = getClass().getSimpleName();
+ }
+ Trace.traceBegin(traceTag, mTracePrefix + " id=0x"
+ + Integer.toHexString(q.mEvent.getId()));
+ }
+ }
+
+ /**
+ * Base class for implementing an input pipeline stage that supports
+ * asynchronous and out-of-order processing of input events.
+ * <p>
+ * In addition to what a normal input stage can do, an asynchronous
+ * input stage may also defer an input event that has been delivered to it
+ * and finish or forward it later.
+ * </p>
+ */
+ abstract class AsyncInputStage extends InputStage {
+ private final String mTraceCounter;
+
+ private QueuedInputEvent mQueueHead;
+ private QueuedInputEvent mQueueTail;
+ private int mQueueLength;
+
+ protected static final int DEFER = 3;
+
+ /**
+ * Creates an asynchronous input stage.
+ * @param next The next stage to which events should be forwarded.
+ * @param traceCounter The name of a counter to record the size of
+ * the queue of pending events.
+ */
+ public AsyncInputStage(InputStage next, String traceCounter) {
+ super(next);
+ mTraceCounter = traceCounter;
+ }
+
+ /**
+ * Marks the event as deferred, which is to say that it will be handled
+ * asynchronously. The caller is responsible for calling {@link #forward}
+ * or {@link #finish} later when it is done handling the event.
+ */
+ protected void defer(QueuedInputEvent q) {
+ q.mFlags |= QueuedInputEvent.FLAG_DEFERRED;
+ enqueue(q);
+ }
+
+ @Override
+ protected void forward(QueuedInputEvent q) {
+ // Clear the deferred flag.
+ q.mFlags &= ~QueuedInputEvent.FLAG_DEFERRED;
+
+ // Fast path if the queue is empty.
+ QueuedInputEvent curr = mQueueHead;
+ if (curr == null) {
+ super.forward(q);
+ return;
+ }
+
+ // Determine whether the event must be serialized behind any others
+ // before it can be delivered to the next stage. This is done because
+ // deferred events might be handled out of order by the stage.
+ final int deviceId = q.mEvent.getDeviceId();
+ QueuedInputEvent prev = null;
+ boolean blocked = false;
+ while (curr != null && curr != q) {
+ if (!blocked && deviceId == curr.mEvent.getDeviceId()) {
+ blocked = true;
+ }
+ prev = curr;
+ curr = curr.mNext;
+ }
+
+ // If the event is blocked, then leave it in the queue to be delivered later.
+ // Note that the event might not yet be in the queue if it was not previously
+ // deferred so we will enqueue it if needed.
+ if (blocked) {
+ if (curr == null) {
+ enqueue(q);
+ }
+ return;
+ }
+
+ // The event is not blocked. Deliver it immediately.
+ if (curr != null) {
+ curr = curr.mNext;
+ dequeue(q, prev);
+ }
+ super.forward(q);
+
+ // Dequeuing this event may have unblocked successors. Deliver them.
+ while (curr != null) {
+ if (deviceId == curr.mEvent.getDeviceId()) {
+ if ((curr.mFlags & QueuedInputEvent.FLAG_DEFERRED) != 0) {
+ break;
+ }
+ QueuedInputEvent next = curr.mNext;
+ dequeue(curr, prev);
+ super.forward(curr);
+ curr = next;
+ } else {
+ prev = curr;
+ curr = curr.mNext;
+ }
+ }
+ }
+
+ @Override
+ protected void apply(QueuedInputEvent q, int result) {
+ if (result == DEFER) {
+ defer(q);
+ } else {
+ super.apply(q, result);
+ }
+ }
+
+ private void enqueue(QueuedInputEvent q) {
+ if (mQueueTail == null) {
+ mQueueHead = q;
+ mQueueTail = q;
+ } else {
+ mQueueTail.mNext = q;
+ mQueueTail = q;
+ }
+
+ mQueueLength += 1;
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, mTraceCounter, mQueueLength);
+ }
+
+ private void dequeue(QueuedInputEvent q, QueuedInputEvent prev) {
+ if (prev == null) {
+ mQueueHead = q.mNext;
+ } else {
+ prev.mNext = q.mNext;
+ }
+ if (mQueueTail == q) {
+ mQueueTail = prev;
+ }
+ q.mNext = null;
+
+ mQueueLength -= 1;
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, mTraceCounter, mQueueLength);
+ }
+
+ @Override
+ void dump(String prefix, PrintWriter writer) {
+ writer.print(prefix);
+ writer.print(getClass().getName());
+ writer.print(": mQueueLength=");
+ writer.println(mQueueLength);
+
+ super.dump(prefix, writer);
+ }
+ }
+
+ /**
+ * Delivers pre-ime input events to a native activity.
+ * Does not support pointer events.
+ */
+ final class NativePreImeInputStage extends AsyncInputStage
+ implements InputQueue.FinishedInputEventCallback {
+ public NativePreImeInputStage(InputStage next, String traceCounter) {
+ super(next, traceCounter);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ if (mInputQueue != null && q.mEvent instanceof KeyEvent) {
+ mInputQueue.sendInputEvent(q.mEvent, q, true, this);
+ return DEFER;
+ }
+ return FORWARD;
+ }
+
+ @Override
+ public void onFinishedInputEvent(Object token, boolean handled) {
+ QueuedInputEvent q = (QueuedInputEvent)token;
+ if (handled) {
+ finish(q, true);
+ return;
+ }
+ forward(q);
+ }
+ }
+
+ /**
+ * Delivers pre-ime input events to the view hierarchy.
+ * Does not support pointer events.
+ */
+ final class ViewPreImeInputStage extends InputStage {
+ public ViewPreImeInputStage(InputStage next) {
+ super(next);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ if (q.mEvent instanceof KeyEvent) {
+ return processKeyEvent(q);
+ }
+ return FORWARD;
+ }
+
+ private int processKeyEvent(QueuedInputEvent q) {
+ final KeyEvent event = (KeyEvent)q.mEvent;
+ if (mView.dispatchKeyEventPreIme(event)) {
+ return FINISH_HANDLED;
+ }
+ return FORWARD;
+ }
+ }
+
+ /**
+ * Delivers input events to the ime.
+ * Does not support pointer events.
+ */
+ final class ImeInputStage extends AsyncInputStage
+ implements InputMethodManager.FinishedInputEventCallback {
+ public ImeInputStage(InputStage next, String traceCounter) {
+ super(next, traceCounter);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ final int result = mImeFocusController.onProcessImeInputStage(
+ q, q.mEvent, mWindowAttributes, this);
+ switch (result) {
+ case InputMethodManager.DISPATCH_IN_PROGRESS:
+ // callback will be invoked later
+ return DEFER;
+ case InputMethodManager.DISPATCH_NOT_HANDLED:
+ // The IME could not handle it, so skip along to the next InputStage
+ return FORWARD;
+ case InputMethodManager.DISPATCH_HANDLED:
+ return FINISH_HANDLED;
+ default:
+ throw new IllegalStateException("Unexpected result=" + result);
+ }
+ }
+
+ @Override
+ public void onFinishedInputEvent(Object token, boolean handled) {
+ QueuedInputEvent q = (QueuedInputEvent)token;
+ if (handled) {
+ finish(q, true);
+ return;
+ }
+ forward(q);
+ }
+ }
+
+ /**
+ * Performs early processing of post-ime input events.
+ */
+ final class EarlyPostImeInputStage extends InputStage {
+ public EarlyPostImeInputStage(InputStage next) {
+ super(next);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ if (q.mEvent instanceof KeyEvent) {
+ return processKeyEvent(q);
+ } else if (q.mEvent instanceof MotionEvent) {
+ return processMotionEvent(q);
+ }
+ return FORWARD;
+ }
+
+ private int processKeyEvent(QueuedInputEvent q) {
+ final KeyEvent event = (KeyEvent)q.mEvent;
+
+ if (mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.handleTooltipKey(event);
+ }
+
+ // If the key's purpose is to exit touch mode then we consume it
+ // and consider it handled.
+ if (checkForLeavingTouchModeAndConsume(event)) {
+ return FINISH_HANDLED;
+ }
+
+ // Make sure the fallback event policy sees all keys that will be
+ // delivered to the view hierarchy.
+ mFallbackEventHandler.preDispatchKeyEvent(event);
+ return FORWARD;
+ }
+
+ private int processMotionEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent) q.mEvent;
+
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ return processPointerEvent(q);
+ }
+
+ // If the motion event is from an absolute position device, exit touch mode
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POSITION)) {
+ ensureTouchMode(false);
+ }
+ }
+ return FORWARD;
+ }
+
+ private int processPointerEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
+
+ // Translate the pointer event for compatibility, if needed.
+ if (mTranslator != null) {
+ mTranslator.translateEventInScreenToAppWindow(event);
+ }
+
+ // Enter touch mode on down or scroll from any type of a device.
+ final int action = event.getAction();
+ if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
+ ensureTouchMode(true);
+ }
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Upon motion event within app window, close autofill ui.
+ AutofillManager afm = getAutofillManager();
+ if (afm != null) {
+ afm.requestHideFillUi();
+ }
+ }
+
+ if (action == MotionEvent.ACTION_DOWN && mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.hideTooltip();
+ }
+
+ // Offset the scroll position.
+ if (mCurScrollY != 0) {
+ event.offsetLocation(0, mCurScrollY);
+ }
+
+ // Remember the touch position for possible drag-initiation.
+ if (event.isTouchEvent()) {
+ mLastTouchPoint.x = event.getRawX();
+ mLastTouchPoint.y = event.getRawY();
+ mLastTouchSource = event.getSource();
+ }
+ return FORWARD;
+ }
+ }
+
+ /**
+ * Delivers post-ime input events to a native activity.
+ */
+ final class NativePostImeInputStage extends AsyncInputStage
+ implements InputQueue.FinishedInputEventCallback {
+ public NativePostImeInputStage(InputStage next, String traceCounter) {
+ super(next, traceCounter);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ if (mInputQueue != null) {
+ mInputQueue.sendInputEvent(q.mEvent, q, false, this);
+ return DEFER;
+ }
+ return FORWARD;
+ }
+
+ @Override
+ public void onFinishedInputEvent(Object token, boolean handled) {
+ QueuedInputEvent q = (QueuedInputEvent)token;
+ if (handled) {
+ finish(q, true);
+ return;
+ }
+ forward(q);
+ }
+ }
+
+ /**
+ * Delivers post-ime input events to the view hierarchy.
+ */
+ final class ViewPostImeInputStage extends InputStage {
+ public ViewPostImeInputStage(InputStage next) {
+ super(next);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ if (q.mEvent instanceof KeyEvent) {
+ return processKeyEvent(q);
+ } else {
+ final int source = q.mEvent.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ return processPointerEvent(q);
+ } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ return processTrackballEvent(q);
+ } else {
+ return processGenericMotionEvent(q);
+ }
+ }
+ }
+
+ @Override
+ protected void onDeliverToNext(QueuedInputEvent q) {
+ if (mUnbufferedInputDispatch
+ && q.mEvent instanceof MotionEvent
+ && ((MotionEvent)q.mEvent).isTouchEvent()
+ && isTerminalInputEvent(q.mEvent)) {
+ mUnbufferedInputDispatch = false;
+ scheduleConsumeBatchedInput();
+ }
+ super.onDeliverToNext(q);
+ }
+
+ private boolean performFocusNavigation(KeyEvent event) {
+ int direction = 0;
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_LEFT;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_RIGHT;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_UP;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_DOWN;
+ }
+ break;
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers()) {
+ direction = View.FOCUS_FORWARD;
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ direction = View.FOCUS_BACKWARD;
+ }
+ break;
+ }
+ if (direction != 0) {
+ View focused = mView.findFocus();
+ if (focused != null) {
+ View v = focused.focusSearch(direction);
+ if (v != null && v != focused) {
+ // do the math the get the interesting rect
+ // of previous focused into the coord system of
+ // newly focused view
+ focused.getFocusedRect(mTempRect);
+ if (mView instanceof ViewGroup) {
+ ((ViewGroup) mView).offsetDescendantRectToMyCoords(
+ focused, mTempRect);
+ ((ViewGroup) mView).offsetRectIntoDescendantCoords(
+ v, mTempRect);
+ }
+ if (v.requestFocus(direction, mTempRect)) {
+ boolean isFastScrolling = event.getRepeatCount() > 0;
+ playSoundEffect(
+ SoundEffectConstants.getConstantForFocusDirection(direction,
+ isFastScrolling));
+ return true;
+ }
+ }
+
+ // Give the focused view a last chance to handle the dpad key.
+ if (mView.dispatchUnhandledMove(focused, direction)) {
+ return true;
+ }
+ } else {
+ if (mView.restoreDefaultFocus()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean performKeyboardGroupNavigation(int direction) {
+ final View focused = mView.findFocus();
+ if (focused == null && mView.restoreDefaultFocus()) {
+ return true;
+ }
+ View cluster = focused == null ? keyboardNavigationClusterSearch(null, direction)
+ : focused.keyboardNavigationClusterSearch(null, direction);
+
+ // Since requestFocus only takes "real" focus directions (and therefore also
+ // restoreFocusInCluster), convert forward/backward focus into FOCUS_DOWN.
+ int realDirection = direction;
+ if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
+ realDirection = View.FOCUS_DOWN;
+ }
+
+ if (cluster != null && cluster.isRootNamespace()) {
+ // the default cluster. Try to find a non-clustered view to focus.
+ if (cluster.restoreFocusNotInCluster()) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ return true;
+ }
+ // otherwise skip to next actual cluster
+ cluster = keyboardNavigationClusterSearch(null, direction);
+ }
+
+ if (cluster != null && cluster.restoreFocusInCluster(realDirection)) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ return true;
+ }
+
+ return false;
+ }
+
+ private int processKeyEvent(QueuedInputEvent q) {
+ final KeyEvent event = (KeyEvent)q.mEvent;
+
+ if (mUnhandledKeyManager.preViewDispatch(event)) {
+ return FINISH_HANDLED;
+ }
+
+ // Deliver the key to the view hierarchy.
+ if (mView.dispatchKeyEvent(event)) {
+ return FINISH_HANDLED;
+ }
+
+ if (shouldDropInputEvent(q)) {
+ return FINISH_NOT_HANDLED;
+ }
+
+ // This dispatch is for windows that don't have a Window.Callback. Otherwise,
+ // the Window.Callback usually will have already called this (see
+ // DecorView.superDispatchKeyEvent) leaving this call a no-op.
+ if (mUnhandledKeyManager.dispatch(mView, event)) {
+ return FINISH_HANDLED;
+ }
+
+ int groupNavigationDirection = 0;
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
+ if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
+ groupNavigationDirection = View.FOCUS_FORWARD;
+ } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
+ KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
+ groupNavigationDirection = View.FOCUS_BACKWARD;
+ }
+ }
+
+ // If a modifier is held, try to interpret the key as a shortcut.
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
+ && event.getRepeatCount() == 0
+ && !KeyEvent.isModifierKey(event.getKeyCode())
+ && groupNavigationDirection == 0) {
+ if (mView.dispatchKeyShortcutEvent(event)) {
+ return FINISH_HANDLED;
+ }
+ if (shouldDropInputEvent(q)) {
+ return FINISH_NOT_HANDLED;
+ }
+ }
+
+ // Apply the fallback event policy.
+ if (mFallbackEventHandler.dispatchKeyEvent(event)) {
+ return FINISH_HANDLED;
+ }
+ if (shouldDropInputEvent(q)) {
+ return FINISH_NOT_HANDLED;
+ }
+
+ // Handle automatic focus changes.
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (groupNavigationDirection != 0) {
+ if (performKeyboardGroupNavigation(groupNavigationDirection)) {
+ return FINISH_HANDLED;
+ }
+ } else {
+ if (performFocusNavigation(event)) {
+ return FINISH_HANDLED;
+ }
+ }
+ }
+ return FORWARD;
+ }
+
+ private int processPointerEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
+
+ mAttachInfo.mUnbufferedDispatchRequested = false;
+ mAttachInfo.mHandlingPointerEvent = true;
+ boolean handled = mView.dispatchPointerEvent(event);
+ maybeUpdatePointerIcon(event);
+ maybeUpdateTooltip(event);
+ mAttachInfo.mHandlingPointerEvent = false;
+ if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
+ mUnbufferedInputDispatch = true;
+ if (mConsumeBatchedInputScheduled) {
+ scheduleConsumeBatchedInputImmediately();
+ }
+ }
+ return handled ? FINISH_HANDLED : FORWARD;
+ }
+
+ private void maybeUpdatePointerIcon(MotionEvent event) {
+ if (event.getPointerCount() == 1 && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
+ || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
+ // Other apps or the window manager may change the icon type outside of
+ // this app, therefore the icon type has to be reset on enter/exit event.
+ mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ }
+
+ if (event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) {
+ if (!updatePointerIcon(event) &&
+ event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
+ mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ }
+ }
+ }
+ }
+
+ private int processTrackballEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
+
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) {
+ if (!hasPointerCapture() || mView.dispatchCapturedPointerEvent(event)) {
+ return FINISH_HANDLED;
+ }
+ }
+
+ if (mView.dispatchTrackballEvent(event)) {
+ return FINISH_HANDLED;
+ }
+ return FORWARD;
+ }
+
+ private int processGenericMotionEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
+
+ if (event.isFromSource(InputDevice.SOURCE_TOUCHPAD)) {
+ if (hasPointerCapture() && mView.dispatchCapturedPointerEvent(event)) {
+ return FINISH_HANDLED;
+ }
+ }
+
+ // Deliver the event to the view.
+ if (mView.dispatchGenericMotionEvent(event)) {
+ return FINISH_HANDLED;
+ }
+ return FORWARD;
+ }
+ }
+
+ private void resetPointerIcon(MotionEvent event) {
+ mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ updatePointerIcon(event);
+ }
+
+ private boolean updatePointerIcon(MotionEvent event) {
+ final int pointerIndex = 0;
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ if (mView == null) {
+ // E.g. click outside a popup to dismiss it
+ Slog.d(mTag, "updatePointerIcon called after view was removed");
+ return false;
+ }
+ if (x < 0 || x >= mView.getWidth() || y < 0 || y >= mView.getHeight()) {
+ // E.g. when moving window divider with mouse
+ Slog.d(mTag, "updatePointerIcon called with position out of bounds");
+ return false;
+ }
+ final PointerIcon pointerIcon = mView.onResolvePointerIcon(event, pointerIndex);
+ final int pointerType = (pointerIcon != null) ?
+ pointerIcon.getType() : PointerIcon.TYPE_DEFAULT;
+
+ if (mPointerIconType != pointerType) {
+ mPointerIconType = pointerType;
+ mCustomPointerIcon = null;
+ if (mPointerIconType != PointerIcon.TYPE_CUSTOM) {
+ InputManager.getInstance().setPointerIconType(pointerType);
+ return true;
+ }
+ }
+ if (mPointerIconType == PointerIcon.TYPE_CUSTOM &&
+ !pointerIcon.equals(mCustomPointerIcon)) {
+ mCustomPointerIcon = pointerIcon;
+ InputManager.getInstance().setCustomPointerIcon(mCustomPointerIcon);
+ }
+ return true;
+ }
+
+ private void maybeUpdateTooltip(MotionEvent event) {
+ if (event.getPointerCount() != 1) {
+ return;
+ }
+ final int action = event.getActionMasked();
+ if (action != MotionEvent.ACTION_HOVER_ENTER
+ && action != MotionEvent.ACTION_HOVER_MOVE
+ && action != MotionEvent.ACTION_HOVER_EXIT) {
+ return;
+ }
+ AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
+ if (manager.isEnabled() && manager.isTouchExplorationEnabled()) {
+ return;
+ }
+ if (mView == null) {
+ Slog.d(mTag, "maybeUpdateTooltip called after view was removed");
+ return;
+ }
+ mView.dispatchTooltipHoverEvent(event);
+ }
+
+ @Nullable
+ private View getFocusedViewOrNull() {
+ return mView != null ? mView.findFocus() : null;
+ }
+
+ /**
+ * Performs synthesis of new input events from unhandled input events.
+ */
+ final class SyntheticInputStage extends InputStage {
+ private final SyntheticTrackballHandler mTrackball = new SyntheticTrackballHandler();
+ private final SyntheticJoystickHandler mJoystick = new SyntheticJoystickHandler();
+ private final SyntheticTouchNavigationHandler mTouchNavigation =
+ new SyntheticTouchNavigationHandler();
+ private final SyntheticKeyboardHandler mKeyboard = new SyntheticKeyboardHandler();
+
+ public SyntheticInputStage() {
+ super(null);
+ }
+
+ @Override
+ protected int onProcess(QueuedInputEvent q) {
+ q.mFlags |= QueuedInputEvent.FLAG_RESYNTHESIZED;
+ if (q.mEvent instanceof MotionEvent) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
+ final int source = event.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ mTrackball.process(event);
+ return FINISH_HANDLED;
+ } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ mJoystick.process(event);
+ return FINISH_HANDLED;
+ } else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION)
+ == InputDevice.SOURCE_TOUCH_NAVIGATION) {
+ mTouchNavigation.process(event);
+ return FINISH_HANDLED;
+ }
+ } else if ((q.mFlags & QueuedInputEvent.FLAG_UNHANDLED) != 0) {
+ mKeyboard.process((KeyEvent)q.mEvent);
+ return FINISH_HANDLED;
+ }
+
+ return FORWARD;
+ }
+
+ @Override
+ protected void onDeliverToNext(QueuedInputEvent q) {
+ if ((q.mFlags & QueuedInputEvent.FLAG_RESYNTHESIZED) == 0) {
+ // Cancel related synthetic events if any prior stage has handled the event.
+ if (q.mEvent instanceof MotionEvent) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
+ final int source = event.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ mTrackball.cancel();
+ } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ mJoystick.cancel();
+ } else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION)
+ == InputDevice.SOURCE_TOUCH_NAVIGATION) {
+ mTouchNavigation.cancel(event);
+ }
+ }
+ }
+ super.onDeliverToNext(q);
+ }
+
+ @Override
+ protected void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (!hasWindowFocus) {
+ mJoystick.cancel();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mJoystick.cancel();
+ }
+ }
+
+ /**
+ * Creates dpad events from unhandled trackball movements.
+ */
+ final class SyntheticTrackballHandler {
+ private final TrackballAxis mX = new TrackballAxis();
+ private final TrackballAxis mY = new TrackballAxis();
+ private long mLastTime;
+
+ public void process(MotionEvent event) {
+ // Translate the trackball event into DPAD keys and try to deliver those.
+ long curTime = SystemClock.uptimeMillis();
+ if ((mLastTime + MAX_TRACKBALL_DELAY) < curTime) {
+ // It has been too long since the last movement,
+ // so restart at the beginning.
+ mX.reset(0);
+ mY.reset(0);
+ mLastTime = curTime;
+ }
+
+ final int action = event.getAction();
+ final int metaState = event.getMetaState();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mX.reset(2);
+ mY.reset(2);
+ enqueueInputEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
+ InputDevice.SOURCE_KEYBOARD));
+ break;
+ case MotionEvent.ACTION_UP:
+ mX.reset(2);
+ mY.reset(2);
+ enqueueInputEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
+ InputDevice.SOURCE_KEYBOARD));
+ break;
+ }
+
+ if (DEBUG_TRACKBALL) Log.v(mTag, "TB X=" + mX.position + " step="
+ + mX.step + " dir=" + mX.dir + " acc=" + mX.acceleration
+ + " move=" + event.getX()
+ + " / Y=" + mY.position + " step="
+ + mY.step + " dir=" + mY.dir + " acc=" + mY.acceleration
+ + " move=" + event.getY());
+ final float xOff = mX.collect(event.getX(), event.getEventTime(), "X");
+ final float yOff = mY.collect(event.getY(), event.getEventTime(), "Y");
+
+ // Generate DPAD events based on the trackball movement.
+ // We pick the axis that has moved the most as the direction of
+ // the DPAD. When we generate DPAD events for one axis, then the
+ // other axis is reset -- we don't want to perform DPAD jumps due
+ // to slight movements in the trackball when making major movements
+ // along the other axis.
+ int keycode = 0;
+ int movement = 0;
+ float accel = 1;
+ if (xOff > yOff) {
+ movement = mX.generate();
+ if (movement != 0) {
+ keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT
+ : KeyEvent.KEYCODE_DPAD_LEFT;
+ accel = mX.acceleration;
+ mY.reset(2);
+ }
+ } else if (yOff > 0) {
+ movement = mY.generate();
+ if (movement != 0) {
+ keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_DOWN
+ : KeyEvent.KEYCODE_DPAD_UP;
+ accel = mY.acceleration;
+ mX.reset(2);
+ }
+ }
+
+ if (keycode != 0) {
+ if (movement < 0) movement = -movement;
+ int accelMovement = (int)(movement * accel);
+ if (DEBUG_TRACKBALL) Log.v(mTag, "Move: movement=" + movement
+ + " accelMovement=" + accelMovement
+ + " accel=" + accel);
+ if (accelMovement > movement) {
+ if (DEBUG_TRACKBALL) Log.v(mTag, "Delivering fake DPAD: "
+ + keycode);
+ movement--;
+ int repeatCount = accelMovement - movement;
+ enqueueInputEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_MULTIPLE, keycode, repeatCount, metaState,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
+ InputDevice.SOURCE_KEYBOARD));
+ }
+ while (movement > 0) {
+ if (DEBUG_TRACKBALL) Log.v(mTag, "Delivering fake DPAD: "
+ + keycode);
+ movement--;
+ curTime = SystemClock.uptimeMillis();
+ enqueueInputEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_DOWN, keycode, 0, metaState,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
+ InputDevice.SOURCE_KEYBOARD));
+ enqueueInputEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_UP, keycode, 0, metaState,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
+ InputDevice.SOURCE_KEYBOARD));
+ }
+ mLastTime = curTime;
+ }
+ }
+
+ public void cancel() {
+ mLastTime = Integer.MIN_VALUE;
+
+ // If we reach this, we consumed a trackball event.
+ // Because we will not translate the trackball event into a key event,
+ // touch mode will not exit, so we exit touch mode here.
+ if (mView != null && mAdded) {
+ ensureTouchMode(false);
+ }
+ }
+ }
+
+ /**
+ * Maintains state information for a single trackball axis, generating
+ * discrete (DPAD) movements based on raw trackball motion.
+ */
+ static final class TrackballAxis {
+ /**
+ * The maximum amount of acceleration we will apply.
+ */
+ static final float MAX_ACCELERATION = 20;
+
+ /**
+ * The maximum amount of time (in milliseconds) between events in order
+ * for us to consider the user to be doing fast trackball movements,
+ * and thus apply an acceleration.
+ */
+ static final long FAST_MOVE_TIME = 150;
+
+ /**
+ * Scaling factor to the time (in milliseconds) between events to how
+ * much to multiple/divide the current acceleration. When movement
+ * is < FAST_MOVE_TIME this multiplies the acceleration; when >
+ * FAST_MOVE_TIME it divides it.
+ */
+ static final float ACCEL_MOVE_SCALING_FACTOR = (1.0f/40);
+
+ static final float FIRST_MOVEMENT_THRESHOLD = 0.5f;
+ static final float SECOND_CUMULATIVE_MOVEMENT_THRESHOLD = 2.0f;
+ static final float SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD = 1.0f;
+
+ float position;
+ float acceleration = 1;
+ long lastMoveTime = 0;
+ int step;
+ int dir;
+ int nonAccelMovement;
+
+ void reset(int _step) {
+ position = 0;
+ acceleration = 1;
+ lastMoveTime = 0;
+ step = _step;
+ dir = 0;
+ }
+
+ /**
+ * Add trackball movement into the state. If the direction of movement
+ * has been reversed, the state is reset before adding the
+ * movement (so that you don't have to compensate for any previously
+ * collected movement before see the result of the movement in the
+ * new direction).
+ *
+ * @return Returns the absolute value of the amount of movement
+ * collected so far.
+ */
+ float collect(float off, long time, String axis) {
+ long normTime;
+ if (off > 0) {
+ normTime = (long)(off * FAST_MOVE_TIME);
+ if (dir < 0) {
+ if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to positive!");
+ position = 0;
+ step = 0;
+ acceleration = 1;
+ lastMoveTime = 0;
+ }
+ dir = 1;
+ } else if (off < 0) {
+ normTime = (long)((-off) * FAST_MOVE_TIME);
+ if (dir > 0) {
+ if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to negative!");
+ position = 0;
+ step = 0;
+ acceleration = 1;
+ lastMoveTime = 0;
+ }
+ dir = -1;
+ } else {
+ normTime = 0;
+ }
+
+ // The number of milliseconds between each movement that is
+ // considered "normal" and will not result in any acceleration
+ // or deceleration, scaled by the offset we have here.
+ if (normTime > 0) {
+ long delta = time - lastMoveTime;
+ lastMoveTime = time;
+ float acc = acceleration;
+ if (delta < normTime) {
+ // The user is scrolling rapidly, so increase acceleration.
+ float scale = (normTime-delta) * ACCEL_MOVE_SCALING_FACTOR;
+ if (scale > 1) acc *= scale;
+ if (DEBUG_TRACKBALL) Log.v(TAG, axis + " accelerate: off="
+ + off + " normTime=" + normTime + " delta=" + delta
+ + " scale=" + scale + " acc=" + acc);
+ acceleration = acc < MAX_ACCELERATION ? acc : MAX_ACCELERATION;
+ } else {
+ // The user is scrolling slowly, so decrease acceleration.
+ float scale = (delta-normTime) * ACCEL_MOVE_SCALING_FACTOR;
+ if (scale > 1) acc /= scale;
+ if (DEBUG_TRACKBALL) Log.v(TAG, axis + " deccelerate: off="
+ + off + " normTime=" + normTime + " delta=" + delta
+ + " scale=" + scale + " acc=" + acc);
+ acceleration = acc > 1 ? acc : 1;
+ }
+ }
+ position += off;
+ return Math.abs(position);
+ }
+
+ /**
+ * Generate the number of discrete movement events appropriate for
+ * the currently collected trackball movement.
+ *
+ * @return Returns the number of discrete movements, either positive
+ * or negative, or 0 if there is not enough trackball movement yet
+ * for a discrete movement.
+ */
+ int generate() {
+ int movement = 0;
+ nonAccelMovement = 0;
+ do {
+ final int dir = position >= 0 ? 1 : -1;
+ switch (step) {
+ // If we are going to execute the first step, then we want
+ // to do this as soon as possible instead of waiting for
+ // a full movement, in order to make things look responsive.
+ case 0:
+ if (Math.abs(position) < FIRST_MOVEMENT_THRESHOLD) {
+ return movement;
+ }
+ movement += dir;
+ nonAccelMovement += dir;
+ step = 1;
+ break;
+ // If we have generated the first movement, then we need
+ // to wait for the second complete trackball motion before
+ // generating the second discrete movement.
+ case 1:
+ if (Math.abs(position) < SECOND_CUMULATIVE_MOVEMENT_THRESHOLD) {
+ return movement;
+ }
+ movement += dir;
+ nonAccelMovement += dir;
+ position -= SECOND_CUMULATIVE_MOVEMENT_THRESHOLD * dir;
+ step = 2;
+ break;
+ // After the first two, we generate discrete movements
+ // consistently with the trackball, applying an acceleration
+ // if the trackball is moving quickly. This is a simple
+ // acceleration on top of what we already compute based
+ // on how quickly the wheel is being turned, to apply
+ // a longer increasing acceleration to continuous movement
+ // in one direction.
+ default:
+ if (Math.abs(position) < SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD) {
+ return movement;
+ }
+ movement += dir;
+ position -= dir * SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD;
+ float acc = acceleration;
+ acc *= 1.1f;
+ acceleration = acc < MAX_ACCELERATION ? acc : acceleration;
+ break;
+ }
+ } while (true);
+ }
+ }
+
+ /**
+ * Creates dpad events from unhandled joystick movements.
+ */
+ final class SyntheticJoystickHandler extends Handler {
+ private final static int MSG_ENQUEUE_X_AXIS_KEY_REPEAT = 1;
+ private final static int MSG_ENQUEUE_Y_AXIS_KEY_REPEAT = 2;
+
+ private final JoystickAxesState mJoystickAxesState = new JoystickAxesState();
+ private final SparseArray<KeyEvent> mDeviceKeyEvents = new SparseArray<>();
+
+ public SyntheticJoystickHandler() {
+ super(true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ENQUEUE_X_AXIS_KEY_REPEAT:
+ case MSG_ENQUEUE_Y_AXIS_KEY_REPEAT: {
+ if (mAttachInfo.mHasWindowFocus) {
+ KeyEvent oldEvent = (KeyEvent) msg.obj;
+ KeyEvent e = KeyEvent.changeTimeRepeat(oldEvent,
+ SystemClock.uptimeMillis(), oldEvent.getRepeatCount() + 1);
+ enqueueInputEvent(e);
+ Message m = obtainMessage(msg.what, e);
+ m.setAsynchronous(true);
+ sendMessageDelayed(m, ViewConfiguration.getKeyRepeatDelay());
+ }
+ } break;
+ }
+ }
+
+ public void process(MotionEvent event) {
+ switch(event.getActionMasked()) {
+ case MotionEvent.ACTION_CANCEL:
+ cancel();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ update(event);
+ break;
+ default:
+ Log.w(mTag, "Unexpected action: " + event.getActionMasked());
+ }
+ }
+
+ private void cancel() {
+ removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT);
+ removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT);
+ for (int i = 0; i < mDeviceKeyEvents.size(); i++) {
+ final KeyEvent keyEvent = mDeviceKeyEvents.valueAt(i);
+ if (keyEvent != null) {
+ enqueueInputEvent(KeyEvent.changeTimeRepeat(keyEvent,
+ SystemClock.uptimeMillis(), 0));
+ }
+ }
+ mDeviceKeyEvents.clear();
+ mJoystickAxesState.resetState();
+ }
+
+ private void update(MotionEvent event) {
+ final int historySize = event.getHistorySize();
+ for (int h = 0; h < historySize; h++) {
+ final long time = event.getHistoricalEventTime(h);
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_X,
+ event.getHistoricalAxisValue(MotionEvent.AXIS_X, 0, h));
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_Y,
+ event.getHistoricalAxisValue(MotionEvent.AXIS_Y, 0, h));
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_X,
+ event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, 0, h));
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_Y,
+ event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, 0, h));
+ }
+ final long time = event.getEventTime();
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_X,
+ event.getAxisValue(MotionEvent.AXIS_X));
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_Y,
+ event.getAxisValue(MotionEvent.AXIS_Y));
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_X,
+ event.getAxisValue(MotionEvent.AXIS_HAT_X));
+ mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_Y,
+ event.getAxisValue(MotionEvent.AXIS_HAT_Y));
+ }
+
+ final class JoystickAxesState {
+ // State machine: from neutral state (no button press) can go into
+ // button STATE_UP_OR_LEFT or STATE_DOWN_OR_RIGHT state, emitting an ACTION_DOWN event.
+ // From STATE_UP_OR_LEFT or STATE_DOWN_OR_RIGHT state can go into neutral state,
+ // emitting an ACTION_UP event.
+ private static final int STATE_UP_OR_LEFT = -1;
+ private static final int STATE_NEUTRAL = 0;
+ private static final int STATE_DOWN_OR_RIGHT = 1;
+
+ final int[] mAxisStatesHat = {STATE_NEUTRAL, STATE_NEUTRAL}; // {AXIS_HAT_X, AXIS_HAT_Y}
+ final int[] mAxisStatesStick = {STATE_NEUTRAL, STATE_NEUTRAL}; // {AXIS_X, AXIS_Y}
+
+ void resetState() {
+ mAxisStatesHat[0] = STATE_NEUTRAL;
+ mAxisStatesHat[1] = STATE_NEUTRAL;
+ mAxisStatesStick[0] = STATE_NEUTRAL;
+ mAxisStatesStick[1] = STATE_NEUTRAL;
+ }
+
+ void updateStateForAxis(MotionEvent event, long time, int axis, float value) {
+ // Emit KeyEvent if necessary
+ // axis can be AXIS_X, AXIS_Y, AXIS_HAT_X, AXIS_HAT_Y
+ final int axisStateIndex;
+ final int repeatMessage;
+ if (isXAxis(axis)) {
+ axisStateIndex = 0;
+ repeatMessage = MSG_ENQUEUE_X_AXIS_KEY_REPEAT;
+ } else if (isYAxis(axis)) {
+ axisStateIndex = 1;
+ repeatMessage = MSG_ENQUEUE_Y_AXIS_KEY_REPEAT;
+ } else {
+ Log.e(mTag, "Unexpected axis " + axis + " in updateStateForAxis!");
+ return;
+ }
+ final int newState = joystickAxisValueToState(value);
+
+ final int currentState;
+ if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y) {
+ currentState = mAxisStatesStick[axisStateIndex];
+ } else {
+ currentState = mAxisStatesHat[axisStateIndex];
+ }
+
+ if (currentState == newState) {
+ return;
+ }
+
+ final int metaState = event.getMetaState();
+ final int deviceId = event.getDeviceId();
+ final int source = event.getSource();
+
+ if (currentState == STATE_DOWN_OR_RIGHT || currentState == STATE_UP_OR_LEFT) {
+ // send a button release event
+ final int keyCode = joystickAxisAndStateToKeycode(axis, currentState);
+ if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
+ enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, keyCode,
+ 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source));
+ // remove the corresponding pending UP event if focus lost/view detached
+ mDeviceKeyEvents.put(deviceId, null);
+ }
+ removeMessages(repeatMessage);
+ }
+
+ if (newState == STATE_DOWN_OR_RIGHT || newState == STATE_UP_OR_LEFT) {
+ // send a button down event
+ final int keyCode = joystickAxisAndStateToKeycode(axis, newState);
+ if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
+ KeyEvent keyEvent = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, keyCode,
+ 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source);
+ enqueueInputEvent(keyEvent);
+ Message m = obtainMessage(repeatMessage, keyEvent);
+ m.setAsynchronous(true);
+ sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout());
+ // store the corresponding ACTION_UP event so that it can be sent
+ // if focus is lost or root view is removed
+ mDeviceKeyEvents.put(deviceId,
+ new KeyEvent(time, time, KeyEvent.ACTION_UP, keyCode,
+ 0, metaState, deviceId, 0,
+ KeyEvent.FLAG_FALLBACK | KeyEvent.FLAG_CANCELED,
+ source));
+ }
+ }
+ if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y) {
+ mAxisStatesStick[axisStateIndex] = newState;
+ } else {
+ mAxisStatesHat[axisStateIndex] = newState;
+ }
+ }
+
+ private boolean isXAxis(int axis) {
+ return axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_HAT_X;
+ }
+ private boolean isYAxis(int axis) {
+ return axis == MotionEvent.AXIS_Y || axis == MotionEvent.AXIS_HAT_Y;
+ }
+
+ private int joystickAxisAndStateToKeycode(int axis, int state) {
+ if (isXAxis(axis) && state == STATE_UP_OR_LEFT) {
+ return KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ if (isXAxis(axis) && state == STATE_DOWN_OR_RIGHT) {
+ return KeyEvent.KEYCODE_DPAD_RIGHT;
+ }
+ if (isYAxis(axis) && state == STATE_UP_OR_LEFT) {
+ return KeyEvent.KEYCODE_DPAD_UP;
+ }
+ if (isYAxis(axis) && state == STATE_DOWN_OR_RIGHT) {
+ return KeyEvent.KEYCODE_DPAD_DOWN;
+ }
+ Log.e(mTag, "Unknown axis " + axis + " or direction " + state);
+ return KeyEvent.KEYCODE_UNKNOWN; // should never happen
+ }
+
+ private int joystickAxisValueToState(float value) {
+ if (value >= 0.5f) {
+ return STATE_DOWN_OR_RIGHT;
+ } else if (value <= -0.5f) {
+ return STATE_UP_OR_LEFT;
+ } else {
+ return STATE_NEUTRAL;
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates dpad events from unhandled touch navigation movements.
+ */
+ final class SyntheticTouchNavigationHandler extends Handler {
+ private static final String LOCAL_TAG = "SyntheticTouchNavigationHandler";
+ private static final boolean LOCAL_DEBUG = false;
+
+ // Assumed nominal width and height in millimeters of a touch navigation pad,
+ // if no resolution information is available from the input system.
+ private static final float DEFAULT_WIDTH_MILLIMETERS = 48;
+ private static final float DEFAULT_HEIGHT_MILLIMETERS = 48;
+
+ /* TODO: These constants should eventually be moved to ViewConfiguration. */
+
+ // The nominal distance traveled to move by one unit.
+ private static final int TICK_DISTANCE_MILLIMETERS = 12;
+
+ // Minimum and maximum fling velocity in ticks per second.
+ // The minimum velocity should be set such that we perform enough ticks per
+ // second that the fling appears to be fluid. For example, if we set the minimum
+ // to 2 ticks per second, then there may be up to half a second delay between the next
+ // to last and last ticks which is noticeably discrete and jerky. This value should
+ // probably not be set to anything less than about 4.
+ // If fling accuracy is a problem then consider tuning the tick distance instead.
+ private static final float MIN_FLING_VELOCITY_TICKS_PER_SECOND = 6f;
+ private static final float MAX_FLING_VELOCITY_TICKS_PER_SECOND = 20f;
+
+ // Fling velocity decay factor applied after each new key is emitted.
+ // This parameter controls the deceleration and overall duration of the fling.
+ // The fling stops automatically when its velocity drops below the minimum
+ // fling velocity defined above.
+ private static final float FLING_TICK_DECAY = 0.8f;
+
+ /* The input device that we are tracking. */
+
+ private int mCurrentDeviceId = -1;
+ private int mCurrentSource;
+ private boolean mCurrentDeviceSupported;
+
+ /* Configuration for the current input device. */
+
+ // The scaled tick distance. A movement of this amount should generally translate
+ // into a single dpad event in a given direction.
+ private float mConfigTickDistance;
+
+ // The minimum and maximum scaled fling velocity.
+ private float mConfigMinFlingVelocity;
+ private float mConfigMaxFlingVelocity;
+
+ /* Tracking state. */
+
+ // The velocity tracker for detecting flings.
+ private VelocityTracker mVelocityTracker;
+
+ // The active pointer id, or -1 if none.
+ private int mActivePointerId = -1;
+
+ // Location where tracking started.
+ private float mStartX;
+ private float mStartY;
+
+ // Most recently observed position.
+ private float mLastX;
+ private float mLastY;
+
+ // Accumulated movement delta since the last direction key was sent.
+ private float mAccumulatedX;
+ private float mAccumulatedY;
+
+ // Set to true if any movement was delivered to the app.
+ // Implies that tap slop was exceeded.
+ private boolean mConsumedMovement;
+
+ // The most recently sent key down event.
+ // The keycode remains set until the direction changes or a fling ends
+ // so that repeated key events may be generated as required.
+ private long mPendingKeyDownTime;
+ private int mPendingKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+ private int mPendingKeyRepeatCount;
+ private int mPendingKeyMetaState;
+
+ // The current fling velocity while a fling is in progress.
+ private boolean mFlinging;
+ private float mFlingVelocity;
+
+ public SyntheticTouchNavigationHandler() {
+ super(true);
+ }
+
+ public void process(MotionEvent event) {
+ // Update the current device information.
+ final long time = event.getEventTime();
+ final int deviceId = event.getDeviceId();
+ final int source = event.getSource();
+ if (mCurrentDeviceId != deviceId || mCurrentSource != source) {
+ finishKeys(time);
+ finishTracking(time);
+ mCurrentDeviceId = deviceId;
+ mCurrentSource = source;
+ mCurrentDeviceSupported = false;
+ InputDevice device = event.getDevice();
+ if (device != null) {
+ // In order to support an input device, we must know certain
+ // characteristics about it, such as its size and resolution.
+ InputDevice.MotionRange xRange = device.getMotionRange(MotionEvent.AXIS_X);
+ InputDevice.MotionRange yRange = device.getMotionRange(MotionEvent.AXIS_Y);
+ if (xRange != null && yRange != null) {
+ mCurrentDeviceSupported = true;
+
+ // Infer the resolution if it not actually known.
+ float xRes = xRange.getResolution();
+ if (xRes <= 0) {
+ xRes = xRange.getRange() / DEFAULT_WIDTH_MILLIMETERS;
+ }
+ float yRes = yRange.getResolution();
+ if (yRes <= 0) {
+ yRes = yRange.getRange() / DEFAULT_HEIGHT_MILLIMETERS;
+ }
+ float nominalRes = (xRes + yRes) * 0.5f;
+
+ // Precompute all of the configuration thresholds we will need.
+ mConfigTickDistance = TICK_DISTANCE_MILLIMETERS * nominalRes;
+ mConfigMinFlingVelocity =
+ MIN_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance;
+ mConfigMaxFlingVelocity =
+ MAX_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance;
+
+ if (LOCAL_DEBUG) {
+ Log.d(LOCAL_TAG, "Configured device " + mCurrentDeviceId
+ + " (" + Integer.toHexString(mCurrentSource) + "): "
+ + ", mConfigTickDistance=" + mConfigTickDistance
+ + ", mConfigMinFlingVelocity=" + mConfigMinFlingVelocity
+ + ", mConfigMaxFlingVelocity=" + mConfigMaxFlingVelocity);
+ }
+ }
+ }
+ }
+ if (!mCurrentDeviceSupported) {
+ return;
+ }
+
+ // Handle the event.
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ boolean caughtFling = mFlinging;
+ finishKeys(time);
+ finishTracking(time);
+ mActivePointerId = event.getPointerId(0);
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(event);
+ mStartX = event.getX();
+ mStartY = event.getY();
+ mLastX = mStartX;
+ mLastY = mStartY;
+ mAccumulatedX = 0;
+ mAccumulatedY = 0;
+
+ // If we caught a fling, then pretend that the tap slop has already
+ // been exceeded to suppress taps whose only purpose is to stop the fling.
+ mConsumedMovement = caughtFling;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE:
+ case MotionEvent.ACTION_UP: {
+ if (mActivePointerId < 0) {
+ break;
+ }
+ final int index = event.findPointerIndex(mActivePointerId);
+ if (index < 0) {
+ finishKeys(time);
+ finishTracking(time);
+ break;
+ }
+
+ mVelocityTracker.addMovement(event);
+ final float x = event.getX(index);
+ final float y = event.getY(index);
+ mAccumulatedX += x - mLastX;
+ mAccumulatedY += y - mLastY;
+ mLastX = x;
+ mLastY = y;
+
+ // Consume any accumulated movement so far.
+ final int metaState = event.getMetaState();
+ consumeAccumulatedMovement(time, metaState);
+
+ // Detect taps and flings.
+ if (action == MotionEvent.ACTION_UP) {
+ if (mConsumedMovement && mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
+ // It might be a fling.
+ mVelocityTracker.computeCurrentVelocity(1000, mConfigMaxFlingVelocity);
+ final float vx = mVelocityTracker.getXVelocity(mActivePointerId);
+ final float vy = mVelocityTracker.getYVelocity(mActivePointerId);
+ if (!startFling(time, vx, vy)) {
+ finishKeys(time);
+ }
+ }
+ finishTracking(time);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL: {
+ finishKeys(time);
+ finishTracking(time);
+ break;
+ }
+ }
+ }
+
+ public void cancel(MotionEvent event) {
+ if (mCurrentDeviceId == event.getDeviceId()
+ && mCurrentSource == event.getSource()) {
+ final long time = event.getEventTime();
+ finishKeys(time);
+ finishTracking(time);
+ }
+ }
+
+ private void finishKeys(long time) {
+ cancelFling();
+ sendKeyUp(time);
+ }
+
+ private void finishTracking(long time) {
+ if (mActivePointerId >= 0) {
+ mActivePointerId = -1;
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void consumeAccumulatedMovement(long time, int metaState) {
+ final float absX = Math.abs(mAccumulatedX);
+ final float absY = Math.abs(mAccumulatedY);
+ if (absX >= absY) {
+ if (absX >= mConfigTickDistance) {
+ mAccumulatedX = consumeAccumulatedMovement(time, metaState, mAccumulatedX,
+ KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT);
+ mAccumulatedY = 0;
+ mConsumedMovement = true;
+ }
+ } else {
+ if (absY >= mConfigTickDistance) {
+ mAccumulatedY = consumeAccumulatedMovement(time, metaState, mAccumulatedY,
+ KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN);
+ mAccumulatedX = 0;
+ mConsumedMovement = true;
+ }
+ }
+ }
+
+ private float consumeAccumulatedMovement(long time, int metaState,
+ float accumulator, int negativeKeyCode, int positiveKeyCode) {
+ while (accumulator <= -mConfigTickDistance) {
+ sendKeyDownOrRepeat(time, negativeKeyCode, metaState);
+ accumulator += mConfigTickDistance;
+ }
+ while (accumulator >= mConfigTickDistance) {
+ sendKeyDownOrRepeat(time, positiveKeyCode, metaState);
+ accumulator -= mConfigTickDistance;
+ }
+ return accumulator;
+ }
+
+ private void sendKeyDownOrRepeat(long time, int keyCode, int metaState) {
+ if (mPendingKeyCode != keyCode) {
+ sendKeyUp(time);
+ mPendingKeyDownTime = time;
+ mPendingKeyCode = keyCode;
+ mPendingKeyRepeatCount = 0;
+ } else {
+ mPendingKeyRepeatCount += 1;
+ }
+ mPendingKeyMetaState = metaState;
+
+ // Note: Normally we would pass FLAG_LONG_PRESS when the repeat count is 1
+ // but it doesn't quite make sense when simulating the events in this way.
+ if (LOCAL_DEBUG) {
+ Log.d(LOCAL_TAG, "Sending key down: keyCode=" + mPendingKeyCode
+ + ", repeatCount=" + mPendingKeyRepeatCount
+ + ", metaState=" + Integer.toHexString(mPendingKeyMetaState));
+ }
+ enqueueInputEvent(new KeyEvent(mPendingKeyDownTime, time,
+ KeyEvent.ACTION_DOWN, mPendingKeyCode, mPendingKeyRepeatCount,
+ mPendingKeyMetaState, mCurrentDeviceId,
+ KeyEvent.FLAG_FALLBACK, mCurrentSource));
+ }
+
+ private void sendKeyUp(long time) {
+ if (mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
+ if (LOCAL_DEBUG) {
+ Log.d(LOCAL_TAG, "Sending key up: keyCode=" + mPendingKeyCode
+ + ", metaState=" + Integer.toHexString(mPendingKeyMetaState));
+ }
+ enqueueInputEvent(new KeyEvent(mPendingKeyDownTime, time,
+ KeyEvent.ACTION_UP, mPendingKeyCode, 0, mPendingKeyMetaState,
+ mCurrentDeviceId, 0, KeyEvent.FLAG_FALLBACK,
+ mCurrentSource));
+ mPendingKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+ }
+ }
+
+ private boolean startFling(long time, float vx, float vy) {
+ if (LOCAL_DEBUG) {
+ Log.d(LOCAL_TAG, "Considering fling: vx=" + vx + ", vy=" + vy
+ + ", min=" + mConfigMinFlingVelocity);
+ }
+
+ // Flings must be oriented in the same direction as the preceding movements.
+ switch (mPendingKeyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (-vx >= mConfigMinFlingVelocity
+ && Math.abs(vy) < mConfigMinFlingVelocity) {
+ mFlingVelocity = -vx;
+ break;
+ }
+ return false;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (vx >= mConfigMinFlingVelocity
+ && Math.abs(vy) < mConfigMinFlingVelocity) {
+ mFlingVelocity = vx;
+ break;
+ }
+ return false;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (-vy >= mConfigMinFlingVelocity
+ && Math.abs(vx) < mConfigMinFlingVelocity) {
+ mFlingVelocity = -vy;
+ break;
+ }
+ return false;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (vy >= mConfigMinFlingVelocity
+ && Math.abs(vx) < mConfigMinFlingVelocity) {
+ mFlingVelocity = vy;
+ break;
+ }
+ return false;
+ }
+
+ // Post the first fling event.
+ mFlinging = postFling(time);
+ return mFlinging;
+ }
+
+ private boolean postFling(long time) {
+ // The idea here is to estimate the time when the pointer would have
+ // traveled one tick distance unit given the current fling velocity.
+ // This effect creates continuity of motion.
+ if (mFlingVelocity >= mConfigMinFlingVelocity) {
+ long delay = (long)(mConfigTickDistance / mFlingVelocity * 1000);
+ postAtTime(mFlingRunnable, time + delay);
+ if (LOCAL_DEBUG) {
+ Log.d(LOCAL_TAG, "Posted fling: velocity="
+ + mFlingVelocity + ", delay=" + delay
+ + ", keyCode=" + mPendingKeyCode);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void cancelFling() {
+ if (mFlinging) {
+ removeCallbacks(mFlingRunnable);
+ mFlinging = false;
+ }
+ }
+
+ private final Runnable mFlingRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final long time = SystemClock.uptimeMillis();
+ sendKeyDownOrRepeat(time, mPendingKeyCode, mPendingKeyMetaState);
+ mFlingVelocity *= FLING_TICK_DECAY;
+ if (!postFling(time)) {
+ mFlinging = false;
+ finishKeys(time);
+ }
+ }
+ };
+ }
+
+ final class SyntheticKeyboardHandler {
+ public void process(KeyEvent event) {
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
+ return;
+ }
+
+ final KeyCharacterMap kcm = event.getKeyCharacterMap();
+ final int keyCode = event.getKeyCode();
+ final int metaState = event.getMetaState();
+
+ // Check for fallback actions specified by the key character map.
+ KeyCharacterMap.FallbackAction fallbackAction =
+ kcm.getFallbackAction(keyCode, metaState);
+ if (fallbackAction != null) {
+ final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
+ KeyEvent fallbackEvent = KeyEvent.obtain(
+ event.getDownTime(), event.getEventTime(),
+ event.getAction(), fallbackAction.keyCode,
+ event.getRepeatCount(), fallbackAction.metaState,
+ event.getDeviceId(), event.getScanCode(),
+ flags, event.getSource(), null);
+ fallbackAction.recycle();
+ enqueueInputEvent(fallbackEvent);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the key is used for keyboard navigation.
+ * @param keyEvent The key event.
+ * @return True if the key is used for keyboard navigation.
+ */
+ private static boolean isNavigationKey(KeyEvent keyEvent) {
+ switch (keyEvent.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_PAGE_UP:
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ case KeyEvent.KEYCODE_MOVE_END:
+ case KeyEvent.KEYCODE_TAB:
+ case KeyEvent.KEYCODE_SPACE:
+ case KeyEvent.KEYCODE_ENTER:
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the key is used for typing.
+ * @param keyEvent The key event.
+ * @return True if the key is used for typing.
+ */
+ private static boolean isTypingKey(KeyEvent keyEvent) {
+ return keyEvent.getUnicodeChar() > 0;
+ }
+
+ /**
+ * See if the key event means we should leave touch mode (and leave touch mode if so).
+ * @param event The key event.
+ * @return Whether this key event should be consumed (meaning the act of
+ * leaving touch mode alone is considered the event).
+ */
+ private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
+ // Only relevant in touch mode.
+ if (!mAttachInfo.mInTouchMode) {
+ return false;
+ }
+
+ // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
+ final int action = event.getAction();
+ if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
+ return false;
+ }
+
+ // Don't leave touch mode if the IME told us not to.
+ if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
+ return false;
+ }
+
+ // If the key can be used for keyboard navigation then leave touch mode
+ // and select a focused view if needed (in ensureTouchMode).
+ // When a new focused view is selected, we consume the navigation key because
+ // navigation doesn't make much sense unless a view already has focus so
+ // the key's purpose is to set focus.
+ if (isNavigationKey(event)) {
+ return ensureTouchMode(false);
+ }
+
+ // If the key can be used for typing then leave touch mode
+ // and select a focused view if needed (in ensureTouchMode).
+ // Always allow the view to process the typing key.
+ if (isTypingKey(event)) {
+ ensureTouchMode(false);
+ return false;
+ }
+
+ return false;
+ }
+
+ /* drag/drop */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ void setLocalDragState(Object obj) {
+ mLocalDragState = obj;
+ }
+
+ private void handleDragEvent(DragEvent event) {
+ // From the root, only drag start/end/location are dispatched. entered/exited
+ // are determined and dispatched by the viewgroup hierarchy, who then report
+ // that back here for ultimate reporting back to the framework.
+ if (mView != null && mAdded) {
+ final int what = event.mAction;
+
+ // Cache the drag description when the operation starts, then fill it in
+ // on subsequent calls as a convenience
+ if (what == DragEvent.ACTION_DRAG_STARTED) {
+ mCurrentDragView = null; // Start the current-recipient tracking
+ mDragDescription = event.mClipDescription;
+ } else {
+ if (what == DragEvent.ACTION_DRAG_ENDED) {
+ mDragDescription = null;
+ }
+ event.mClipDescription = mDragDescription;
+ }
+
+ if (what == DragEvent.ACTION_DRAG_EXITED) {
+ // A direct EXITED event means that the window manager knows we've just crossed
+ // a window boundary, so the current drag target within this one must have
+ // just been exited. Send the EXITED notification to the current drag view, if any.
+ if (View.sCascadedDragDrop) {
+ mView.dispatchDragEnterExitInPreN(event);
+ }
+ setDragFocus(null, event);
+ } else {
+ // For events with a [screen] location, translate into window coordinates
+ if ((what == DragEvent.ACTION_DRAG_LOCATION) || (what == DragEvent.ACTION_DROP)) {
+ mDragPoint.set(event.mX, event.mY);
+ if (mTranslator != null) {
+ mTranslator.translatePointInScreenToAppWindow(mDragPoint);
+ }
+
+ if (mCurScrollY != 0) {
+ mDragPoint.offset(0, mCurScrollY);
+ }
+
+ event.mX = mDragPoint.x;
+ event.mY = mDragPoint.y;
+ }
+
+ // Remember who the current drag target is pre-dispatch
+ final View prevDragView = mCurrentDragView;
+
+ if (what == DragEvent.ACTION_DROP && event.mClipData != null) {
+ event.mClipData.prepareToEnterProcess(
+ mView.getContext().getAttributionSource());
+ }
+
+ // Now dispatch the drag/drop event
+ boolean result = mView.dispatchDragEvent(event);
+
+ if (what == DragEvent.ACTION_DRAG_LOCATION && !event.mEventHandlerWasCalled) {
+ // If the LOCATION event wasn't delivered to any handler, no view now has a drag
+ // focus.
+ setDragFocus(null, event);
+ }
+
+ // If we changed apparent drag target, tell the OS about it
+ if (prevDragView != mCurrentDragView) {
+ try {
+ if (prevDragView != null) {
+ mWindowSession.dragRecipientExited(mWindow);
+ }
+ if (mCurrentDragView != null) {
+ mWindowSession.dragRecipientEntered(mWindow);
+ }
+ } catch (RemoteException e) {
+ Slog.e(mTag, "Unable to note drag target change");
+ }
+ }
+
+ // Report the drop result when we're done
+ if (what == DragEvent.ACTION_DROP) {
+ try {
+ Log.i(mTag, "Reporting drop result: " + result);
+ mWindowSession.reportDropResult(mWindow, result);
+ } catch (RemoteException e) {
+ Log.e(mTag, "Unable to report drop result");
+ }
+ }
+
+ // When the drag operation ends, reset drag-related state
+ if (what == DragEvent.ACTION_DRAG_ENDED) {
+ mCurrentDragView = null;
+ setLocalDragState(null);
+ mAttachInfo.mDragToken = null;
+ if (mAttachInfo.mDragSurface != null) {
+ mAttachInfo.mDragSurface.release();
+ mAttachInfo.mDragSurface = null;
+ }
+ }
+ }
+ }
+ event.recycle();
+ }
+
+ /**
+ * Notify that the window title changed
+ */
+ public void onWindowTitleChanged() {
+ mAttachInfo.mForceReportNewAttributes = true;
+ }
+
+ public void handleDispatchWindowShown() {
+ mAttachInfo.mTreeObserver.dispatchOnWindowShown();
+ }
+
+ public void handleRequestKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
+ Bundle data = new Bundle();
+ ArrayList<KeyboardShortcutGroup> list = new ArrayList<>();
+ if (mView != null) {
+ mView.requestKeyboardShortcuts(list, deviceId);
+ }
+ data.putParcelableArrayList(WindowManager.PARCEL_KEY_SHORTCUTS_ARRAY, list);
+ try {
+ receiver.send(0, data);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @UnsupportedAppUsage
+ public void getLastTouchPoint(Point outLocation) {
+ outLocation.x = (int) mLastTouchPoint.x;
+ outLocation.y = (int) mLastTouchPoint.y;
+ }
+
+ public int getLastTouchSource() {
+ return mLastTouchSource;
+ }
+
+ public void setDragFocus(View newDragTarget, DragEvent event) {
+ if (mCurrentDragView != newDragTarget && !View.sCascadedDragDrop) {
+ // Send EXITED and ENTERED notifications to the old and new drag focus views.
+
+ final float tx = event.mX;
+ final float ty = event.mY;
+ final int action = event.mAction;
+ final ClipData td = event.mClipData;
+ // Position should not be available for ACTION_DRAG_ENTERED and ACTION_DRAG_EXITED.
+ event.mX = 0;
+ event.mY = 0;
+ event.mClipData = null;
+
+ if (mCurrentDragView != null) {
+ event.mAction = DragEvent.ACTION_DRAG_EXITED;
+ mCurrentDragView.callDragEventHandler(event);
+ }
+
+ if (newDragTarget != null) {
+ event.mAction = DragEvent.ACTION_DRAG_ENTERED;
+ newDragTarget.callDragEventHandler(event);
+ }
+
+ event.mAction = action;
+ event.mX = tx;
+ event.mY = ty;
+ event.mClipData = td;
+ }
+
+ mCurrentDragView = newDragTarget;
+ }
+
+ private AudioManager getAudioManager() {
+ if (mView == null) {
+ throw new IllegalStateException("getAudioManager called when there is no mView");
+ }
+ if (mAudioManager == null) {
+ mAudioManager = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE);
+ }
+ return mAudioManager;
+ }
+
+ private @Nullable AutofillManager getAutofillManager() {
+ if (mView instanceof ViewGroup) {
+ ViewGroup decorView = (ViewGroup) mView;
+ if (decorView.getChildCount() > 0) {
+ // We cannot use decorView's Context for querying AutofillManager: DecorView's
+ // context is based on Application Context, it would allocate a different
+ // AutofillManager instance.
+ return decorView.getChildAt(0).getContext()
+ .getSystemService(AutofillManager.class);
+ }
+ }
+ return null;
+ }
+
+ private boolean isAutofillUiShowing() {
+ AutofillManager afm = getAutofillManager();
+ if (afm == null) {
+ return false;
+ }
+ return afm.isAutofillUiShowing();
+ }
+
+ public AccessibilityInteractionController getAccessibilityInteractionController() {
+ if (mView == null) {
+ throw new IllegalStateException("getAccessibilityInteractionController"
+ + " called when there is no mView");
+ }
+ if (mAccessibilityInteractionController == null) {
+ mAccessibilityInteractionController = new AccessibilityInteractionController(this);
+ }
+ return mAccessibilityInteractionController;
+ }
+
+ private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
+ boolean insetsPending) throws RemoteException {
+
+ float appScale = mAttachInfo.mApplicationScale;
+ boolean restore = false;
+ if (params != null && mTranslator != null) {
+ restore = true;
+ params.backup();
+ mTranslator.translateWindowLayout(params);
+ }
+
+ if (params != null) {
+ if (DBG) Log.d(mTag, "WindowLayout in layoutWindow:" + params);
+
+ if (mOrigWindowType != params.type) {
+ // For compatibility with old apps, don't crash here.
+ if (mTargetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Slog.w(mTag, "Window type can not be changed after "
+ + "the window is added; ignoring change of " + mView);
+ params.type = mOrigWindowType;
+ }
+ }
+ }
+
+ long frameNumber = -1;
+ if (mSurface.isValid()) {
+ frameNumber = mSurface.getNextFrameNumber();
+ }
+
+ int relayoutResult = mWindowSession.relayout(mWindow, params,
+ (int) (mView.getMeasuredWidth() * appScale + 0.5f),
+ (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
+ insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
+ mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
+ mTempControls, mSurfaceSize);
+ mPendingBackDropFrame.set(mTmpFrames.backdropFrame);
+ if (mSurfaceControl.isValid()) {
+ if (!useBLAST()) {
+ mSurface.copyFrom(mSurfaceControl);
+ } else {
+ final Surface blastSurface = getOrCreateBLASTSurface();
+ // If blastSurface == null that means it hasn't changed since the last time we
+ // called. In this situation, avoid calling transferFrom as we would then
+ // inc the generation ID and cause EGL resources to be recreated.
+ if (blastSurface != null) {
+ mSurface.transferFrom(blastSurface);
+ }
+ }
+ if (mAttachInfo.mThreadedRenderer != null) {
+ if (HardwareRenderer.isWebViewOverlaysEnabled()) {
+ addPrepareSurfaceControlForWebviewCallback();
+ addASurfaceTransactionCallback();
+ }
+ mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
+ }
+ } else {
+ destroySurface();
+ }
+
+ mPendingAlwaysConsumeSystemBars =
+ (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
+
+ if (restore) {
+ params.restore();
+ }
+
+ if (mTranslator != null) {
+ mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
+ mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
+ mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
+ }
+ setFrame(mTmpFrames.frame);
+ mWillMove = false;
+ mWillResize = false;
+ mInsetsController.onStateChanged(mTempInsets);
+ mInsetsController.onControlsChanged(mTempControls);
+ return relayoutResult;
+ }
+
+ private void updateOpacity(WindowManager.LayoutParams params, boolean dragResizing) {
+ boolean opaque = false;
+
+ if (!PixelFormat.formatHasAlpha(params.format)
+ // Don't make surface with surfaceInsets opaque as they display a
+ // translucent shadow.
+ && params.surfaceInsets.left == 0
+ && params.surfaceInsets.top == 0
+ && params.surfaceInsets.right == 0
+ && params.surfaceInsets.bottom == 0
+ // Don't make surface opaque when resizing to reduce the amount of
+ // artifacts shown in areas the app isn't drawing content to.
+ && !dragResizing) {
+ opaque = true;
+ }
+
+ if (mIsSurfaceOpaque == opaque) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mIsForWebviewOverlay) {
+ mIsSurfaceOpaque = false;
+ return;
+ }
+ mTransaction.setOpaque(mSurfaceControl, opaque).apply();
+ }
+
+ mIsSurfaceOpaque = opaque;
+ }
+
+ private void setFrame(Rect frame) {
+ mWinFrame.set(frame);
+ mInsetsController.onFrameChanged(frame);
+ }
+
+ /**
+ * Gets the current display size in which the window is being laid out, accounting for screen
+ * decorations around it.
+ */
+ void getDisplayFrame(Rect outFrame) {
+ outFrame.set(mTmpFrames.displayFrame);
+ }
+
+ /**
+ * Gets the current display size in which the window is being laid out, accounting for screen
+ * decorations around it.
+ */
+ void getWindowVisibleDisplayFrame(Rect outFrame) {
+ outFrame.set(mTmpFrames.displayFrame);
+ // XXX This is really broken, and probably all needs to be done
+ // in the window manager, and we need to know more about whether
+ // we want the area behind or in front of the IME.
+ final Rect insets = mAttachInfo.mVisibleInsets;
+ outFrame.left += insets.left;
+ outFrame.top += insets.top;
+ outFrame.right -= insets.right;
+ outFrame.bottom -= insets.bottom;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void playSoundEffect(@SoundEffectConstants.SoundEffect int effectId) {
+ checkThread();
+
+ try {
+ final AudioManager audioManager = getAudioManager();
+
+ if (mFastScrollSoundEffectsEnabled
+ && SoundEffectConstants.isNavigationRepeat(effectId)) {
+ audioManager.playSoundEffect(
+ SoundEffectConstants.nextNavigationRepeatSoundEffectId());
+ return;
+ }
+
+ switch (effectId) {
+ case SoundEffectConstants.CLICK:
+ audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
+ return;
+ case SoundEffectConstants.NAVIGATION_DOWN:
+ case SoundEffectConstants.NAVIGATION_REPEAT_DOWN:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
+ return;
+ case SoundEffectConstants.NAVIGATION_LEFT:
+ case SoundEffectConstants.NAVIGATION_REPEAT_LEFT:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
+ return;
+ case SoundEffectConstants.NAVIGATION_RIGHT:
+ case SoundEffectConstants.NAVIGATION_REPEAT_RIGHT:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
+ return;
+ case SoundEffectConstants.NAVIGATION_UP:
+ case SoundEffectConstants.NAVIGATION_REPEAT_UP:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
+ return;
+ default:
+ throw new IllegalArgumentException("unknown effect id " + effectId +
+ " not defined in " + SoundEffectConstants.class.getCanonicalName());
+ }
+ } catch (IllegalStateException e) {
+ // Exception thrown by getAudioManager() when mView is null
+ Log.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean performHapticFeedback(int effectId, boolean always) {
+ try {
+ return mWindowSession.performHapticFeedback(effectId, always);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View focusSearch(View focused, int direction) {
+ checkThread();
+ if (!(mView instanceof ViewGroup)) {
+ return null;
+ }
+ return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View keyboardNavigationClusterSearch(View currentCluster,
+ @FocusDirection int direction) {
+ checkThread();
+ return FocusFinder.getInstance().findNextKeyboardNavigationCluster(
+ mView, currentCluster, direction);
+ }
+
+ public void debug() {
+ mView.debug();
+ }
+
+ /**
+ * Export the state of {@link ViewRootImpl} and other relevant classes into a protocol buffer
+ * output stream.
+ *
+ * @param proto Stream to write the state to
+ * @param fieldId FieldId of ViewRootImpl as defined in the parent message
+ */
+ @GuardedBy("this")
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(VIEW, Objects.toString(mView));
+ proto.write(DISPLAY_ID, mDisplay.getDisplayId());
+ proto.write(APP_VISIBLE, mAppVisible);
+ proto.write(HEIGHT, mHeight);
+ proto.write(WIDTH, mWidth);
+ proto.write(IS_ANIMATING, mIsAnimating);
+ mVisRect.dumpDebug(proto, VISIBLE_RECT);
+ proto.write(IS_DRAWING, mIsDrawing);
+ proto.write(ADDED, mAdded);
+ mWinFrame.dumpDebug(proto, WIN_FRAME);
+ proto.write(LAST_WINDOW_INSETS, Objects.toString(mLastWindowInsets));
+ proto.write(SOFT_INPUT_MODE, InputMethodDebug.softInputModeToString(mSoftInputMode));
+ proto.write(SCROLL_Y, mScrollY);
+ proto.write(CUR_SCROLL_Y, mCurScrollY);
+ proto.write(REMOVED, mRemoved);
+ mWindowAttributes.dumpDebug(proto, WINDOW_ATTRIBUTES);
+ proto.end(token);
+ mInsetsController.dumpDebug(proto, INSETS_CONTROLLER);
+ mImeFocusController.dumpDebug(proto, IME_FOCUS_CONTROLLER);
+ }
+
+ /**
+ * Dump information about this ViewRootImpl
+ * @param prefix the prefix that will be prepended to each line of the produced output
+ * @param writer the writer that will receive the resulting text
+ */
+ public void dump(String prefix, PrintWriter writer) {
+ String innerPrefix = prefix + " ";
+ writer.println(prefix + "ViewRoot:");
+ writer.println(innerPrefix + "mAdded=" + mAdded);
+ writer.println(innerPrefix + "mRemoved=" + mRemoved);
+ writer.println(innerPrefix + "mStopped=" + mStopped);
+ writer.println(innerPrefix + "mPausedForTransition=" + mPausedForTransition);
+ writer.println(innerPrefix + "mConsumeBatchedInputScheduled="
+ + mConsumeBatchedInputScheduled);
+ writer.println(innerPrefix + "mConsumeBatchedInputImmediatelyScheduled="
+ + mConsumeBatchedInputImmediatelyScheduled);
+ writer.println(innerPrefix + "mPendingInputEventCount=" + mPendingInputEventCount);
+ writer.println(innerPrefix + "mProcessInputEventsScheduled="
+ + mProcessInputEventsScheduled);
+ writer.println(innerPrefix + "mTraversalScheduled=" + mTraversalScheduled);
+ if (mTraversalScheduled) {
+ writer.println(innerPrefix + " (barrier=" + mTraversalBarrier + ")");
+ }
+ writer.println(innerPrefix + "mIsAmbientMode=" + mIsAmbientMode);
+ writer.println(innerPrefix + "mUnbufferedInputSource="
+ + Integer.toHexString(mUnbufferedInputSource));
+ if (mAttachInfo != null) {
+ writer.print(innerPrefix + "mAttachInfo= ");
+ mAttachInfo.dump(innerPrefix, writer);
+ } else {
+ writer.println(innerPrefix + "mAttachInfo=<null>");
+ }
+
+ mFirstInputStage.dump(innerPrefix, writer);
+
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dump(innerPrefix, writer);
+ }
+
+ mChoreographer.dump(prefix, writer);
+
+ mInsetsController.dump(prefix, writer);
+
+ writer.println(prefix + "View Hierarchy:");
+ dumpViewHierarchy(innerPrefix, writer, mView);
+ }
+
+ private void dumpViewHierarchy(String prefix, PrintWriter writer, View view) {
+ writer.print(prefix);
+ if (view == null) {
+ writer.println("null");
+ return;
+ }
+ writer.println(view.toString());
+ if (!(view instanceof ViewGroup)) {
+ return;
+ }
+ ViewGroup grp = (ViewGroup)view;
+ final int N = grp.getChildCount();
+ if (N <= 0) {
+ return;
+ }
+ prefix = prefix + " ";
+ for (int i=0; i<N; i++) {
+ dumpViewHierarchy(prefix, writer, grp.getChildAt(i));
+ }
+ }
+
+ static final class GfxInfo {
+ public int viewCount;
+ public long renderNodeMemoryUsage;
+ public long renderNodeMemoryAllocated;
+
+ void add(GfxInfo other) {
+ viewCount += other.viewCount;
+ renderNodeMemoryUsage += other.renderNodeMemoryUsage;
+ renderNodeMemoryAllocated += other.renderNodeMemoryAllocated;
+ }
+ }
+
+ GfxInfo getGfxInfo() {
+ GfxInfo info = new GfxInfo();
+ if (mView != null) {
+ appendGfxInfo(mView, info);
+ }
+ return info;
+ }
+
+ private static void computeRenderNodeUsage(RenderNode node, GfxInfo info) {
+ if (node == null) return;
+ info.renderNodeMemoryUsage += node.computeApproximateMemoryUsage();
+ info.renderNodeMemoryAllocated += node.computeApproximateMemoryAllocated();
+ }
+
+ private static void appendGfxInfo(View view, GfxInfo info) {
+ info.viewCount++;
+ computeRenderNodeUsage(view.mRenderNode, info);
+ computeRenderNodeUsage(view.mBackgroundRenderNode, info);
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+
+ int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ appendGfxInfo(group.getChildAt(i), info);
+ }
+ }
+ }
+
+ /**
+ * @param immediate True, do now if not in traversal. False, put on queue and do later.
+ * @return True, request has been queued. False, request has been completed.
+ */
+ boolean die(boolean immediate) {
+ // Make sure we do execute immediately if we are in the middle of a traversal or the damage
+ // done by dispatchDetachedFromWindow will cause havoc on return.
+ if (immediate && !mIsInTraversal) {
+ doDie();
+ return false;
+ }
+
+ if (!mIsDrawing) {
+ destroyHardwareRenderer();
+ } else {
+ Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
+ " window=" + this + ", title=" + mWindowAttributes.getTitle());
+ }
+ mHandler.sendEmptyMessage(MSG_DIE);
+ return true;
+ }
+
+ void doDie() {
+ checkThread();
+ if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
+ synchronized (this) {
+ if (mRemoved) {
+ return;
+ }
+ mRemoved = true;
+ if (mAdded) {
+ dispatchDetachedFromWindow();
+ }
+
+ if (mAdded && !mFirst) {
+ destroyHardwareRenderer();
+
+ if (mView != null) {
+ int viewVisibility = mView.getVisibility();
+ boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
+ if (mWindowAttributesChanged || viewVisibilityChanged) {
+ // If layout params have been changed, first give them
+ // to the window manager to make sure it has the correct
+ // animation info.
+ try {
+ if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
+ & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
+ mWindowSession.finishDrawing(
+ mWindow, null /* postDrawTransaction */);
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ destroySurface();
+ }
+ }
+
+ // If our window is removed, we might not get notified about losing control.
+ // Invoking this can release the leashes as soon as possible instead of relying on GC.
+ mInsetsController.onControlsChanged(null);
+
+ mAdded = false;
+ }
+ WindowManagerGlobal.getInstance().doRemoveView(this);
+ }
+
+ public void requestUpdateConfiguration(Configuration config) {
+ Message msg = mHandler.obtainMessage(MSG_UPDATE_CONFIGURATION, config);
+ mHandler.sendMessage(msg);
+ }
+
+ public void loadSystemProperties() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Profiling
+ mProfileRendering = SystemProperties.getBoolean(PROPERTY_PROFILE_RENDERING, false);
+ profileRendering(mAttachInfo.mHasWindowFocus);
+
+ // Hardware rendering
+ if (mAttachInfo.mThreadedRenderer != null) {
+ if (mAttachInfo.mThreadedRenderer.loadSystemProperties()) {
+ invalidate();
+ }
+ }
+
+ // Layout debugging
+ boolean layout = DisplayProperties.debug_layout().orElse(false);
+ if (layout != mAttachInfo.mDebugLayout) {
+ mAttachInfo.mDebugLayout = layout;
+ if (!mHandler.hasMessages(MSG_INVALIDATE_WORLD)) {
+ mHandler.sendEmptyMessageDelayed(MSG_INVALIDATE_WORLD, 200);
+ }
+ }
+ }
+ });
+ }
+
+ private void destroyHardwareRenderer() {
+ ThreadedRenderer hardwareRenderer = mAttachInfo.mThreadedRenderer;
+
+ if (hardwareRenderer != null) {
+ if (mHardwareRendererObserver != null) {
+ hardwareRenderer.removeObserver(mHardwareRendererObserver);
+ }
+ if (mView != null) {
+ hardwareRenderer.destroyHardwareResources(mView);
+ }
+ hardwareRenderer.destroy();
+ hardwareRenderer.setRequested(false);
+
+ mAttachInfo.mThreadedRenderer = null;
+ mAttachInfo.mHardwareAccelerated = false;
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private void dispatchResized(ClientWindowFrames frames, boolean reportDraw,
+ MergedConfiguration mergedConfiguration, boolean forceLayout,
+ boolean alwaysConsumeSystemBars, int displayId) {
+ final Rect frame = frames.frame;
+ final Rect backDropFrame = frames.backdropFrame;
+ if (DEBUG_LAYOUT) Log.v(mTag, "Resizing " + this + ": frame=" + frame.toShortString()
+ + " reportDraw=" + reportDraw
+ + " backDropFrame=" + backDropFrame);
+
+ // Tell all listeners that we are resizing the window so that the chrome can get
+ // updated as fast as possible on a separate thread,
+ if (mDragResizing && mUseMTRenderer) {
+ boolean fullscreen = frame.equals(backDropFrame);
+ synchronized (mWindowCallbacks) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onWindowSizeIsChanging(backDropFrame, fullscreen,
+ mAttachInfo.mVisibleInsets, mAttachInfo.mStableInsets);
+ }
+ }
+ }
+
+ Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED);
+ if (mTranslator != null) {
+ mTranslator.translateRectInScreenToAppWindow(frame);
+ }
+ SomeArgs args = SomeArgs.obtain();
+ final boolean sameProcessCall = (Binder.getCallingPid() == android.os.Process.myPid());
+ args.arg1 = sameProcessCall ? new ClientWindowFrames(frames) : frames;
+ args.arg2 = sameProcessCall && mergedConfiguration != null
+ ? new MergedConfiguration(mergedConfiguration) : mergedConfiguration;
+ args.argi1 = forceLayout ? 1 : 0;
+ args.argi2 = alwaysConsumeSystemBars ? 1 : 0;
+ args.argi3 = displayId;
+ msg.obj = args;
+ mHandler.sendMessage(msg);
+ }
+
+ private void dispatchInsetsChanged(InsetsState insetsState, boolean willMove,
+ boolean willResize) {
+ if (Binder.getCallingPid() == android.os.Process.myPid()) {
+ insetsState = new InsetsState(insetsState, true /* copySource */);
+ }
+ if (mTranslator != null) {
+ mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
+ }
+ if (insetsState != null && insetsState.getSource(ITYPE_IME).isVisible()) {
+ ImeTracing.getInstance().triggerClientDump("ViewRootImpl#dispatchInsetsChanged",
+ getInsetsController().getHost().getInputMethodManager(), null /* icProto */);
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = insetsState;
+ args.argi1 = willMove ? 1 : 0;
+ args.argi2 = willResize ? 1 : 0;
+ mHandler.obtainMessage(MSG_INSETS_CHANGED, args).sendToTarget();
+ }
+
+ private void dispatchInsetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls, boolean willMove, boolean willResize) {
+ if (Binder.getCallingPid() == android.os.Process.myPid()) {
+ insetsState = new InsetsState(insetsState, true /* copySource */);
+ if (activeControls != null) {
+ for (int i = activeControls.length - 1; i >= 0; i--) {
+ activeControls[i] = new InsetsSourceControl(activeControls[i]);
+ }
+ }
+ }
+ if (mTranslator != null) {
+ mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
+ mTranslator.translateSourceControlsInScreenToAppWindow(activeControls);
+ }
+ if (insetsState != null && insetsState.getSource(ITYPE_IME).isVisible()) {
+ ImeTracing.getInstance().triggerClientDump("ViewRootImpl#dispatchInsetsControlChanged",
+ getInsetsController().getHost().getInputMethodManager(), null /* icProto */);
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = insetsState;
+ args.arg2 = activeControls;
+ args.argi1 = willMove ? 1 : 0;
+ args.argi2 = willResize ? 1 : 0;
+ mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
+ }
+
+ private void showInsets(@InsetsType int types, boolean fromIme) {
+ mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+ }
+
+ private void hideInsets(@InsetsType int types, boolean fromIme) {
+ mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+ }
+
+ public void dispatchMoved(int newX, int newY) {
+ if (DEBUG_LAYOUT) Log.v(mTag, "Window moved " + this + ": newX=" + newX + " newY=" + newY);
+ if (mTranslator != null) {
+ PointF point = new PointF(newX, newY);
+ mTranslator.translatePointInScreenToAppWindow(point);
+ newX = (int) (point.x + 0.5);
+ newY = (int) (point.y + 0.5);
+ }
+ Message msg = mHandler.obtainMessage(MSG_WINDOW_MOVED, newX, newY);
+ mHandler.sendMessage(msg);
+ }
+
+ /**
+ * Represents a pending input event that is waiting in a queue.
+ *
+ * Input events are processed in serial order by the timestamp specified by
+ * {@link InputEvent#getEventTimeNano()}. In general, the input dispatcher delivers
+ * one input event to the application at a time and waits for the application
+ * to finish handling it before delivering the next one.
+ *
+ * However, because the application or IME can synthesize and inject multiple
+ * key events at a time without going through the input dispatcher, we end up
+ * needing a queue on the application's side.
+ */
+ private static final class QueuedInputEvent {
+ public static final int FLAG_DELIVER_POST_IME = 1 << 0;
+ public static final int FLAG_DEFERRED = 1 << 1;
+ public static final int FLAG_FINISHED = 1 << 2;
+ public static final int FLAG_FINISHED_HANDLED = 1 << 3;
+ public static final int FLAG_RESYNTHESIZED = 1 << 4;
+ public static final int FLAG_UNHANDLED = 1 << 5;
+ public static final int FLAG_MODIFIED_FOR_COMPATIBILITY = 1 << 6;
+
+ public QueuedInputEvent mNext;
+
+ public InputEvent mEvent;
+ public InputEventReceiver mReceiver;
+ public int mFlags;
+
+ public boolean shouldSkipIme() {
+ if ((mFlags & FLAG_DELIVER_POST_IME) != 0) {
+ return true;
+ }
+ return mEvent instanceof MotionEvent
+ && (mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
+ || mEvent.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER));
+ }
+
+ public boolean shouldSendToSynthesizer() {
+ if ((mFlags & FLAG_UNHANDLED) != 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("QueuedInputEvent{flags=");
+ boolean hasPrevious = false;
+ hasPrevious = flagToString("DELIVER_POST_IME", FLAG_DELIVER_POST_IME, hasPrevious, sb);
+ hasPrevious = flagToString("DEFERRED", FLAG_DEFERRED, hasPrevious, sb);
+ hasPrevious = flagToString("FINISHED", FLAG_FINISHED, hasPrevious, sb);
+ hasPrevious = flagToString("FINISHED_HANDLED", FLAG_FINISHED_HANDLED, hasPrevious, sb);
+ hasPrevious = flagToString("RESYNTHESIZED", FLAG_RESYNTHESIZED, hasPrevious, sb);
+ hasPrevious = flagToString("UNHANDLED", FLAG_UNHANDLED, hasPrevious, sb);
+ if (!hasPrevious) {
+ sb.append("0");
+ }
+ sb.append(", hasNextQueuedEvent=" + (mEvent != null ? "true" : "false"));
+ sb.append(", hasInputEventReceiver=" + (mReceiver != null ? "true" : "false"));
+ sb.append(", mEvent=" + mEvent + "}");
+ return sb.toString();
+ }
+
+ private boolean flagToString(String name, int flag,
+ boolean hasPrevious, StringBuilder sb) {
+ if ((mFlags & flag) != 0) {
+ if (hasPrevious) {
+ sb.append("|");
+ }
+ sb.append(name);
+ return true;
+ }
+ return hasPrevious;
+ }
+ }
+
+ private QueuedInputEvent obtainQueuedInputEvent(InputEvent event,
+ InputEventReceiver receiver, int flags) {
+ QueuedInputEvent q = mQueuedInputEventPool;
+ if (q != null) {
+ mQueuedInputEventPoolSize -= 1;
+ mQueuedInputEventPool = q.mNext;
+ q.mNext = null;
+ } else {
+ q = new QueuedInputEvent();
+ }
+
+ q.mEvent = event;
+ q.mReceiver = receiver;
+ q.mFlags = flags;
+ return q;
+ }
+
+ private void recycleQueuedInputEvent(QueuedInputEvent q) {
+ q.mEvent = null;
+ q.mReceiver = null;
+
+ if (mQueuedInputEventPoolSize < MAX_QUEUED_INPUT_EVENT_POOL_SIZE) {
+ mQueuedInputEventPoolSize += 1;
+ q.mNext = mQueuedInputEventPool;
+ mQueuedInputEventPool = q;
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ void enqueueInputEvent(InputEvent event) {
+ enqueueInputEvent(event, null, 0, false);
+ }
+
+ @UnsupportedAppUsage
+ void enqueueInputEvent(InputEvent event,
+ InputEventReceiver receiver, int flags, boolean processImmediately) {
+ QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
+
+ if (event instanceof MotionEvent) {
+ MotionEvent me = (MotionEvent) event;
+ if (me.getAction() == MotionEvent.ACTION_CANCEL) {
+ EventLog.writeEvent(EventLogTags.VIEW_ENQUEUE_INPUT_EVENT, "Motion - Cancel",
+ getTitle());
+ }
+ } else if (event instanceof KeyEvent) {
+ KeyEvent ke = (KeyEvent) event;
+ if (ke.isCanceled()) {
+ EventLog.writeEvent(EventLogTags.VIEW_ENQUEUE_INPUT_EVENT, "Key - Cancel",
+ getTitle());
+ }
+ }
+ // Always enqueue the input event in order, regardless of its time stamp.
+ // We do this because the application or the IME may inject key events
+ // in response to touch events and we want to ensure that the injected keys
+ // are processed in the order they were received and we cannot trust that
+ // the time stamp of injected events are monotonic.
+ QueuedInputEvent last = mPendingInputEventTail;
+ if (last == null) {
+ mPendingInputEventHead = q;
+ mPendingInputEventTail = q;
+ } else {
+ last.mNext = q;
+ mPendingInputEventTail = q;
+ }
+ mPendingInputEventCount += 1;
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
+ mPendingInputEventCount);
+
+ if (processImmediately) {
+ doProcessInputEvents();
+ } else {
+ scheduleProcessInputEvents();
+ }
+ }
+
+ private void scheduleProcessInputEvents() {
+ if (!mProcessInputEventsScheduled) {
+ mProcessInputEventsScheduled = true;
+ Message msg = mHandler.obtainMessage(MSG_PROCESS_INPUT_EVENTS);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ void doProcessInputEvents() {
+ // Deliver all pending input events in the queue.
+ while (mPendingInputEventHead != null) {
+ QueuedInputEvent q = mPendingInputEventHead;
+ mPendingInputEventHead = q.mNext;
+ if (mPendingInputEventHead == null) {
+ mPendingInputEventTail = null;
+ }
+ q.mNext = null;
+
+ mPendingInputEventCount -= 1;
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
+ mPendingInputEventCount);
+
+ mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent));
+
+ deliverInputEvent(q);
+ }
+
+ // We are done processing all input events that we can process right now
+ // so we can clear the pending flag immediately.
+ if (mProcessInputEventsScheduled) {
+ mProcessInputEventsScheduled = false;
+ mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
+ }
+ }
+
+ private void deliverInputEvent(QueuedInputEvent q) {
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
+ q.mEvent.getId());
+
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent src=0x"
+ + Integer.toHexString(q.mEvent.getSource()) + " eventTimeNano="
+ + q.mEvent.getEventTimeNano() + " id=0x"
+ + Integer.toHexString(q.mEvent.getId()));
+ }
+ try {
+ if (mInputEventConsistencyVerifier != null) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "verifyEventConsistency");
+ try {
+ mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ InputStage stage;
+ if (q.shouldSendToSynthesizer()) {
+ stage = mSyntheticInputStage;
+ } else {
+ stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
+ }
+
+ if (q.mEvent instanceof KeyEvent) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "preDispatchToUnhandledKeyManager");
+ try {
+ mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ if (stage != null) {
+ handleWindowFocusChanged();
+ stage.deliver(q);
+ } else {
+ finishInputEvent(q);
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
+ private void finishInputEvent(QueuedInputEvent q) {
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
+ q.mEvent.getId());
+
+ if (q.mReceiver != null) {
+ boolean handled = (q.mFlags & QueuedInputEvent.FLAG_FINISHED_HANDLED) != 0;
+ boolean modified = (q.mFlags & QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY) != 0;
+ if (modified) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventBeforeFinish");
+ InputEvent processedEvent;
+ try {
+ processedEvent =
+ mInputCompatProcessor.processInputEventBeforeFinish(q.mEvent);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ if (processedEvent != null) {
+ q.mReceiver.finishInputEvent(processedEvent, handled);
+ }
+ } else {
+ q.mReceiver.finishInputEvent(q.mEvent, handled);
+ }
+ } else {
+ q.mEvent.recycleIfNeededAfterDispatch();
+ }
+
+ recycleQueuedInputEvent(q);
+ }
+
+ static boolean isTerminalInputEvent(InputEvent event) {
+ if (event instanceof KeyEvent) {
+ final KeyEvent keyEvent = (KeyEvent)event;
+ return keyEvent.getAction() == KeyEvent.ACTION_UP;
+ } else {
+ final MotionEvent motionEvent = (MotionEvent)event;
+ final int action = motionEvent.getAction();
+ return action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_HOVER_EXIT;
+ }
+ }
+
+ void scheduleConsumeBatchedInput() {
+ // If anything is currently scheduled to consume batched input then there's no point in
+ // scheduling it again.
+ if (!mConsumeBatchedInputScheduled && !mConsumeBatchedInputImmediatelyScheduled) {
+ mConsumeBatchedInputScheduled = true;
+ mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
+ mConsumedBatchedInputRunnable, null);
+ }
+ }
+
+ void unscheduleConsumeBatchedInput() {
+ if (mConsumeBatchedInputScheduled) {
+ mConsumeBatchedInputScheduled = false;
+ mChoreographer.removeCallbacks(Choreographer.CALLBACK_INPUT,
+ mConsumedBatchedInputRunnable, null);
+ }
+ }
+
+ void scheduleConsumeBatchedInputImmediately() {
+ if (!mConsumeBatchedInputImmediatelyScheduled) {
+ unscheduleConsumeBatchedInput();
+ mConsumeBatchedInputImmediatelyScheduled = true;
+ mHandler.post(mConsumeBatchedInputImmediatelyRunnable);
+ }
+ }
+
+ boolean doConsumeBatchedInput(long frameTimeNanos) {
+ final boolean consumedBatches;
+ if (mInputEventReceiver != null) {
+ consumedBatches = mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos);
+ } else {
+ consumedBatches = false;
+ }
+ doProcessInputEvents();
+ return consumedBatches;
+ }
+
+ final class TraversalRunnable implements Runnable {
+ @Override
+ public void run() {
+ doTraversal();
+ }
+ }
+ final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
+
+ final class WindowInputEventReceiver extends InputEventReceiver {
+ public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
+
+ @Override
+ public void onInputEvent(InputEvent event) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
+ List<InputEvent> processedEvents;
+ try {
+ processedEvents =
+ mInputCompatProcessor.processInputEventForCompatibility(event);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ if (processedEvents != null) {
+ if (processedEvents.isEmpty()) {
+ // InputEvent consumed by mInputCompatProcessor
+ finishInputEvent(event, true);
+ } else {
+ for (int i = 0; i < processedEvents.size(); i++) {
+ enqueueInputEvent(
+ processedEvents.get(i), this,
+ QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
+ }
+ }
+ } else {
+ enqueueInputEvent(event, this, 0, true);
+ }
+ }
+
+ @Override
+ public void onBatchedInputEventPending(int source) {
+ final boolean unbuffered = mUnbufferedInputDispatch
+ || (source & mUnbufferedInputSource) != SOURCE_CLASS_NONE;
+ if (unbuffered) {
+ if (mConsumeBatchedInputScheduled) {
+ unscheduleConsumeBatchedInput();
+ }
+ // Consume event immediately if unbuffered input dispatch has been requested.
+ consumeBatchedInputEvents(-1);
+ return;
+ }
+ scheduleConsumeBatchedInput();
+ }
+
+ @Override
+ public void onFocusEvent(boolean hasFocus, boolean inTouchMode) {
+ windowFocusChanged(hasFocus, inTouchMode);
+ }
+
+ @Override
+ public void onPointerCaptureEvent(boolean pointerCaptureEnabled) {
+ dispatchPointerCaptureChanged(pointerCaptureEnabled);
+ }
+
+ @Override
+ public void onDragEvent(boolean isExiting, float x, float y) {
+ // force DRAG_EXITED_EVENT if appropriate
+ DragEvent event = DragEvent.obtain(
+ isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION,
+ x, y, 0 /* offsetX */, 0 /* offsetY */, null/* localState */,
+ null/* description */, null /* data */, null /* dragSurface */,
+ null /* dragAndDropPermissions */, false /* result */);
+ dispatchDragEvent(event);
+ }
+
+ @Override
+ public void dispose() {
+ unscheduleConsumeBatchedInput();
+ super.dispose();
+ }
+ }
+ private WindowInputEventReceiver mInputEventReceiver;
+
+ final class InputMetricsListener
+ implements HardwareRendererObserver.OnFrameMetricsAvailableListener {
+ public long[] data = new long[FrameMetrics.Index.FRAME_STATS_COUNT];
+
+ @Override
+ public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
+ final int inputEventId = (int) data[FrameMetrics.Index.INPUT_EVENT_ID];
+ if (inputEventId == INVALID_INPUT_EVENT_ID) {
+ return;
+ }
+ final long presentTime = data[FrameMetrics.Index.DISPLAY_PRESENT_TIME];
+ if (presentTime <= 0) {
+ // Present time is not available for this frame. If the present time is not
+ // available, we cannot compute end-to-end input latency metrics.
+ return;
+ }
+ final long gpuCompletedTime = data[FrameMetrics.Index.GPU_COMPLETED];
+ if (mInputEventReceiver == null) {
+ return;
+ }
+ if (gpuCompletedTime >= presentTime) {
+ final double discrepancyMs = (gpuCompletedTime - presentTime) * 1E-6;
+ final long vsyncId = data[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
+ Log.w(TAG, "Not reporting timeline because gpuCompletedTime is " + discrepancyMs
+ + "ms ahead of presentTime. FRAME_TIMELINE_VSYNC_ID=" + vsyncId
+ + ", INPUT_EVENT_ID=" + inputEventId);
+ // TODO(b/186664409): figure out why this sometimes happens
+ return;
+ }
+ mInputEventReceiver.reportTimeline(inputEventId, gpuCompletedTime, presentTime);
+ }
+ }
+ HardwareRendererObserver mHardwareRendererObserver;
+
+ final class ConsumeBatchedInputRunnable implements Runnable {
+ @Override
+ public void run() {
+ mConsumeBatchedInputScheduled = false;
+ if (doConsumeBatchedInput(mChoreographer.getFrameTimeNanos())) {
+ // If we consumed a batch here, we want to go ahead and schedule the
+ // consumption of batched input events on the next frame. Otherwise, we would
+ // wait until we have more input events pending and might get starved by other
+ // things occurring in the process.
+ scheduleConsumeBatchedInput();
+ }
+ }
+ }
+ final ConsumeBatchedInputRunnable mConsumedBatchedInputRunnable =
+ new ConsumeBatchedInputRunnable();
+ boolean mConsumeBatchedInputScheduled;
+
+ final class ConsumeBatchedInputImmediatelyRunnable implements Runnable {
+ @Override
+ public void run() {
+ mConsumeBatchedInputImmediatelyScheduled = false;
+ doConsumeBatchedInput(-1);
+ }
+ }
+ final ConsumeBatchedInputImmediatelyRunnable mConsumeBatchedInputImmediatelyRunnable =
+ new ConsumeBatchedInputImmediatelyRunnable();
+ boolean mConsumeBatchedInputImmediatelyScheduled;
+
+ final class InvalidateOnAnimationRunnable implements Runnable {
+ private boolean mPosted;
+ private final ArrayList<View> mViews = new ArrayList<View>();
+ private final ArrayList<AttachInfo.InvalidateInfo> mViewRects =
+ new ArrayList<AttachInfo.InvalidateInfo>();
+ private View[] mTempViews;
+ private AttachInfo.InvalidateInfo[] mTempViewRects;
+
+ public void addView(View view) {
+ synchronized (this) {
+ mViews.add(view);
+ postIfNeededLocked();
+ }
+ }
+
+ public void addViewRect(AttachInfo.InvalidateInfo info) {
+ synchronized (this) {
+ mViewRects.add(info);
+ postIfNeededLocked();
+ }
+ }
+
+ public void removeView(View view) {
+ synchronized (this) {
+ mViews.remove(view);
+
+ for (int i = mViewRects.size(); i-- > 0; ) {
+ AttachInfo.InvalidateInfo info = mViewRects.get(i);
+ if (info.target == view) {
+ mViewRects.remove(i);
+ info.recycle();
+ }
+ }
+
+ if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {
+ mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);
+ mPosted = false;
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ final int viewCount;
+ final int viewRectCount;
+ synchronized (this) {
+ mPosted = false;
+
+ viewCount = mViews.size();
+ if (viewCount != 0) {
+ mTempViews = mViews.toArray(mTempViews != null
+ ? mTempViews : new View[viewCount]);
+ mViews.clear();
+ }
+
+ viewRectCount = mViewRects.size();
+ if (viewRectCount != 0) {
+ mTempViewRects = mViewRects.toArray(mTempViewRects != null
+ ? mTempViewRects : new AttachInfo.InvalidateInfo[viewRectCount]);
+ mViewRects.clear();
+ }
+ }
+
+ for (int i = 0; i < viewCount; i++) {
+ mTempViews[i].invalidate();
+ mTempViews[i] = null;
+ }
+
+ for (int i = 0; i < viewRectCount; i++) {
+ final View.AttachInfo.InvalidateInfo info = mTempViewRects[i];
+ info.target.invalidate(info.left, info.top, info.right, info.bottom);
+ info.recycle();
+ }
+ }
+
+ private void postIfNeededLocked() {
+ if (!mPosted) {
+ mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
+ mPosted = true;
+ }
+ }
+ }
+ final InvalidateOnAnimationRunnable mInvalidateOnAnimationRunnable =
+ new InvalidateOnAnimationRunnable();
+
+ public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
+ Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
+ mHandler.sendMessageDelayed(msg, delayMilliseconds);
+ }
+
+ public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info,
+ long delayMilliseconds) {
+ final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
+ mHandler.sendMessageDelayed(msg, delayMilliseconds);
+ }
+
+ public void dispatchInvalidateOnAnimation(View view) {
+ mInvalidateOnAnimationRunnable.addView(view);
+ }
+
+ public void dispatchInvalidateRectOnAnimation(AttachInfo.InvalidateInfo info) {
+ mInvalidateOnAnimationRunnable.addViewRect(info);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void cancelInvalidate(View view) {
+ mHandler.removeMessages(MSG_INVALIDATE, view);
+ // fixme: might leak the AttachInfo.InvalidateInfo objects instead of returning
+ // them to the pool
+ mHandler.removeMessages(MSG_INVALIDATE_RECT, view);
+ mInvalidateOnAnimationRunnable.removeView(view);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void dispatchInputEvent(InputEvent event) {
+ dispatchInputEvent(event, null);
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = event;
+ args.arg2 = receiver;
+ Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ }
+
+ public void synthesizeInputEvent(InputEvent event) {
+ Message msg = mHandler.obtainMessage(MSG_SYNTHESIZE_INPUT_EVENT, event);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ }
+
+ @UnsupportedAppUsage
+ public void dispatchKeyFromIme(KeyEvent event) {
+ Message msg = mHandler.obtainMessage(MSG_DISPATCH_KEY_FROM_IME, event);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ }
+
+ public void dispatchKeyFromAutofill(KeyEvent event) {
+ Message msg = mHandler.obtainMessage(MSG_DISPATCH_KEY_FROM_AUTOFILL, event);
+ msg.setAsynchronous(true);
+ mHandler.sendMessage(msg);
+ }
+
+ /**
+ * Reinject unhandled {@link InputEvent}s in order to synthesize fallbacks events.
+ *
+ * Note that it is the responsibility of the caller of this API to recycle the InputEvent it
+ * passes in.
+ */
+ @UnsupportedAppUsage
+ public void dispatchUnhandledInputEvent(InputEvent event) {
+ if (event instanceof MotionEvent) {
+ event = MotionEvent.obtain((MotionEvent) event);
+ }
+ synthesizeInputEvent(event);
+ }
+
+ public void dispatchAppVisibility(boolean visible) {
+ Message msg = mHandler.obtainMessage(MSG_DISPATCH_APP_VISIBILITY);
+ msg.arg1 = visible ? 1 : 0;
+ mHandler.sendMessage(msg);
+ }
+
+ public void dispatchGetNewSurface() {
+ Message msg = mHandler.obtainMessage(MSG_DISPATCH_GET_NEW_SURFACE);
+ mHandler.sendMessage(msg);
+ }
+
+ /**
+ * Dispatch the offset changed.
+ *
+ * @param offset the offset of this view in the parent window.
+ */
+ public void dispatchLocationInParentDisplayChanged(Point offset) {
+ Message msg =
+ mHandler.obtainMessage(MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED, offset.x, offset.y);
+ mHandler.sendMessage(msg);
+ }
+
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
+ synchronized (this) {
+ mWindowFocusChanged = true;
+ mUpcomingWindowFocus = hasFocus;
+ mUpcomingInTouchMode = inTouchMode;
+ }
+ Message msg = Message.obtain();
+ msg.what = MSG_WINDOW_FOCUS_CHANGED;
+ mHandler.sendMessage(msg);
+ }
+
+ public void dispatchWindowShown() {
+ mHandler.sendEmptyMessage(MSG_DISPATCH_WINDOW_SHOWN);
+ }
+
+ public void dispatchCloseSystemDialogs(String reason) {
+ Message msg = Message.obtain();
+ msg.what = MSG_CLOSE_SYSTEM_DIALOGS;
+ msg.obj = reason;
+ mHandler.sendMessage(msg);
+ }
+
+ public void dispatchDragEvent(DragEvent event) {
+ final int what;
+ if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
+ what = MSG_DISPATCH_DRAG_LOCATION_EVENT;
+ mHandler.removeMessages(what);
+ } else {
+ what = MSG_DISPATCH_DRAG_EVENT;
+ }
+ Message msg = mHandler.obtainMessage(what, event);
+ mHandler.sendMessage(msg);
+ }
+
+ public void updatePointerIcon(float x, float y) {
+ final int what = MSG_UPDATE_POINTER_ICON;
+ mHandler.removeMessages(what);
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent event = MotionEvent.obtain(
+ 0, now, MotionEvent.ACTION_HOVER_MOVE, x, y, 0);
+ Message msg = mHandler.obtainMessage(what, event);
+ mHandler.sendMessage(msg);
+ }
+
+ public void dispatchCheckFocus() {
+ if (!mHandler.hasMessages(MSG_CHECK_FOCUS)) {
+ // This will result in a call to checkFocus() below.
+ mHandler.sendEmptyMessage(MSG_CHECK_FOCUS);
+ }
+ }
+
+ public void dispatchRequestKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
+ mHandler.obtainMessage(
+ MSG_REQUEST_KEYBOARD_SHORTCUTS, deviceId, 0, receiver).sendToTarget();
+ }
+
+ private void dispatchPointerCaptureChanged(boolean on) {
+ final int what = MSG_POINTER_CAPTURE_CHANGED;
+ mHandler.removeMessages(what);
+ Message msg = mHandler.obtainMessage(what);
+ msg.arg1 = on ? 1 : 0;
+ mHandler.sendMessage(msg);
+ }
+
+ /**
+ * Post a callback to send a
+ * {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event.
+ * This event is send at most once every
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
+ */
+ private void postSendWindowContentChangedCallback(View source, int changeType) {
+ if (mSendWindowContentChangedAccessibilityEvent == null) {
+ mSendWindowContentChangedAccessibilityEvent =
+ new SendWindowContentChangedAccessibilityEvent();
+ }
+ mSendWindowContentChangedAccessibilityEvent.runOrPost(source, changeType);
+ }
+
+ /**
+ * Remove a posted callback to send a
+ * {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event.
+ */
+ private void removeSendWindowContentChangedCallback() {
+ if (mSendWindowContentChangedAccessibilityEvent != null) {
+ mHandler.removeCallbacks(mSendWindowContentChangedAccessibilityEvent);
+ }
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+ return false;
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView, float x, float y) {
+ return false;
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback) {
+ return null;
+ }
+
+ @Override
+ public ActionMode startActionModeForChild(
+ View originalView, ActionMode.Callback callback, int type) {
+ return null;
+ }
+
+ @Override
+ public void createContextMenu(ContextMenu menu) {
+ }
+
+ @Override
+ public void childDrawableStateChanged(View child) {
+ }
+
+ @Override
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ if (mView == null || mStopped || mPausedForTransition) {
+ return false;
+ }
+
+ // Immediately flush pending content changed event (if any) to preserve event order
+ if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+ && mSendWindowContentChangedAccessibilityEvent != null
+ && mSendWindowContentChangedAccessibilityEvent.mSource != null) {
+ mSendWindowContentChangedAccessibilityEvent.removeCallbacksAndRun();
+ }
+
+ // Intercept accessibility focus events fired by virtual nodes to keep
+ // track of accessibility focus position in such nodes.
+ final int eventType = event.getEventType();
+ final View source = getSourceForAccessibilityEvent(event);
+ switch (eventType) {
+ case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
+ if (source != null) {
+ AccessibilityNodeProvider provider = source.getAccessibilityNodeProvider();
+ if (provider != null) {
+ final int virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId(
+ event.getSourceNodeId());
+ final AccessibilityNodeInfo node;
+ node = provider.createAccessibilityNodeInfo(virtualNodeId);
+ setAccessibilityFocus(source, node);
+ }
+ }
+ } break;
+ case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
+ if (source != null && source.getAccessibilityNodeProvider() != null) {
+ setAccessibilityFocus(null, null);
+ }
+ } break;
+
+
+ case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
+ handleWindowContentChangedEvent(event);
+ } break;
+ }
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ return true;
+ }
+
+ private View getSourceForAccessibilityEvent(AccessibilityEvent event) {
+ final long sourceNodeId = event.getSourceNodeId();
+ final int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(
+ sourceNodeId);
+ return AccessibilityNodeIdManager.getInstance().findView(accessibilityViewId);
+ }
+
+ /**
+ * Updates the focused virtual view, when necessary, in response to a
+ * content changed event.
+ * <p>
+ * This is necessary to get updated bounds after a position change.
+ *
+ * @param event an accessibility event of type
+ * {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED}
+ */
+ private void handleWindowContentChangedEvent(AccessibilityEvent event) {
+ final View focusedHost = mAccessibilityFocusedHost;
+ if (focusedHost == null || mAccessibilityFocusedVirtualView == null) {
+ // No virtual view focused, nothing to do here.
+ return;
+ }
+
+ final AccessibilityNodeProvider provider = focusedHost.getAccessibilityNodeProvider();
+ if (provider == null) {
+ // Error state: virtual view with no provider. Clear focus.
+ mAccessibilityFocusedHost = null;
+ mAccessibilityFocusedVirtualView = null;
+ focusedHost.clearAccessibilityFocusNoCallbacks(0);
+ return;
+ }
+
+ // We only care about change types that may affect the bounds of the
+ // focused virtual view.
+ final int changes = event.getContentChangeTypes();
+ if ((changes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) == 0
+ && changes != AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED) {
+ return;
+ }
+
+ final long eventSourceNodeId = event.getSourceNodeId();
+ final int changedViewId = AccessibilityNodeInfo.getAccessibilityViewId(eventSourceNodeId);
+
+ // Search up the tree for subtree containment.
+ boolean hostInSubtree = false;
+ View root = mAccessibilityFocusedHost;
+ while (root != null && !hostInSubtree) {
+ if (changedViewId == root.getAccessibilityViewId()) {
+ hostInSubtree = true;
+ } else {
+ final ViewParent parent = root.getParent();
+ if (parent instanceof View) {
+ root = (View) parent;
+ } else {
+ root = null;
+ }
+ }
+ }
+
+ // We care only about changes in subtrees containing the host view.
+ if (!hostInSubtree) {
+ return;
+ }
+
+ final long focusedSourceNodeId = mAccessibilityFocusedVirtualView.getSourceNodeId();
+ int focusedChildId = AccessibilityNodeInfo.getVirtualDescendantId(focusedSourceNodeId);
+
+ // Refresh the node for the focused virtual view.
+ final Rect oldBounds = mTempRect;
+ mAccessibilityFocusedVirtualView.getBoundsInScreen(oldBounds);
+ mAccessibilityFocusedVirtualView = provider.createAccessibilityNodeInfo(focusedChildId);
+ if (mAccessibilityFocusedVirtualView == null) {
+ // Error state: The node no longer exists. Clear focus.
+ mAccessibilityFocusedHost = null;
+ focusedHost.clearAccessibilityFocusNoCallbacks(0);
+
+ // This will probably fail, but try to keep the provider's internal
+ // state consistent by clearing focus.
+ provider.performAction(focusedChildId,
+ AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS.getId(), null);
+ invalidateRectOnScreen(oldBounds);
+ } else {
+ // The node was refreshed, invalidate bounds if necessary.
+ final Rect newBounds = mAccessibilityFocusedVirtualView.getBoundsInScreen();
+ if (!oldBounds.equals(newBounds)) {
+ oldBounds.union(newBounds);
+ invalidateRectOnScreen(oldBounds);
+ }
+ }
+ }
+
+ @Override
+ public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+ postSendWindowContentChangedCallback(Preconditions.checkNotNull(source), changeType);
+ }
+
+ @Override
+ public boolean canResolveLayoutDirection() {
+ return true;
+ }
+
+ @Override
+ public boolean isLayoutDirectionResolved() {
+ return true;
+ }
+
+ @Override
+ public int getLayoutDirection() {
+ return View.LAYOUT_DIRECTION_RESOLVED_DEFAULT;
+ }
+
+ @Override
+ public boolean canResolveTextDirection() {
+ return true;
+ }
+
+ @Override
+ public boolean isTextDirectionResolved() {
+ return true;
+ }
+
+ @Override
+ public int getTextDirection() {
+ return View.TEXT_DIRECTION_RESOLVED_DEFAULT;
+ }
+
+ @Override
+ public boolean canResolveTextAlignment() {
+ return true;
+ }
+
+ @Override
+ public boolean isTextAlignmentResolved() {
+ return true;
+ }
+
+ @Override
+ public int getTextAlignment() {
+ return View.TEXT_ALIGNMENT_RESOLVED_DEFAULT;
+ }
+
+ private View getCommonPredecessor(View first, View second) {
+ if (mTempHashSet == null) {
+ mTempHashSet = new HashSet<View>();
+ }
+ HashSet<View> seen = mTempHashSet;
+ seen.clear();
+ View firstCurrent = first;
+ while (firstCurrent != null) {
+ seen.add(firstCurrent);
+ ViewParent firstCurrentParent = firstCurrent.mParent;
+ if (firstCurrentParent instanceof View) {
+ firstCurrent = (View) firstCurrentParent;
+ } else {
+ firstCurrent = null;
+ }
+ }
+ View secondCurrent = second;
+ while (secondCurrent != null) {
+ if (seen.contains(secondCurrent)) {
+ seen.clear();
+ return secondCurrent;
+ }
+ ViewParent secondCurrentParent = secondCurrent.mParent;
+ if (secondCurrentParent instanceof View) {
+ secondCurrent = (View) secondCurrentParent;
+ } else {
+ secondCurrent = null;
+ }
+ }
+ seen.clear();
+ return null;
+ }
+
+ void checkThread() {
+ if (mThread != Thread.currentThread()) {
+ throw new CalledFromWrongThreadException(
+ "Only the original thread that created a view hierarchy can touch its views.");
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ // ViewAncestor never intercepts touch event, so this can be a no-op
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
+ if (rectangle == null) {
+ return scrollToRectOrFocus(null, immediate);
+ }
+ rectangle.offset(child.getLeft() - child.getScrollX(),
+ child.getTop() - child.getScrollY());
+ final boolean scrolled = scrollToRectOrFocus(rectangle, immediate);
+ mTempRect.set(rectangle);
+ mTempRect.offset(0, -mCurScrollY);
+ mTempRect.offset(mAttachInfo.mWindowLeft, mAttachInfo.mWindowTop);
+ try {
+ mWindowSession.onRectangleOnScreenRequested(mWindow, mTempRect);
+ } catch (RemoteException re) {
+ /* ignore */
+ }
+ return scrolled;
+ }
+
+ @Override
+ public void childHasTransientStateChanged(View child, boolean hasTransientState) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ return false;
+ }
+
+ @Override
+ public void onStopNestedScroll(View target) {
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ return false;
+ }
+
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ return false;
+ }
+
+ @Override
+ public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+ return false;
+ }
+
+ /**
+ * Adds a scroll capture callback to this window.
+ *
+ * @param callback the callback to add
+ */
+ public void addScrollCaptureCallback(ScrollCaptureCallback callback) {
+ if (mRootScrollCaptureCallbacks == null) {
+ mRootScrollCaptureCallbacks = new HashSet<>();
+ }
+ mRootScrollCaptureCallbacks.add(callback);
+ }
+
+ /**
+ * Removes a scroll capture callback from this window.
+ *
+ * @param callback the callback to remove
+ */
+ public void removeScrollCaptureCallback(ScrollCaptureCallback callback) {
+ if (mRootScrollCaptureCallbacks != null) {
+ mRootScrollCaptureCallbacks.remove(callback);
+ if (mRootScrollCaptureCallbacks.isEmpty()) {
+ mRootScrollCaptureCallbacks = null;
+ }
+ }
+ }
+
+ /**
+ * Dispatches a scroll capture request to the view hierarchy on the ui thread.
+ *
+ * @param listener for the response
+ */
+ public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureResponseListener listener) {
+ mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, listener).sendToTarget();
+ }
+
+ /**
+ * Collect and include any ScrollCaptureCallback instances registered with the window.
+ *
+ * @see #addScrollCaptureCallback(ScrollCaptureCallback)
+ * @param results an object to collect the results of the search
+ */
+ private void collectRootScrollCaptureTargets(ScrollCaptureSearchResults results) {
+ if (mRootScrollCaptureCallbacks == null) {
+ return;
+ }
+ for (ScrollCaptureCallback cb : mRootScrollCaptureCallbacks) {
+ // Add to the list for consideration
+ Point offset = new Point(mView.getLeft(), mView.getTop());
+ Rect rect = new Rect(0, 0, mView.getWidth(), mView.getHeight());
+ results.addTarget(new ScrollCaptureTarget(mView, rect, offset, cb));
+ }
+ }
+
+ /**
+ * Update the timeout for scroll capture requests. Only affects this view root.
+ * The default value is {@link #SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS}.
+ *
+ * @param timeMillis the new timeout in milliseconds
+ */
+ public void setScrollCaptureRequestTimeout(int timeMillis) {
+ mScrollCaptureRequestTimeout = timeMillis;
+ }
+
+ /**
+ * Get the current timeout for scroll capture requests.
+ *
+ * @return the timeout in milliseconds
+ */
+ public long getScrollCaptureRequestTimeout() {
+ return mScrollCaptureRequestTimeout;
+ }
+
+ /**
+ * Handles an inbound request for scroll capture from the system. A search will be
+ * dispatched through the view tree to locate scrolling content.
+ * <p>
+ * A call to
+ * {@link IScrollCaptureResponseListener#onScrollCaptureResponse} will follow.
+ *
+ * @param listener to receive responses
+ * @see ScrollCaptureSearchResults
+ */
+ public void handleScrollCaptureRequest(@NonNull IScrollCaptureResponseListener listener) {
+ ScrollCaptureSearchResults results =
+ new ScrollCaptureSearchResults(mContext.getMainExecutor());
+
+ // Window (root) level callbacks
+ collectRootScrollCaptureTargets(results);
+
+ // Search through View-tree
+ View rootView = getView();
+ if (rootView != null) {
+ Point point = new Point();
+ Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight());
+ getChildVisibleRect(rootView, rect, point);
+ rootView.dispatchScrollCaptureSearch(rect, point, results::addTarget);
+ }
+ Runnable onComplete = () -> dispatchScrollCaptureSearchResponse(listener, results);
+ results.setOnCompleteListener(onComplete);
+ if (!results.isComplete()) {
+ mHandler.postDelayed(results::finish, getScrollCaptureRequestTimeout());
+ }
+ }
+
+ /** Called by {@link #handleScrollCaptureRequest} when a result is returned */
+ private void dispatchScrollCaptureSearchResponse(
+ @NonNull IScrollCaptureResponseListener listener,
+ @NonNull ScrollCaptureSearchResults results) {
+
+ ScrollCaptureTarget selectedTarget = results.getTopResult();
+
+ ScrollCaptureResponse.Builder response = new ScrollCaptureResponse.Builder();
+ response.setWindowTitle(getTitle().toString());
+
+ StringWriter writer = new StringWriter();
+ IndentingPrintWriter pw = new IndentingPrintWriter(writer);
+ results.dump(pw);
+ pw.flush();
+ response.addMessage(writer.toString());
+
+ if (selectedTarget == null) {
+ response.setDescription("No scrollable targets found in window");
+ try {
+ listener.onScrollCaptureResponse(response.build());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send scroll capture search result", e);
+ }
+ return;
+ }
+
+ response.setDescription("Connected");
+
+ // Compute area covered by scrolling content within window
+ Rect boundsInWindow = new Rect();
+ View containingView = selectedTarget.getContainingView();
+ containingView.getLocationInWindow(mAttachInfo.mTmpLocation);
+ boundsInWindow.set(selectedTarget.getScrollBounds());
+ boundsInWindow.offset(mAttachInfo.mTmpLocation[0], mAttachInfo.mTmpLocation[1]);
+ response.setBoundsInWindow(boundsInWindow);
+
+ // Compute the area on screen covered by the window
+ Rect boundsOnScreen = new Rect();
+ mView.getLocationOnScreen(mAttachInfo.mTmpLocation);
+ boundsOnScreen.set(0, 0, mView.getWidth(), mView.getHeight());
+ boundsOnScreen.offset(mAttachInfo.mTmpLocation[0], mAttachInfo.mTmpLocation[1]);
+ response.setWindowBounds(boundsOnScreen);
+
+ // Create a connection and return it to the caller
+ ScrollCaptureConnection connection = new ScrollCaptureConnection(
+ mView.getContext().getMainExecutor(), selectedTarget);
+ response.setConnection(connection);
+
+ try {
+ listener.onScrollCaptureResponse(response.build());
+ } catch (RemoteException e) {
+ if (DEBUG_SCROLL_CAPTURE) {
+ Log.w(TAG, "Failed to send scroll capture search response.", e);
+ }
+ connection.close();
+ }
+ }
+
+ private void reportNextDraw() {
+ if (mReportNextDraw == false) {
+ drawPending();
+ }
+ mReportNextDraw = true;
+ }
+
+ /**
+ * Force the window to report its next draw.
+ * <p>
+ * This method is only supposed to be used to speed up the interaction from SystemUI and window
+ * manager when waiting for the first frame to be drawn when turning on the screen. DO NOT USE
+ * unless you fully understand this interaction.
+ * @hide
+ */
+ public void setReportNextDraw() {
+ reportNextDraw();
+ invalidate();
+ }
+
+ void changeCanvasOpacity(boolean opaque) {
+ Log.d(mTag, "changeCanvasOpacity: opaque=" + opaque);
+ opaque = opaque & ((mView.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0);
+ if (mAttachInfo.mThreadedRenderer != null) {
+ mAttachInfo.mThreadedRenderer.setOpaque(opaque);
+ }
+ }
+
+ /**
+ * Dispatches a KeyEvent to all registered key fallback handlers.
+ *
+ * @param event
+ * @return {@code true} if the event was handled, {@code false} otherwise.
+ */
+ public boolean dispatchUnhandledKeyEvent(KeyEvent event) {
+ return mUnhandledKeyManager.dispatch(mView, event);
+ }
+
+ class TakenSurfaceHolder extends BaseSurfaceHolder {
+ @Override
+ public boolean onAllowLockCanvas() {
+ return mDrawingAllowed;
+ }
+
+ @Override
+ public void onRelayoutContainer() {
+ // Not currently interesting -- from changing between fixed and layout size.
+ }
+
+ @Override
+ public void setFormat(int format) {
+ ((RootViewSurfaceTaker)mView).setSurfaceFormat(format);
+ }
+
+ @Override
+ public void setType(int type) {
+ ((RootViewSurfaceTaker)mView).setSurfaceType(type);
+ }
+
+ @Override
+ public void onUpdateSurface() {
+ // We take care of format and type changes on our own.
+ throw new IllegalStateException("Shouldn't be here");
+ }
+
+ @Override
+ public boolean isCreating() {
+ return mIsCreating;
+ }
+
+ @Override
+ public void setFixedSize(int width, int height) {
+ throw new UnsupportedOperationException(
+ "Currently only support sizing from layout");
+ }
+
+ @Override
+ public void setKeepScreenOn(boolean screenOn) {
+ ((RootViewSurfaceTaker)mView).setSurfaceKeepScreenOn(screenOn);
+ }
+ }
+
+ static class W extends IWindow.Stub {
+ private final WeakReference<ViewRootImpl> mViewAncestor;
+ private final IWindowSession mWindowSession;
+
+ W(ViewRootImpl viewAncestor) {
+ mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
+ mWindowSession = viewAncestor.mWindowSession;
+ }
+
+ @Override
+ public void resized(ClientWindowFrames frames, boolean reportDraw,
+ MergedConfiguration mergedConfiguration, boolean forceLayout,
+ boolean alwaysConsumeSystemBars, int displayId) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, forceLayout,
+ alwaysConsumeSystemBars, displayId);
+ }
+ }
+
+ @Override
+ public void locationInParentDisplayChanged(Point offset) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchLocationInParentDisplayChanged(offset);
+ }
+ }
+
+ @Override
+ public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchInsetsChanged(insetsState, willMove, willResize);
+ }
+ }
+
+ @Override
+ public void insetsControlChanged(InsetsState insetsState,
+ InsetsSourceControl[] activeControls, boolean willMove, boolean willResize) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchInsetsControlChanged(
+ insetsState, activeControls, willMove, willResize);
+ }
+ }
+
+ @Override
+ public void showInsets(@InsetsType int types, boolean fromIme) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#showInsets",
+ viewAncestor.getInsetsController().getHost().getInputMethodManager(),
+ null /* icProto */);
+ }
+ if (viewAncestor != null) {
+ viewAncestor.showInsets(types, fromIme);
+ }
+ }
+
+ @Override
+ public void hideInsets(@InsetsType int types, boolean fromIme) {
+
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (fromIme) {
+ ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#hideInsets",
+ viewAncestor.getInsetsController().getHost().getInputMethodManager(),
+ null /* icProto */);
+ }
+ if (viewAncestor != null) {
+ viewAncestor.hideInsets(types, fromIme);
+ }
+ }
+
+ @Override
+ public void moved(int newX, int newY) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchMoved(newX, newY);
+ }
+ }
+
+ @Override
+ public void dispatchAppVisibility(boolean visible) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchAppVisibility(visible);
+ }
+ }
+
+ @Override
+ public void dispatchGetNewSurface() {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchGetNewSurface();
+ }
+ }
+
+ @Override
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.windowFocusChanged(hasFocus, inTouchMode);
+ }
+ }
+
+ private static int checkCallingPermission(String permission) {
+ try {
+ return ActivityManager.getService().checkPermission(
+ permission, Binder.getCallingPid(), Binder.getCallingUid());
+ } catch (RemoteException e) {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ }
+
+ @Override
+ public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ final View view = viewAncestor.mView;
+ if (view != null) {
+ if (checkCallingPermission(Manifest.permission.DUMP) !=
+ PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Insufficient permissions to invoke"
+ + " executeCommand() from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ }
+
+ OutputStream clientStream = null;
+ try {
+ clientStream = new ParcelFileDescriptor.AutoCloseOutputStream(out);
+ ViewDebug.dispatchCommand(view, command, parameters, clientStream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (clientStream != null) {
+ try {
+ clientStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void closeSystemDialogs(String reason) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchCloseSystemDialogs(reason);
+ }
+ }
+
+ @Override
+ public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep,
+ float zoom, boolean sync) {
+ if (sync) {
+ try {
+ mWindowSession.wallpaperOffsetsComplete(asBinder());
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ @Override
+ public void dispatchWallpaperCommand(String action, int x, int y,
+ int z, Bundle extras, boolean sync) {
+ if (sync) {
+ try {
+ mWindowSession.wallpaperCommandComplete(asBinder(), null);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /* Drag/drop */
+ @Override
+ public void dispatchDragEvent(DragEvent event) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchDragEvent(event);
+ }
+ }
+
+ @Override
+ public void updatePointerIcon(float x, float y) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.updatePointerIcon(x, y);
+ }
+ }
+
+ @Override
+ public void dispatchWindowShown() {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchWindowShown();
+ }
+ }
+
+ @Override
+ public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
+ ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchRequestKeyboardShortcuts(receiver, deviceId);
+ }
+ }
+
+ @Override
+ public void requestScrollCapture(IScrollCaptureResponseListener listener) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.dispatchScrollCaptureRequest(listener);
+ }
+ }
+ }
+
+ public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
+ @UnsupportedAppUsage
+ public CalledFromWrongThreadException(String msg) {
+ super(msg);
+ }
+ }
+
+ static HandlerActionQueue getRunQueue() {
+ HandlerActionQueue rq = sRunQueues.get();
+ if (rq != null) {
+ return rq;
+ }
+ rq = new HandlerActionQueue();
+ sRunQueues.set(rq);
+ return rq;
+ }
+
+ /**
+ * Start a drag resizing which will inform all listeners that a window resize is taking place.
+ */
+ private void startDragResizing(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+ Rect stableInsets, int resizeMode) {
+ if (!mDragResizing) {
+ mDragResizing = true;
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onWindowDragResizeStart(
+ initialBounds, fullscreen, systemInsets, stableInsets, resizeMode);
+ }
+ }
+ mFullRedrawNeeded = true;
+ }
+ }
+
+ /**
+ * End a drag resize which will inform all listeners that a window resize has ended.
+ */
+ private void endDragResizing() {
+ if (mDragResizing) {
+ mDragResizing = false;
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onWindowDragResizeEnd();
+ }
+ }
+ mFullRedrawNeeded = true;
+ }
+ }
+
+ private boolean updateContentDrawBounds() {
+ boolean updated = false;
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ updated |=
+ mWindowCallbacks.get(i).onContentDrawn(mWindowAttributes.surfaceInsets.left,
+ mWindowAttributes.surfaceInsets.top, mWidth, mHeight);
+ }
+ }
+ return updated | (mDragResizing && mReportNextDraw);
+ }
+
+ private void requestDrawWindow() {
+ if (!mUseMTRenderer) {
+ return;
+ }
+ mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size());
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onRequestDraw(mReportNextDraw);
+ }
+ }
+
+ /**
+ * Tells this instance that its corresponding activity has just relaunched. In this case, we
+ * need to force a relayout of the window to make sure we get the correct bounds from window
+ * manager.
+ */
+ public void reportActivityRelaunched() {
+ mActivityRelaunched = true;
+ }
+
+ public SurfaceControl getSurfaceControl() {
+ return mSurfaceControl;
+ }
+
+ /**
+ * @return Returns a token used to identify the windows input channel.
+ */
+ public IBinder getInputToken() {
+ if (mInputEventReceiver == null) {
+ return null;
+ }
+ return mInputEventReceiver.getToken();
+ }
+
+ @NonNull
+ public IBinder getWindowToken() {
+ return mAttachInfo.mWindowToken;
+ }
+
+ /**
+ * Class for managing the accessibility interaction connection
+ * based on the global accessibility state.
+ */
+ final class AccessibilityInteractionConnectionManager
+ implements AccessibilityStateChangeListener {
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ if (enabled) {
+ ensureConnection();
+ if (mAttachInfo.mHasWindowFocus && (mView != null)) {
+ mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ View focusedView = mView.findFocus();
+ if (focusedView != null && focusedView != mView) {
+ focusedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
+ }
+ if (mAttachInfo.mLeashedParentToken != null) {
+ mAccessibilityManager.associateEmbeddedHierarchy(
+ mAttachInfo.mLeashedParentToken, mLeashToken);
+ }
+ } else {
+ ensureNoConnection();
+ mHandler.obtainMessage(MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST).sendToTarget();
+ }
+ }
+
+ public void ensureConnection() {
+ final boolean registered = mAttachInfo.mAccessibilityWindowId
+ != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ if (!registered) {
+ mAttachInfo.mAccessibilityWindowId =
+ mAccessibilityManager.addAccessibilityInteractionConnection(mWindow,
+ mLeashToken,
+ mContext.getPackageName(),
+ new AccessibilityInteractionConnection(ViewRootImpl.this));
+ }
+ }
+
+ public void ensureNoConnection() {
+ final boolean registered = mAttachInfo.mAccessibilityWindowId
+ != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ if (registered) {
+ mAttachInfo.mAccessibilityWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ mAccessibilityManager.removeAccessibilityInteractionConnection(mWindow);
+ }
+ }
+ }
+
+ final class HighContrastTextManager implements HighTextContrastChangeListener {
+ HighContrastTextManager() {
+ ThreadedRenderer.setHighContrastText(mAccessibilityManager.isHighTextContrastEnabled());
+ }
+ @Override
+ public void onHighTextContrastStateChanged(boolean enabled) {
+ ThreadedRenderer.setHighContrastText(enabled);
+
+ // Destroy Displaylists so they can be recreated with high contrast recordings
+ destroyHardwareResources();
+
+ // Schedule redraw, which will rerecord + redraw all text
+ invalidate();
+ }
+ }
+
+ /**
+ * This class is an interface this ViewAncestor provides to the
+ * AccessibilityManagerService to the latter can interact with
+ * the view hierarchy in this ViewAncestor.
+ */
+ static final class AccessibilityInteractionConnection
+ extends IAccessibilityInteractionConnection.Stub {
+ private final WeakReference<ViewRootImpl> mViewRootImpl;
+
+ AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {
+ mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);
+ }
+
+ @Override
+ public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
+ interactiveRegion, interactionId, callback, flags, interrogatingPid,
+ interrogatingTid, spec, args);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setFindAccessibilityNodeInfosResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
+ public void performAccessibilityAction(long accessibilityNodeId, int action,
+ Bundle arguments, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .performAccessibilityActionClientThread(accessibilityNodeId, action, arguments,
+ interactionId, callback, flags, interrogatingPid, interrogatingTid);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setPerformAccessibilityActionResult(false, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
+ public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId,
+ String viewId, Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .findAccessibilityNodeInfosByViewIdClientThread(accessibilityNodeId,
+ viewId, interactiveRegion, interactionId, callback, flags,
+ interrogatingPid, interrogatingTid, spec);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
+ public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text,
+ Region interactiveRegion, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text,
+ interactiveRegion, interactionId, callback, flags, interrogatingPid,
+ interrogatingTid, spec);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setFindAccessibilityNodeInfosResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
+ public void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion,
+ int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .findFocusClientThread(accessibilityNodeId, focusType, interactiveRegion,
+ interactionId, callback, flags, interrogatingPid, interrogatingTid,
+ spec);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
+ public void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion,
+ int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
+ int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .focusSearchClientThread(accessibilityNodeId, direction, interactiveRegion,
+ interactionId, callback, flags, interrogatingPid, interrogatingTid,
+ spec);
+ } else {
+ // We cannot make the call and notify the caller so it does not wait.
+ try {
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
+ } catch (RemoteException re) {
+ /* best effort - ignore */
+ }
+ }
+ }
+
+ @Override
+ public void clearAccessibilityFocus() {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .clearAccessibilityFocusClientThread();
+ }
+ }
+
+ @Override
+ public void notifyOutsideTouch() {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
+ viewRootImpl.getAccessibilityInteractionController()
+ .notifyOutsideTouchClientThread();
+ }
+ }
+ }
+
+ /**
+ * Gets an accessibility embedded connection interface for this ViewRootImpl.
+ * @hide
+ */
+ public IAccessibilityEmbeddedConnection getAccessibilityEmbeddedConnection() {
+ if (mAccessibilityEmbeddedConnection == null) {
+ mAccessibilityEmbeddedConnection = new AccessibilityEmbeddedConnection(
+ ViewRootImpl.this);
+ }
+ return mAccessibilityEmbeddedConnection;
+ }
+
+ private class SendWindowContentChangedAccessibilityEvent implements Runnable {
+ private int mChangeTypes = 0;
+
+ public View mSource;
+ public long mLastEventTimeMillis;
+ /**
+ * Override for {@link AccessibilityEvent#originStackTrace} to provide the stack trace
+ * of the original {@link #runOrPost} call instead of one for sending the delayed event
+ * from a looper.
+ */
+ public StackTraceElement[] mOrigin;
+
+ @Override
+ public void run() {
+ // Protect against re-entrant code and attempt to do the right thing in the case that
+ // we're multithreaded.
+ View source = mSource;
+ mSource = null;
+ if (source == null) {
+ Log.e(TAG, "Accessibility content change has no source");
+ return;
+ }
+ // The accessibility may be turned off while we were waiting so check again.
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ mLastEventTimeMillis = SystemClock.uptimeMillis();
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(mChangeTypes);
+ if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin;
+ source.sendAccessibilityEventUnchecked(event);
+ } else {
+ mLastEventTimeMillis = 0;
+ }
+ // In any case reset to initial state.
+ source.resetSubtreeAccessibilityStateChanged();
+ mChangeTypes = 0;
+ if (AccessibilityEvent.DEBUG_ORIGIN) mOrigin = null;
+ }
+
+ public void runOrPost(View source, int changeType) {
+ if (mHandler.getLooper() != Looper.myLooper()) {
+ CalledFromWrongThreadException e = new CalledFromWrongThreadException("Only the "
+ + "original thread that created a view hierarchy can touch its views.");
+ // TODO: Throw the exception
+ Log.e(TAG, "Accessibility content change on non-UI thread. Future Android "
+ + "versions will throw an exception.", e);
+ // Attempt to recover. This code does not eliminate the thread safety issue, but
+ // it should force any issues to happen near the above log.
+ mHandler.removeCallbacks(this);
+ if (mSource != null) {
+ // Dispatch whatever was pending. It's still possible that the runnable started
+ // just before we removed the callbacks, and bad things will happen, but at
+ // least they should happen very close to the logged error.
+ run();
+ }
+ }
+ if (mSource != null) {
+ // If there is no common predecessor, then mSource points to
+ // a removed view, hence in this case always prefer the source.
+ View predecessor = getCommonPredecessor(mSource, source);
+ if (predecessor != null) {
+ predecessor = predecessor.getSelfOrParentImportantForA11y();
+ }
+ mSource = (predecessor != null) ? predecessor : source;
+ mChangeTypes |= changeType;
+ return;
+ }
+ mSource = source;
+ mChangeTypes = changeType;
+ if (AccessibilityEvent.DEBUG_ORIGIN) {
+ mOrigin = Thread.currentThread().getStackTrace();
+ }
+ final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis;
+ final long minEventIntevalMillis =
+ ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
+ if (timeSinceLastMillis >= minEventIntevalMillis) {
+ removeCallbacksAndRun();
+ } else {
+ mHandler.postDelayed(this, minEventIntevalMillis - timeSinceLastMillis);
+ }
+ }
+
+ public void removeCallbacksAndRun() {
+ mHandler.removeCallbacks(this);
+ run();
+ }
+ }
+
+ private static class UnhandledKeyManager {
+ // This is used to ensure that unhandled events are only dispatched once. We attempt
+ // to dispatch more than once in order to achieve a certain order. Specifically, if we
+ // are in an Activity or Dialog (and have a Window.Callback), the unhandled events should
+ // be dispatched after the view hierarchy, but before the Callback. However, if we aren't
+ // in an activity, we still want unhandled keys to be dispatched.
+ private boolean mDispatched = true;
+
+ // Keeps track of which Views have unhandled key focus for which keys. This doesn't
+ // include modifiers.
+ private final SparseArray<WeakReference<View>> mCapturedKeys = new SparseArray<>();
+
+ // The current receiver. This value is transient and used between the pre-dispatch and
+ // pre-view phase to ensure that other input-stages don't interfere with tracking.
+ private WeakReference<View> mCurrentReceiver = null;
+
+ boolean dispatch(View root, KeyEvent event) {
+ if (mDispatched) {
+ return false;
+ }
+ View consumer;
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "UnhandledKeyEvent dispatch");
+ mDispatched = true;
+
+ consumer = root.dispatchUnhandledKeyEvent(event);
+
+ // If an unhandled listener handles one, then keep track of it so that the
+ // consuming view is first to receive its repeats and release as well.
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ int keycode = event.getKeyCode();
+ if (consumer != null && !KeyEvent.isModifierKey(keycode)) {
+ mCapturedKeys.put(keycode, new WeakReference<>(consumer));
+ }
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ return consumer != null;
+ }
+
+ /**
+ * Called before the event gets dispatched to anything
+ */
+ void preDispatch(KeyEvent event) {
+ // Always clean-up 'up' events since it's possible for earlier dispatch stages to
+ // consume them without consuming the corresponding 'down' event.
+ mCurrentReceiver = null;
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ int idx = mCapturedKeys.indexOfKey(event.getKeyCode());
+ if (idx >= 0) {
+ mCurrentReceiver = mCapturedKeys.valueAt(idx);
+ mCapturedKeys.removeAt(idx);
+ }
+ }
+ }
+
+ /**
+ * Called before the event gets dispatched to the view hierarchy
+ * @return {@code true} if an unhandled handler has focus and consumed the event
+ */
+ boolean preViewDispatch(KeyEvent event) {
+ mDispatched = false;
+ if (mCurrentReceiver == null) {
+ mCurrentReceiver = mCapturedKeys.get(event.getKeyCode());
+ }
+ if (mCurrentReceiver != null) {
+ View target = mCurrentReceiver.get();
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ mCurrentReceiver = null;
+ }
+ if (target != null && target.isAttachedToWindow()) {
+ target.onUnhandledKeyEvent(event);
+ }
+ // consume anyways so that we don't feed uncaptured key events to other views
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Sends a list of blur regions to SurfaceFlinger, tagged with a frame.
+ *
+ * @param regionCopy List of regions
+ * @param frameNumber Frame where it should be applied (or current when using BLAST)
+ */
+ public void dispatchBlurRegions(float[][] regionCopy, long frameNumber) {
+ final SurfaceControl surfaceControl = getSurfaceControl();
+ if (!surfaceControl.isValid()) {
+ return;
+ }
+
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ transaction.setBlurRegions(surfaceControl, regionCopy);
+
+ if (useBLAST() && mBlastBufferQueue != null) {
+ mBlastBufferQueue.mergeWithNextTransaction(transaction, frameNumber);
+ }
+ }
+
+ /**
+ * Creates a background blur drawable for the backing {@link Surface}.
+ */
+ public BackgroundBlurDrawable createBackgroundBlurDrawable() {
+ return mBlurRegionAggregator.createBackgroundBlurDrawable(mContext);
+ }
+
+ @Override
+ public void onDescendantUnbufferedRequested() {
+ mUnbufferedInputSource = mView.mUnbufferedInputSource;
+ }
+
+ /**
+ * Force disabling use of the BLAST adapter regardless of the system
+ * flag. Needs to be called before addView.
+ */
+ void forceDisableBLAST() {
+ mForceDisableBLAST = true;
+ }
+
+ boolean useBLAST() {
+ return mUseBLASTAdapter && !mForceDisableBLAST;
+ }
+
+ int getSurfaceSequenceId() {
+ return mSurfaceSequenceId;
+ }
+
+ /**
+ * Merges the transaction passed in with the next transaction in BLASTBufferQueue. This ensures
+ * you can add transactions to the upcoming frame.
+ */
+ public void mergeWithNextTransaction(Transaction t, long frameNumber) {
+ if (mBlastBufferQueue != null) {
+ mBlastBufferQueue.mergeWithNextTransaction(t, frameNumber);
+ } else {
+ t.apply();
+ }
+ }
+
+ @Override
+ @Nullable public SurfaceControl.Transaction buildReparentTransaction(
+ @NonNull SurfaceControl child) {
+ if (mSurfaceControl.isValid()) {
+ return new SurfaceControl.Transaction().reparent(child, mSurfaceControl);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t) {
+ registerRtFrameCallback(frame -> {
+ mergeWithNextTransaction(t, frame);
+ });
+ return true;
+ }
+
+ int getSurfaceTransformHint() {
+ return mSurfaceControl.getTransformHint();
+ }
+}
diff --git a/android/view/ViewRootImpl_Accessor.java b/android/view/ViewRootImpl_Accessor.java
new file mode 100644
index 0000000..0e15b97
--- /dev/null
+++ b/android/view/ViewRootImpl_Accessor.java
@@ -0,0 +1,26 @@
+/*
+ * 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.view;
+
+/**
+ * Accessor to allow layoutlib to call {@link ViewRootImpl#dispatchApplyInsets} directly.
+ */
+public class ViewRootImpl_Accessor {
+ public static void dispatchApplyInsets(ViewRootImpl viewRoot, View host) {
+ viewRoot.dispatchApplyInsets(host);
+ }
+}
diff --git a/android/view/ViewRootImpl_Delegate.java b/android/view/ViewRootImpl_Delegate.java
new file mode 100644
index 0000000..14b84ef
--- /dev/null
+++ b/android/view/ViewRootImpl_Delegate.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link ViewRootImpl}
+ *
+ * Through the layoutlib_create tool, the original methods of ViewRootImpl have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class ViewRootImpl_Delegate {
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isInTouchMode() {
+ return false; // this allows displaying selection.
+ }
+}
diff --git a/android/view/ViewRootInsetsControllerHost.java b/android/view/ViewRootInsetsControllerHost.java
new file mode 100644
index 0000000..ce882da
--- /dev/null
+++ b/android/view/ViewRootInsetsControllerHost.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsController.DEBUG;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_APPEARANCE_CONTROLLED;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
+
+import android.annotation.NonNull;
+import android.content.res.CompatibilityInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+
+import java.util.List;
+
+/**
+ * Implements {@link InsetsController.Host} for {@link ViewRootImpl}s.
+ * @hide
+ */
+public class ViewRootInsetsControllerHost implements InsetsController.Host {
+
+ private final String TAG = "VRInsetsControllerHost";
+
+ private final ViewRootImpl mViewRoot;
+ private SyncRtSurfaceTransactionApplier mApplier;
+
+ public ViewRootInsetsControllerHost(ViewRootImpl viewRoot) {
+ mViewRoot = viewRoot;
+ }
+
+ @Override
+ public Handler getHandler() {
+ return mViewRoot.mHandler;
+ }
+
+ @Override
+ public void notifyInsetsChanged() {
+ mViewRoot.notifyInsetsChanged();
+ }
+
+ @Override
+ public void addOnPreDrawRunnable(Runnable r) {
+ if (mViewRoot.mView == null) {
+ return;
+ }
+ mViewRoot.mView.getViewTreeObserver().addOnPreDrawListener(
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ mViewRoot.mView.getViewTreeObserver().removeOnPreDrawListener(this);
+ r.run();
+ return true;
+ }
+ });
+ mViewRoot.mView.invalidate();
+ }
+
+ @Override
+ public void dispatchWindowInsetsAnimationPrepare(@NonNull WindowInsetsAnimation animation) {
+ if (mViewRoot.mView == null) {
+ return;
+ }
+ mViewRoot.mView.dispatchWindowInsetsAnimationPrepare(animation);
+ }
+
+ @Override
+ public WindowInsetsAnimation.Bounds dispatchWindowInsetsAnimationStart(
+ @NonNull WindowInsetsAnimation animation,
+ @NonNull WindowInsetsAnimation.Bounds bounds) {
+ if (mViewRoot.mView == null) {
+ return null;
+ }
+ if (DEBUG) Log.d(TAG, "windowInsetsAnimation started");
+ return mViewRoot.mView.dispatchWindowInsetsAnimationStart(animation, bounds);
+ }
+
+ @Override
+ public WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets,
+ @NonNull List<WindowInsetsAnimation> runningAnimations) {
+ if (mViewRoot.mView == null) {
+ // The view has already detached from window.
+ return null;
+ }
+ if (DEBUG) {
+ for (WindowInsetsAnimation anim : runningAnimations) {
+ Log.d(TAG, "windowInsetsAnimation progress: "
+ + anim.getInterpolatedFraction());
+ }
+ }
+ return mViewRoot.mView.dispatchWindowInsetsAnimationProgress(insets, runningAnimations);
+ }
+
+ @Override
+ public void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation) {
+ if (DEBUG) Log.d(TAG, "windowInsetsAnimation ended");
+ if (mViewRoot.mView == null) {
+ // The view has already detached from window.
+ return;
+ }
+ mViewRoot.mView.dispatchWindowInsetsAnimationEnd(animation);
+ }
+
+ @Override
+ public void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... params) {
+ if (mViewRoot.mView == null) {
+ throw new IllegalStateException("View of the ViewRootImpl is not initiated.");
+ }
+ if (mApplier == null) {
+ mApplier = new SyncRtSurfaceTransactionApplier(mViewRoot.mView);
+ }
+ if (mViewRoot.mView.isHardwareAccelerated()) {
+ mApplier.scheduleApply(params);
+ } else {
+ // Window doesn't support hardware acceleration, no synchronization for now.
+ // TODO(b/149342281): use mViewRoot.mSurface.getNextFrameNumber() to sync on every
+ // frame instead.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ mApplier.applyParams(t, params);
+ mApplier.applyTransaction(t, -1);
+ }
+ }
+
+ @Override
+ public void postInsetsAnimationCallback(Runnable r) {
+ mViewRoot.mChoreographer.postCallback(Choreographer.CALLBACK_INSETS_ANIMATION, r,
+ null /* token */);
+ }
+
+ @Override
+ public void updateCompatSysUiVisibility(int type, boolean visible, boolean hasControl) {
+ mViewRoot.updateCompatSysUiVisibility(type, visible, hasControl);
+ }
+
+ @Override
+ public void onInsetsModified(InsetsState insetsState) {
+ try {
+ if (mViewRoot.mAdded) {
+ mViewRoot.mWindowSession.insetsModified(mViewRoot.mWindow, insetsState);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call insetsModified", e);
+ }
+ }
+
+ @Override
+ public boolean hasAnimationCallbacks() {
+ if (mViewRoot.mView == null) {
+ return false;
+ }
+ return mViewRoot.mView.hasWindowInsetsAnimationCallback();
+ }
+
+ @Override
+ public void setSystemBarsAppearance(int appearance, int mask) {
+ mViewRoot.mWindowAttributes.privateFlags |= PRIVATE_FLAG_APPEARANCE_CONTROLLED;
+ final InsetsFlags insetsFlags = mViewRoot.mWindowAttributes.insetsFlags;
+ if (insetsFlags.appearance != appearance) {
+ insetsFlags.appearance = (insetsFlags.appearance & ~mask) | (appearance & mask);
+ mViewRoot.mWindowAttributesChanged = true;
+ mViewRoot.scheduleTraversals();
+ }
+ }
+
+ @Override
+ public int getSystemBarsAppearance() {
+ return mViewRoot.mWindowAttributes.insetsFlags.appearance;
+ }
+
+ @Override
+ public boolean isSystemBarsAppearanceControlled() {
+ return (mViewRoot.mWindowAttributes.privateFlags & PRIVATE_FLAG_APPEARANCE_CONTROLLED) != 0;
+ }
+
+ @Override
+ public void setSystemBarsBehavior(int behavior) {
+ mViewRoot.mWindowAttributes.privateFlags |= PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
+ if (mViewRoot.mWindowAttributes.insetsFlags.behavior != behavior) {
+ mViewRoot.mWindowAttributes.insetsFlags.behavior = behavior;
+ mViewRoot.mWindowAttributesChanged = true;
+ mViewRoot.scheduleTraversals();
+ }
+ }
+
+ @Override
+ public int getSystemBarsBehavior() {
+ return mViewRoot.mWindowAttributes.insetsFlags.behavior;
+ }
+
+ @Override
+ public boolean isSystemBarsBehaviorControlled() {
+ return (mViewRoot.mWindowAttributes.privateFlags & PRIVATE_FLAG_BEHAVIOR_CONTROLLED) != 0;
+ }
+
+ @Override
+ public void releaseSurfaceControlFromRt(SurfaceControl surfaceControl) {
+
+ // At the time we receive new leashes (e.g. InsetsSourceConsumer is processing
+ // setControl) we need to release the old leash. But we may have already scheduled
+ // a SyncRtSurfaceTransaction applier to use it from the RenderThread. To avoid
+ // synchronization issues we also release from the RenderThread so this release
+ // happens after any existing items on the work queue.
+
+ if (mViewRoot.mView != null && mViewRoot.mView.isHardwareAccelerated()) {
+ mViewRoot.registerRtFrameCallback(frame -> {
+ surfaceControl.release();
+ });
+ // Make sure a frame gets scheduled.
+ mViewRoot.mView.invalidate();
+ } else {
+ surfaceControl.release();
+ }
+ }
+
+ @Override
+ public InputMethodManager getInputMethodManager() {
+ return mViewRoot.mContext.getSystemService(InputMethodManager.class);
+ }
+
+ @Override
+ public String getRootViewTitle() {
+ if (mViewRoot == null) {
+ return null;
+ }
+ return mViewRoot.getTitle().toString();
+ }
+
+ @Override
+ public int dipToPx(int dips) {
+ if (mViewRoot != null) {
+ return mViewRoot.dipToPx(dips);
+ }
+ return 0;
+ }
+
+ @Override
+ public IBinder getWindowToken() {
+ if (mViewRoot == null) {
+ return null;
+ }
+ final View view = mViewRoot.getView();
+ if (view == null) {
+ return null;
+ }
+ return view.getWindowToken();
+ }
+
+ @Override
+ public CompatibilityInfo.Translator getTranslator() {
+ if (mViewRoot != null) {
+ return mViewRoot.mTranslator;
+ }
+ return null;
+ }
+}
diff --git a/android/view/ViewShowHidePerfTest.java b/android/view/ViewShowHidePerfTest.java
new file mode 100644
index 0000000..a69d3ff
--- /dev/null
+++ b/android/view/ViewShowHidePerfTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.view;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.perftests.utils.PerfTestActivity;
+import android.view.View.MeasureSpec;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class ViewShowHidePerfTest {
+
+ @Rule
+ public ActivityTestRule<PerfTestActivity> mActivityRule =
+ new ActivityTestRule<>(PerfTestActivity.class);
+
+ @Rule
+ public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+
+ public Context getContext() {
+ return InstrumentationRegistry.getInstrumentation().getTargetContext();
+ }
+
+ static abstract class SubTreeFactory {
+ String mName;
+ SubTreeFactory(String name) { mName = name; }
+
+ abstract View create(Context context, int depth);
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+ }
+
+ private static SubTreeFactory[] sSubTreeFactories = new SubTreeFactory[] {
+ new SubTreeFactory("NestedLinearLayoutTree") {
+ private int mColorToggle = 0;
+
+ private void createNestedLinearLayoutTree(Context context, LinearLayout parent,
+ int remainingDepth) {
+ if (remainingDepth <= 0) {
+ mColorToggle = (mColorToggle + 1) % 4;
+ parent.setBackgroundColor((mColorToggle < 2) ? Color.RED : Color.BLUE);
+ return;
+ }
+
+ boolean vertical = remainingDepth % 2 == 0;
+ parent.setOrientation(vertical ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
+
+ for (int i = 0; i < 2; i++) {
+ LinearLayout child = new LinearLayout(context);
+ // vertical: match parent in x axis, horizontal: y axis.
+ parent.addView(child, new LinearLayout.LayoutParams(
+ (vertical ? ViewGroup.LayoutParams.MATCH_PARENT : 0),
+ (vertical ? 0 : ViewGroup.LayoutParams.MATCH_PARENT),
+ 1.0f));
+
+ createNestedLinearLayoutTree(context, child, remainingDepth - 1);
+ }
+ }
+
+ @Override
+ public View create(Context context, int depth) {
+ LinearLayout root = new LinearLayout(context);
+ createNestedLinearLayoutTree(context, root, depth - 1);
+ return root;
+ }
+ },
+ new SubTreeFactory("ImageViewList") {
+ @Override
+ public View create(Context context, int depth) {
+ LinearLayout root = new LinearLayout(context);
+ root.setOrientation(LinearLayout.HORIZONTAL);
+ int childCount = (int) Math.pow(2, depth);
+ for (int i = 0; i < childCount; i++) {
+ ImageView imageView = new ImageView(context);
+ root.addView(imageView, new LinearLayout.LayoutParams(
+ 0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f));
+ imageView.setImageDrawable(new ColorDrawable(Color.RED));
+ }
+ return root;
+ }
+ },
+ };
+
+
+ @Parameterized.Parameters(name = "Factory:{0},depth:{1}")
+ public static Iterable<Object[]> params() {
+ List<Object[]> params = new ArrayList<>();
+ for (int depth : new int[] { 6 }) {
+ for (SubTreeFactory subTreeFactory : sSubTreeFactories) {
+ params.add(new Object[]{ subTreeFactory, depth });
+ }
+ }
+ return params;
+ }
+
+ private final View mChild;
+
+ public ViewShowHidePerfTest(SubTreeFactory subTreeFactory, int depth) {
+ mChild = subTreeFactory.create(getContext(), depth);
+ }
+
+ interface TestCallback {
+ void run(BenchmarkState state, int width, int height, ViewGroup parent, View child);
+ }
+
+ private void testParentWithChild(TestCallback callback) throws Throwable {
+ mActivityRule.runOnUiThread(() -> {
+ final BenchmarkState state = mBenchmarkRule.getState();
+
+ FrameLayout parent = new FrameLayout(getContext());
+ mActivityRule.getActivity().setContentView(parent);
+
+ final int width = 1000;
+ final int height = 1000;
+ layout(width, height, parent);
+
+ callback.run(state, width, height, parent, mChild);
+ });
+ }
+
+ private void updateAndValidateDisplayList(View view) {
+ boolean hasDisplayList = view.updateDisplayListIfDirty().hasDisplayList();
+ assertTrue(hasDisplayList);
+ }
+
+ private void layout(int width, int height, View view) {
+ view.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ view.layout(0, 0, height, width);
+ }
+
+ @Test
+ public void testRemove() throws Throwable {
+ testParentWithChild((state, width, height, parent, child) -> {
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ updateAndValidateDisplayList(parent); // Note, done to be safe, likely not needed
+ parent.addView(child);
+ layout(width, height, child);
+ updateAndValidateDisplayList(parent);
+ state.resumeTiming();
+
+ parent.removeAllViews();
+ }
+ });
+ }
+
+ @Test
+ public void testAdd() throws Throwable {
+ testParentWithChild((state, width, height, parent, child) -> {
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ layout(width, height, child); // Note, done to be safe, likely not needed
+ updateAndValidateDisplayList(parent); // Note, done to be safe, likely not needed
+ parent.removeAllViews();
+ updateAndValidateDisplayList(parent);
+ state.resumeTiming();
+
+ parent.addView(child);
+ }
+ });
+ }
+
+ @Test
+ public void testRecordAfterAdd() throws Throwable {
+ testParentWithChild((state, width, height, parent, child) -> {
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ parent.removeAllViews();
+ updateAndValidateDisplayList(parent); // Note, done to be safe, likely not needed
+ parent.addView(child);
+ layout(width, height, child);
+ state.resumeTiming();
+
+ updateAndValidateDisplayList(parent);
+ }
+ });
+ }
+
+ private void testVisibility(int fromVisibility, int toVisibility) throws Throwable {
+ testParentWithChild((state, width, height, parent, child) -> {
+ parent.addView(child);
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ layout(width, height, parent);
+ updateAndValidateDisplayList(parent);
+ child.setVisibility(fromVisibility);
+ layout(width, height, parent);
+ updateAndValidateDisplayList(parent);
+ state.resumeTiming();
+
+ child.setVisibility(toVisibility);
+ }
+ });
+ }
+
+ @Test
+ public void testInvisibleToVisible() throws Throwable {
+ testVisibility(View.INVISIBLE, View.VISIBLE);
+ }
+
+ @Test
+ public void testVisibleToInvisible() throws Throwable {
+ testVisibility(View.VISIBLE, View.INVISIBLE);
+ }
+ @Test
+ public void testGoneToVisible() throws Throwable {
+ testVisibility(View.GONE, View.VISIBLE);
+ }
+
+ @Test
+ public void testVisibleToGone() throws Throwable {
+ testVisibility(View.VISIBLE, View.GONE);
+ }
+}
diff --git a/android/view/ViewStructure.java b/android/view/ViewStructure.java
new file mode 100644
index 0000000..e246634
--- /dev/null
+++ b/android/view/ViewStructure.java
@@ -0,0 +1,521 @@
+/*
+ * 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.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.util.Pair;
+import android.view.View.AutofillImportance;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * <p><code>ViewStructure</code> is a container for storing additional
+ * per-view data generated by {@link View#onProvideStructure
+ * View.onProvideStructure} and {@link View#onProvideAutofillStructure
+ * View.onProvideAutofillStructure}.
+ *
+ * <p>To learn more about using Autofill in your app, read the
+ * <a href="/guide/topics/text/autofill">Autofill Framework</a> guides.
+ *
+ */
+public abstract class ViewStructure {
+
+ /**
+ * Set the identifier for this view.
+ *
+ * @param id The view's identifier, as per {@link View#getId View.getId()}.
+ * @param packageName The package name of the view's identifier, or null if there is none.
+ * @param typeName The type name of the view's identifier, or null if there is none.
+ * @param entryName The entry name of the view's identifier, or null if there is none.
+ */
+ public abstract void setId(int id, String packageName, String typeName, String entryName);
+
+ /**
+ * Set the basic dimensions of this view.
+ *
+ * @param left The view's left position, in pixels relative to its parent's left edge.
+ * @param top The view's top position, in pixels relative to its parent's top edge.
+ * @param scrollX How much the view's x coordinate space has been scrolled, in pixels.
+ * @param scrollY How much the view's y coordinate space has been scrolled, in pixels.
+ * @param width The view's visible width, in pixels. This is the width visible on screen,
+ * not the total data width of a scrollable view.
+ * @param height The view's visible height, in pixels. This is the height visible on
+ * screen, not the total data height of a scrollable view.
+ */
+ public abstract void setDimens(int left, int top, int scrollX, int scrollY, int width,
+ int height);
+
+ /**
+ * Set the transformation matrix associated with this view, as per
+ * {@link View#getMatrix View.getMatrix()}, or null if there is none.
+ */
+ public abstract void setTransformation(Matrix matrix);
+
+ /**
+ * Set the visual elevation (shadow) of the view, as per
+ * {@link View#getZ View.getZ()}. Note this is <em>not</em> related
+ * to the physical Z-ordering of this view relative to its other siblings (that is how
+ * they overlap when drawing), it is only the visual representation for shadowing.
+ */
+ public abstract void setElevation(float elevation);
+
+ /**
+ * Set an alpha transformation that is applied to this view, as per
+ * {@link View#getAlpha View.getAlpha()}. Value ranges from 0
+ * (completely transparent) to 1 (completely opaque); the default is 1, which means
+ * no transformation.
+ */
+ public abstract void setAlpha(float alpha);
+
+ /**
+ * Set the visibility state of this view, as per
+ * {@link View#getVisibility View.getVisibility()}.
+ */
+ public abstract void setVisibility(int visibility);
+
+ /** @hide */
+ @SuppressWarnings("HiddenAbstractMethod")
+ public abstract void setAssistBlocked(boolean state);
+
+ /**
+ * Set the enabled state of this view, as per {@link View#isEnabled View.isEnabled()}.
+ */
+ public abstract void setEnabled(boolean state);
+
+ /**
+ * Set the clickable state of this view, as per {@link View#isClickable View.isClickable()}.
+ */
+ public abstract void setClickable(boolean state);
+
+ /**
+ * Set the long clickable state of this view, as per
+ * {@link View#isLongClickable View.isLongClickable()}.
+ */
+ public abstract void setLongClickable(boolean state);
+
+ /**
+ * Set the context clickable state of this view, as per
+ * {@link View#isContextClickable View.isContextClickable()}.
+ */
+ public abstract void setContextClickable(boolean state);
+
+ /**
+ * Set the focusable state of this view, as per {@link View#isFocusable View.isFocusable()}.
+ */
+ public abstract void setFocusable(boolean state);
+
+ /**
+ * Set the focused state of this view, as per {@link View#isFocused View.isFocused()}.
+ */
+ public abstract void setFocused(boolean state);
+
+ /**
+ * Set the accessibility focused state of this view, as per
+ * {@link View#isAccessibilityFocused View.isAccessibilityFocused()}.
+ */
+ public abstract void setAccessibilityFocused(boolean state);
+
+ /**
+ * Set the checkable state of this view, such as whether it implements the
+ * {@link android.widget.Checkable} interface.
+ */
+ public abstract void setCheckable(boolean state);
+
+ /**
+ * Set the checked state of this view, such as
+ * {@link android.widget.Checkable#isChecked Checkable.isChecked()}.
+ */
+ public abstract void setChecked(boolean state);
+
+ /**
+ * Set the selected state of this view, as per {@link View#isSelected View.isSelected()}.
+ */
+ public abstract void setSelected(boolean state);
+
+ /**
+ * Set the activated state of this view, as per {@link View#isActivated View.isActivated()}.
+ */
+ public abstract void setActivated(boolean state);
+
+ /**
+ * Set the opaque state of this view, as per {@link View#isOpaque View.isOpaque()}.
+ */
+ public abstract void setOpaque(boolean opaque);
+
+ /**
+ * Set the class name of the view, as per
+ * {@link View#getAccessibilityClassName View.getAccessibilityClassName()}.
+ */
+ public abstract void setClassName(String className);
+
+ /**
+ * Set the content description of the view, as per
+ * {@link View#getContentDescription View.getContentDescription()}.
+ */
+ public abstract void setContentDescription(CharSequence contentDescription);
+
+ /**
+ * Set the text that is associated with this view. There is no selection
+ * associated with the text. The text may have style spans to supply additional
+ * display and semantic information.
+ */
+ public abstract void setText(CharSequence text);
+
+ /**
+ * Like {@link #setText(CharSequence)} but with an active selection
+ * extending from <var>selectionStart</var> through <var>selectionEnd</var>.
+ */
+ public abstract void setText(CharSequence text, int selectionStart, int selectionEnd);
+
+ /**
+ * Explicitly set default global style information for text that was previously set with
+ * {@link #setText}.
+ *
+ * @param size The size, in pixels, of the text.
+ * @param fgColor The foreground color, packed as 0xAARRGGBB.
+ * @param bgColor The background color, packed as 0xAARRGGBB.
+ * @param style Style flags, as defined by {@link android.app.assist.AssistStructure.ViewNode}.
+ */
+ public abstract void setTextStyle(float size, int fgColor, int bgColor, int style);
+
+ /**
+ * Set line information for test that was previously supplied through
+ * {@link #setText(CharSequence)}. This provides the line breaking of the text as it
+ * is shown on screen. This function takes ownership of the provided arrays; you should
+ * not make further modification to them.
+ *
+ * @param charOffsets The offset in to {@link #setText} where a line starts.
+ * @param baselines The baseline where the line is drawn on screen.
+ */
+ public abstract void setTextLines(int[] charOffsets, int[] baselines);
+
+ /**
+ * Sets the identifier used to set the text associated with this view.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setTextIdEntry(@NonNull String entryName) {
+ Preconditions.checkNotNull(entryName);
+ }
+
+ /**
+ * Set optional hint text associated with this view; this is for example the text that is
+ * shown by an EditText when it is empty to indicate to the user the kind of text to input.
+ */
+ public abstract void setHint(CharSequence hint);
+
+ /**
+ * Sets the identifier used to set the hint associated with this view.
+ *
+ * <p>Used as metadata for fingerprinting view nodes/structures.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setHintIdEntry(@NonNull String entryName) {
+ Preconditions.checkNotNull(entryName);
+ }
+
+ /**
+ * Retrieve the last {@link #setText(CharSequence)}.
+ */
+ public abstract CharSequence getText();
+
+ /**
+ * Retrieve the last selection start set by {@link #setText(CharSequence, int, int)}.
+ */
+ public abstract int getTextSelectionStart();
+
+ /**
+ * Retrieve the last selection end set by {@link #setText(CharSequence, int, int)}.
+ */
+ public abstract int getTextSelectionEnd();
+
+ /**
+ * Retrieve the last hint set by {@link #setHint}.
+ */
+ public abstract CharSequence getHint();
+
+ /**
+ * Get extra data associated with this view structure; the returned Bundle is mutable,
+ * allowing you to view and modify its contents. Keys placed in the Bundle should use
+ * an appropriate namespace prefix (such as com.google.MY_KEY) to avoid conflicts.
+ */
+ public abstract Bundle getExtras();
+
+ /**
+ * Returns true if {@link #getExtras} has been used to create extra content.
+ */
+ public abstract boolean hasExtras();
+
+ /**
+ * Set the number of children of this view, which defines the range of indices you can
+ * use with {@link #newChild} and {@link #asyncNewChild}. Calling this method again
+ * resets all of the child state of the view, removing any children that had previously
+ * been added.
+ */
+ public abstract void setChildCount(int num);
+
+ /**
+ * Add to this view's child count. This increases the current child count by
+ * <var>num</var> children beyond what was last set by {@link #setChildCount}
+ * or {@link #addChildCount}. The index at which the new child starts in the child
+ * array is returned.
+ *
+ * @param num The number of new children to add.
+ * @return Returns the index in the child array at which the new children start.
+ */
+ public abstract int addChildCount(int num);
+
+ /**
+ * Return the child count as set by {@link #setChildCount}.
+ */
+ public abstract int getChildCount();
+
+ /**
+ * Create a new child {@link ViewStructure} in this view, putting into the list of
+ * children at <var>index</var>.
+ *
+ * <p><b>NOTE: </b>you must pre-allocate space for the child first, by calling either
+ * {@link #addChildCount(int)} or {@link #setChildCount(int)}.
+ *
+ * @return Returns an fresh {@link ViewStructure} ready to be filled in.
+ */
+ public abstract ViewStructure newChild(int index);
+
+ /**
+ * Like {@link #newChild}, but allows the caller to asynchronously populate the returned
+ * child. It can transfer the returned {@link ViewStructure} to another thread for it
+ * to build its content (and children etc). Once done, some thread must call
+ * {@link #asyncCommit} to tell the containing {@link ViewStructure} that the async
+ * population is done.
+ *
+ * <p><b>NOTE: </b>you must pre-allocate space for the child first, by calling either
+ * {@link #addChildCount(int)} or {@link #setChildCount(int)}.
+ *
+ * @return Returns an fresh {@link ViewStructure} ready to be filled in.
+ */
+ public abstract ViewStructure asyncNewChild(int index);
+
+ /**
+ * Gets the {@link AutofillId} associated with this node.
+ */
+ @Nullable
+ public abstract AutofillId getAutofillId();
+
+ /**
+ * Sets the {@link AutofillId} associated with this node.
+ */
+ public abstract void setAutofillId(@NonNull AutofillId id);
+
+ /**
+ * Sets the {@link AutofillId} for this virtual node.
+ *
+ * @param parentId id of the parent node.
+ * @param virtualId an opaque ID to the Android System; it's the same id used on
+ * {@link View#autofill(android.util.SparseArray)}.
+ */
+ public abstract void setAutofillId(@NonNull AutofillId parentId, int virtualId);
+
+ /**
+ * Sets the {@link View#getAutofillType()} that can be used to autofill this node.
+ */
+ public abstract void setAutofillType(@View.AutofillType int type);
+
+ /**
+ * Sets the a hints that helps the autofill service to select the appropriate data to fill the
+ * view.
+ */
+ public abstract void setAutofillHints(@Nullable String[] hint);
+
+ /**
+ * Sets the {@link AutofillValue} representing the current value of this node.
+ */
+ public abstract void setAutofillValue(AutofillValue value);
+
+ /**
+ * Sets the options that can be used to autofill this node.
+ *
+ * <p>Typically used by nodes whose {@link View#getAutofillType()} is a list to indicate the
+ * meaning of each possible value in the list.
+ */
+ public abstract void setAutofillOptions(CharSequence[] options);
+
+ /**
+ * Sets the {@link View#setImportantForAutofill(int) importantForAutofill mode} of the
+ * view associated with this node.
+ */
+ public void setImportantForAutofill(@AutofillImportance int mode) {}
+
+ /**
+ * Sets the MIME types accepted by this view. See {@link View#getReceiveContentMimeTypes()}.
+ *
+ * <p>Should only be set when the node is used for Autofill or Content Capture purposes - it
+ * will be ignored when used for Assist.
+ */
+ public void setReceiveContentMimeTypes(
+ @SuppressLint("NullableCollection") @Nullable String[] mimeTypes) {}
+
+ /**
+ * Sets the {@link android.text.InputType} bits of this node.
+ *
+ * @param inputType inputType bits as defined by {@link android.text.InputType}.
+ */
+ public abstract void setInputType(int inputType);
+
+ /**
+ * Sets whether the data on this node is sensitive; if it is, then its content (text, autofill
+ * value, etc..) is striped before calls to {@link
+ * android.service.autofill.AutofillService#onFillRequest(android.service.autofill.FillRequest,
+ * android.os.CancellationSignal, android.service.autofill.FillCallback)}.
+ *
+ * <p>By default, all nodes are assumed to be sensitive, and only nodes that does not have PII
+ * (Personally Identifiable Information - sensitive data such as email addresses, credit card
+ * numbers, passwords, etc...) should be marked as non-sensitive; a good rule of thumb is to
+ * mark as non-sensitive nodes whose value were statically set from resources.
+ *
+ * <p>Notice that the content of even sensitive nodes are sent to the service (through the
+ * {@link
+ * android.service.autofill.AutofillService#onSaveRequest(android.service.autofill.SaveRequest,
+ * android.service.autofill.SaveCallback)} call) when the user consented to save
+ * thedata, so it is important to set the content of sensitive nodes as well, but mark them as
+ * sensitive.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public abstract void setDataIsSensitive(boolean sensitive);
+
+ /**
+ * Sets the minimum width in ems of the text associated with this view, when supported.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setMinTextEms(@SuppressWarnings("unused") int minEms) {}
+
+ /**
+ * Sets the maximum width in ems of the text associated with this view, when supported.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setMaxTextEms(@SuppressWarnings("unused") int maxEms) {}
+
+ /**
+ * Sets the maximum length of the text associated with this view, when supported.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setMaxTextLength(@SuppressWarnings("unused") int maxLength) {}
+
+ /**
+ * Call when done populating a {@link ViewStructure} returned by
+ * {@link #asyncNewChild}.
+ */
+ public abstract void asyncCommit();
+
+ /** @hide */
+ @SuppressWarnings("HiddenAbstractMethod")
+ public abstract Rect getTempRect();
+
+ /**
+ * Sets the Web domain represented by this node.
+ *
+ * <p>Typically used when the view is a container for an HTML document.
+ *
+ * @param domain RFC 2396-compliant URI representing the domain.
+ */
+ public abstract void setWebDomain(@Nullable String domain);
+
+ /**
+ * Sets the the list of locales associated with this node.
+ */
+ public abstract void setLocaleList(LocaleList localeList);
+
+ /**
+ * Creates a new {@link HtmlInfo.Builder} for the given HTML tag.
+ *
+ * @param tagName name of the HTML tag.
+ * @return a new builder.
+ */
+ public abstract HtmlInfo.Builder newHtmlInfoBuilder(@NonNull String tagName);
+
+ /**
+ * Sets the HTML properties of this node when it represents an HTML element.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for assist.
+ *
+ * @param htmlInfo HTML properties.
+ */
+ public abstract void setHtmlInfo(@NonNull HtmlInfo htmlInfo);
+
+ /**
+ * Simplified representation of the HTML properties of a node that represents an HTML element.
+ */
+ public abstract static class HtmlInfo {
+
+ /**
+ * Gets the HTML tag.
+ */
+ @NonNull
+ public abstract String getTag();
+
+ /**
+ * Gets the list of HTML attributes.
+ *
+ * @return list of key/value pairs; could contain pairs with the same keys.
+ */
+ @Nullable
+ public abstract List<Pair<String, String>> getAttributes();
+
+ /**
+ * Builder for {@link HtmlInfo} objects.
+ */
+ public abstract static class Builder {
+
+ /**
+ * Adds an HTML attribute.
+ *
+ * @param name name of the attribute.
+ * @param value value of the attribute.
+ * @return same builder, for chaining.
+ */
+ public abstract Builder addAttribute(@NonNull String name, @NonNull String value);
+
+ /**
+ * Builds the {@link HtmlInfo} object.
+ *
+ * @return a new {@link HtmlInfo} instance.
+ */
+ public abstract HtmlInfo build();
+ }
+ }
+}
diff --git a/android/view/ViewStub.java b/android/view/ViewStub.java
new file mode 100644
index 0000000..9ca1632
--- /dev/null
+++ b/android/view/ViewStub.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate
+ * layout resources at runtime.
+ *
+ * When a ViewStub is made visible, or when {@link #inflate()} is invoked, the layout resource
+ * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views.
+ * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or
+ * {@link #inflate()} is invoked.
+ *
+ * The inflated View is added to the ViewStub's parent with the ViewStub's layout
+ * parameters. Similarly, you can define/override the inflate View's id by using the
+ * ViewStub's inflatedId property. For instance:
+ *
+ * <pre>
+ * <ViewStub android:id="@+id/stub"
+ * android:inflatedId="@+id/subTree"
+ * android:layout="@layout/mySubTree"
+ * android:layout_width="120dip"
+ * android:layout_height="40dip" />
+ * </pre>
+ *
+ * The ViewStub thus defined can be found using the id "stub." After inflation of
+ * the layout resource "mySubTree," the ViewStub is removed from its parent. The
+ * View created by inflating the layout resource "mySubTree" can be found using the
+ * id "subTree," specified by the inflatedId property. The inflated View is finally
+ * assigned a width of 120dip and a height of 40dip.
+ *
+ * The preferred way to perform the inflation of the layout resource is the following:
+ *
+ * <pre>
+ * ViewStub stub = findViewById(R.id.stub);
+ * View inflated = stub.inflate();
+ * </pre>
+ *
+ * When {@link #inflate()} is invoked, the ViewStub is replaced by the inflated View
+ * and the inflated View is returned. This lets applications get a reference to the
+ * inflated View without executing an extra findViewById().
+ *
+ * @attr ref android.R.styleable#ViewStub_inflatedId
+ * @attr ref android.R.styleable#ViewStub_layout
+ */
+@RemoteView
+public final class ViewStub extends View {
+ private int mInflatedId;
+ private int mLayoutResource;
+
+ private WeakReference<View> mInflatedViewRef;
+
+ private LayoutInflater mInflater;
+ private OnInflateListener mInflateListener;
+
+ public ViewStub(Context context) {
+ this(context, 0);
+ }
+
+ /**
+ * Creates a new ViewStub with the specified layout resource.
+ *
+ * @param context The application's environment.
+ * @param layoutResource The reference to a layout resource that will be inflated.
+ */
+ public ViewStub(Context context, @LayoutRes int layoutResource) {
+ this(context, null);
+
+ mLayoutResource = layoutResource;
+ }
+
+ public ViewStub(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.ViewStub, defStyleAttr, defStyleRes);
+ saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, defStyleAttr,
+ defStyleRes);
+
+ mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
+ mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
+ mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
+ a.recycle();
+
+ setVisibility(GONE);
+ setWillNotDraw(true);
+ }
+
+ /**
+ * Returns the id taken by the inflated view. If the inflated id is
+ * {@link View#NO_ID}, the inflated view keeps its original id.
+ *
+ * @return A positive integer used to identify the inflated view or
+ * {@link #NO_ID} if the inflated view should keep its id.
+ *
+ * @see #setInflatedId(int)
+ * @attr ref android.R.styleable#ViewStub_inflatedId
+ */
+ @IdRes
+ public int getInflatedId() {
+ return mInflatedId;
+ }
+
+ /**
+ * Defines the id taken by the inflated view. If the inflated id is
+ * {@link View#NO_ID}, the inflated view keeps its original id.
+ *
+ * @param inflatedId A positive integer used to identify the inflated view or
+ * {@link #NO_ID} if the inflated view should keep its id.
+ *
+ * @see #getInflatedId()
+ * @attr ref android.R.styleable#ViewStub_inflatedId
+ */
+ @android.view.RemotableViewMethod(asyncImpl = "setInflatedIdAsync")
+ public void setInflatedId(@IdRes int inflatedId) {
+ mInflatedId = inflatedId;
+ }
+
+ /** @hide **/
+ public Runnable setInflatedIdAsync(@IdRes int inflatedId) {
+ mInflatedId = inflatedId;
+ return null;
+ }
+
+ /**
+ * Returns the layout resource that will be used by {@link #setVisibility(int)} or
+ * {@link #inflate()} to replace this StubbedView
+ * in its parent by another view.
+ *
+ * @return The layout resource identifier used to inflate the new View.
+ *
+ * @see #setLayoutResource(int)
+ * @see #setVisibility(int)
+ * @see #inflate()
+ * @attr ref android.R.styleable#ViewStub_layout
+ */
+ @LayoutRes
+ public int getLayoutResource() {
+ return mLayoutResource;
+ }
+
+ /**
+ * Specifies the layout resource to inflate when this StubbedView becomes visible or invisible
+ * or when {@link #inflate()} is invoked. The View created by inflating the layout resource is
+ * used to replace this StubbedView in its parent.
+ *
+ * @param layoutResource A valid layout resource identifier (different from 0.)
+ *
+ * @see #getLayoutResource()
+ * @see #setVisibility(int)
+ * @see #inflate()
+ * @attr ref android.R.styleable#ViewStub_layout
+ */
+ @android.view.RemotableViewMethod(asyncImpl = "setLayoutResourceAsync")
+ public void setLayoutResource(@LayoutRes int layoutResource) {
+ mLayoutResource = layoutResource;
+ }
+
+ /** @hide **/
+ public Runnable setLayoutResourceAsync(@LayoutRes int layoutResource) {
+ mLayoutResource = layoutResource;
+ return null;
+ }
+
+ /**
+ * Set {@link LayoutInflater} to use in {@link #inflate()}, or {@code null}
+ * to use the default.
+ */
+ public void setLayoutInflater(LayoutInflater inflater) {
+ mInflater = inflater;
+ }
+
+ /**
+ * Get current {@link LayoutInflater} used in {@link #inflate()}.
+ */
+ public LayoutInflater getLayoutInflater() {
+ return mInflater;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(0, 0);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ }
+
+ /**
+ * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
+ * {@link #inflate()} is invoked and this StubbedView is replaced in its parent
+ * by the inflated layout resource. After that calls to this function are passed
+ * through to the inflated view.
+ *
+ * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ *
+ * @see #inflate()
+ */
+ @Override
+ @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
+ public void setVisibility(int visibility) {
+ if (mInflatedViewRef != null) {
+ View view = mInflatedViewRef.get();
+ if (view != null) {
+ view.setVisibility(visibility);
+ } else {
+ throw new IllegalStateException("setVisibility called on un-referenced view");
+ }
+ } else {
+ super.setVisibility(visibility);
+ if (visibility == VISIBLE || visibility == INVISIBLE) {
+ inflate();
+ }
+ }
+ }
+
+ /** @hide **/
+ public Runnable setVisibilityAsync(int visibility) {
+ if (visibility == VISIBLE || visibility == INVISIBLE) {
+ ViewGroup parent = (ViewGroup) getParent();
+ return new ViewReplaceRunnable(inflateViewNoAdd(parent));
+ } else {
+ return null;
+ }
+ }
+
+ private View inflateViewNoAdd(ViewGroup parent) {
+ final LayoutInflater factory;
+ if (mInflater != null) {
+ factory = mInflater;
+ } else {
+ factory = LayoutInflater.from(mContext);
+ }
+ final View view = factory.inflate(mLayoutResource, parent, false);
+
+ if (mInflatedId != NO_ID) {
+ view.setId(mInflatedId);
+ }
+ return view;
+ }
+
+ private void replaceSelfWithView(View view, ViewGroup parent) {
+ final int index = parent.indexOfChild(this);
+ parent.removeViewInLayout(this);
+
+ final ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ if (layoutParams != null) {
+ parent.addView(view, index, layoutParams);
+ } else {
+ parent.addView(view, index);
+ }
+ }
+
+ /**
+ * Inflates the layout resource identified by {@link #getLayoutResource()}
+ * and replaces this StubbedView in its parent by the inflated layout resource.
+ *
+ * @return The inflated layout resource.
+ *
+ */
+ public View inflate() {
+ final ViewParent viewParent = getParent();
+
+ if (viewParent != null && viewParent instanceof ViewGroup) {
+ if (mLayoutResource != 0) {
+ final ViewGroup parent = (ViewGroup) viewParent;
+ final View view = inflateViewNoAdd(parent);
+ replaceSelfWithView(view, parent);
+
+ mInflatedViewRef = new WeakReference<>(view);
+ if (mInflateListener != null) {
+ mInflateListener.onInflate(this, view);
+ }
+
+ return view;
+ } else {
+ throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
+ }
+ } else {
+ throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
+ }
+ }
+
+ /**
+ * Specifies the inflate listener to be notified after this ViewStub successfully
+ * inflated its layout resource.
+ *
+ * @param inflateListener The OnInflateListener to notify of successful inflation.
+ *
+ * @see android.view.ViewStub.OnInflateListener
+ */
+ public void setOnInflateListener(OnInflateListener inflateListener) {
+ mInflateListener = inflateListener;
+ }
+
+ /**
+ * Listener used to receive a notification after a ViewStub has successfully
+ * inflated its layout resource.
+ *
+ * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener)
+ */
+ public static interface OnInflateListener {
+ /**
+ * Invoked after a ViewStub successfully inflated its layout resource.
+ * This method is invoked after the inflated view was added to the
+ * hierarchy but before the layout pass.
+ *
+ * @param stub The ViewStub that initiated the inflation.
+ * @param inflated The inflated View.
+ */
+ void onInflate(ViewStub stub, View inflated);
+ }
+
+ /** @hide **/
+ public class ViewReplaceRunnable implements Runnable {
+ public final View view;
+
+ ViewReplaceRunnable(View view) {
+ this.view = view;
+ }
+
+ @Override
+ public void run() {
+ replaceSelfWithView(view, (ViewGroup) getParent());
+ }
+ }
+}
diff --git a/android/view/ViewTreeObserver.java b/android/view/ViewTreeObserver.java
new file mode 100644
index 0000000..5a99ab2
--- /dev/null
+++ b/android/view/ViewTreeObserver.java
@@ -0,0 +1,1331 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+
+/**
+ * A view tree observer is used to register listeners that can be notified of global
+ * changes in the view tree. Such global events include, but are not limited to,
+ * layout of the whole tree, beginning of the drawing pass, touch mode change....
+ *
+ * A ViewTreeObserver should never be instantiated by applications as it is provided
+ * by the views hierarchy. Refer to {@link android.view.View#getViewTreeObserver()}
+ * for more information.
+ */
+public final class ViewTreeObserver {
+ // Recursive listeners use CopyOnWriteArrayList
+ private CopyOnWriteArrayList<OnWindowFocusChangeListener> mOnWindowFocusListeners;
+ private CopyOnWriteArrayList<OnWindowAttachListener> mOnWindowAttachListeners;
+ private CopyOnWriteArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
+ @UnsupportedAppUsage
+ private CopyOnWriteArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
+ private CopyOnWriteArrayList<OnEnterAnimationCompleteListener>
+ mOnEnterAnimationCompleteListeners;
+
+ // Non-recursive listeners use CopyOnWriteArray
+ // Any listener invoked from ViewRootImpl.performTraversals() should not be recursive
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
+ @UnsupportedAppUsage
+ private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners;
+ @UnsupportedAppUsage
+ private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;
+ private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
+ private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners;
+ private CopyOnWriteArray<Consumer<List<Rect>>> mGestureExclusionListeners;
+
+ // These listeners cannot be mutated during dispatch
+ private boolean mInDispatchOnDraw;
+ private ArrayList<OnDrawListener> mOnDrawListeners;
+ private static boolean sIllegalOnDrawModificationIsFatal;
+
+ // These listeners are one-shot
+ private ArrayList<Runnable> mOnFrameCommitListeners;
+
+ /** Remains false until #dispatchOnWindowShown() is called. If a listener registers after
+ * that the listener will be immediately called. */
+ private boolean mWindowShown;
+
+ private boolean mAlive = true;
+
+ /**
+ * Interface definition for a callback to be invoked when the view hierarchy is
+ * attached to and detached from its window.
+ */
+ public interface OnWindowAttachListener {
+ /**
+ * Callback method to be invoked when the view hierarchy is attached to a window
+ */
+ public void onWindowAttached();
+
+ /**
+ * Callback method to be invoked when the view hierarchy is detached from a window
+ */
+ public void onWindowDetached();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the view hierarchy's window
+ * focus state changes.
+ */
+ public interface OnWindowFocusChangeListener {
+ /**
+ * Callback method to be invoked when the window focus changes in the view tree.
+ *
+ * @param hasFocus Set to true if the window is gaining focus, false if it is
+ * losing focus.
+ */
+ public void onWindowFocusChanged(boolean hasFocus);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the focus state within
+ * the view tree changes.
+ */
+ public interface OnGlobalFocusChangeListener {
+ /**
+ * Callback method to be invoked when the focus changes in the view tree. When
+ * the view tree transitions from touch mode to non-touch mode, oldFocus is null.
+ * When the view tree transitions from non-touch mode to touch mode, newFocus is
+ * null. When focus changes in non-touch mode (without transition from or to
+ * touch mode) either oldFocus or newFocus can be null.
+ *
+ * @param oldFocus The previously focused view, if any.
+ * @param newFocus The newly focused View, if any.
+ */
+ public void onGlobalFocusChanged(View oldFocus, View newFocus);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the global layout state
+ * or the visibility of views within the view tree changes.
+ */
+ public interface OnGlobalLayoutListener {
+ /**
+ * Callback method to be invoked when the global layout state or the visibility of views
+ * within the view tree changes
+ */
+ public void onGlobalLayout();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the view tree is about to be drawn.
+ */
+ public interface OnPreDrawListener {
+ /**
+ * Callback method to be invoked when the view tree is about to be drawn. At this point, all
+ * views in the tree have been measured and given a frame. Clients can use this to adjust
+ * their scroll bounds or even to request a new layout before drawing occurs.
+ *
+ * @return Return true to proceed with the current drawing pass, or false to cancel.
+ *
+ * @see android.view.View#onMeasure
+ * @see android.view.View#onLayout
+ * @see android.view.View#onDraw
+ */
+ public boolean onPreDraw();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the view tree is about to be drawn.
+ */
+ public interface OnDrawListener {
+ /**
+ * <p>Callback method to be invoked when the view tree is about to be drawn. At this point,
+ * views cannot be modified in any way.</p>
+ *
+ * <p>Unlike with {@link OnPreDrawListener}, this method cannot be used to cancel the
+ * current drawing pass.</p>
+ *
+ * <p>An {@link OnDrawListener} listener <strong>cannot be added or removed</strong>
+ * from this method.</p>
+ *
+ * @see android.view.View#onMeasure
+ * @see android.view.View#onLayout
+ * @see android.view.View#onDraw
+ */
+ public void onDraw();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the touch mode changes.
+ */
+ public interface OnTouchModeChangeListener {
+ /**
+ * Callback method to be invoked when the touch mode changes.
+ *
+ * @param isInTouchMode True if the view hierarchy is now in touch mode, false otherwise.
+ */
+ public void onTouchModeChanged(boolean isInTouchMode);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when
+ * something in the view tree has been scrolled.
+ */
+ public interface OnScrollChangedListener {
+ /**
+ * Callback method to be invoked when something in the view tree
+ * has been scrolled.
+ */
+ public void onScrollChanged();
+ }
+
+ /**
+ * Interface definition for a callback noting when a system window has been displayed.
+ * This is only used for non-Activity windows. Activity windows can use
+ * Activity.onEnterAnimationComplete() to get the same signal.
+ * @hide
+ */
+ public interface OnWindowShownListener {
+ /**
+ * Callback method to be invoked when a non-activity window is fully shown.
+ */
+ void onWindowShown();
+ }
+
+ /**
+ * Parameters used with OnComputeInternalInsetsListener.
+ *
+ * We are not yet ready to commit to this API and support it, so
+ * @hide
+ */
+ public final static class InternalInsetsInfo {
+
+ @UnsupportedAppUsage
+ public InternalInsetsInfo() {
+ }
+
+ /**
+ * Offsets from the frame of the window at which the content of
+ * windows behind it should be placed.
+ */
+ @UnsupportedAppUsage
+ public final Rect contentInsets = new Rect();
+
+ /**
+ * Offsets from the frame of the window at which windows behind it
+ * are visible.
+ */
+ @UnsupportedAppUsage
+ public final Rect visibleInsets = new Rect();
+
+ /**
+ * Touchable region defined relative to the origin of the frame of the window.
+ * Only used when {@link #setTouchableInsets(int)} is called with
+ * the option {@link #TOUCHABLE_INSETS_REGION}.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final Region touchableRegion = new Region();
+
+ /**
+ * Option for {@link #setTouchableInsets(int)}: the entire window frame
+ * can be touched.
+ */
+ public static final int TOUCHABLE_INSETS_FRAME = 0;
+
+ /**
+ * Option for {@link #setTouchableInsets(int)}: the area inside of
+ * the content insets can be touched.
+ */
+ public static final int TOUCHABLE_INSETS_CONTENT = 1;
+
+ /**
+ * Option for {@link #setTouchableInsets(int)}: the area inside of
+ * the visible insets can be touched.
+ */
+ public static final int TOUCHABLE_INSETS_VISIBLE = 2;
+
+ /**
+ * Option for {@link #setTouchableInsets(int)}: the area inside of
+ * the provided touchable region in {@link #touchableRegion} can be touched.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int TOUCHABLE_INSETS_REGION = 3;
+
+ /**
+ * Set which parts of the window can be touched: either
+ * {@link #TOUCHABLE_INSETS_FRAME}, {@link #TOUCHABLE_INSETS_CONTENT},
+ * {@link #TOUCHABLE_INSETS_VISIBLE}, or {@link #TOUCHABLE_INSETS_REGION}.
+ */
+ @UnsupportedAppUsage
+ public void setTouchableInsets(int val) {
+ mTouchableInsets = val;
+ }
+
+ @UnsupportedAppUsage
+ int mTouchableInsets;
+
+ void reset() {
+ contentInsets.setEmpty();
+ visibleInsets.setEmpty();
+ touchableRegion.setEmpty();
+ mTouchableInsets = TOUCHABLE_INSETS_FRAME;
+ }
+
+ boolean isEmpty() {
+ return contentInsets.isEmpty()
+ && visibleInsets.isEmpty()
+ && touchableRegion.isEmpty()
+ && mTouchableInsets == TOUCHABLE_INSETS_FRAME;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = contentInsets.hashCode();
+ result = 31 * result + visibleInsets.hashCode();
+ result = 31 * result + touchableRegion.hashCode();
+ result = 31 * result + mTouchableInsets;
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ InternalInsetsInfo other = (InternalInsetsInfo)o;
+ return mTouchableInsets == other.mTouchableInsets &&
+ contentInsets.equals(other.contentInsets) &&
+ visibleInsets.equals(other.visibleInsets) &&
+ touchableRegion.equals(other.touchableRegion);
+ }
+
+ @UnsupportedAppUsage
+ void set(InternalInsetsInfo other) {
+ contentInsets.set(other.contentInsets);
+ visibleInsets.set(other.visibleInsets);
+ touchableRegion.set(other.touchableRegion);
+ mTouchableInsets = other.mTouchableInsets;
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when layout has
+ * completed and the client can compute its interior insets.
+ *
+ * We are not yet ready to commit to this API and support it, so
+ * @hide
+ */
+ public interface OnComputeInternalInsetsListener {
+ /**
+ * Callback method to be invoked when layout has completed and the
+ * client can compute its interior insets.
+ *
+ * @param inoutInfo Should be filled in by the implementation with
+ * the information about the insets of the window. This is called
+ * with whatever values the previous OnComputeInternalInsetsListener
+ * returned, if there are multiple such listeners in the window.
+ */
+ public void onComputeInternalInsets(InternalInsetsInfo inoutInfo);
+ }
+
+ /**
+ * @hide
+ */
+ public interface OnEnterAnimationCompleteListener {
+ public void onEnterAnimationComplete();
+ }
+
+ /**
+ * Creates a new ViewTreeObserver. This constructor should not be called
+ */
+ ViewTreeObserver(Context context) {
+ sIllegalOnDrawModificationIsFatal =
+ context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O;
+ }
+
+ /**
+ * Merges all the listeners registered on the specified observer with the listeners
+ * registered on this object. After this method is invoked, the specified observer
+ * will return false in {@link #isAlive()} and should not be used anymore.
+ *
+ * @param observer The ViewTreeObserver whose listeners must be added to this observer
+ */
+ void merge(ViewTreeObserver observer) {
+ if (observer.mOnWindowAttachListeners != null) {
+ if (mOnWindowAttachListeners != null) {
+ mOnWindowAttachListeners.addAll(observer.mOnWindowAttachListeners);
+ } else {
+ mOnWindowAttachListeners = observer.mOnWindowAttachListeners;
+ }
+ }
+
+ if (observer.mOnWindowFocusListeners != null) {
+ if (mOnWindowFocusListeners != null) {
+ mOnWindowFocusListeners.addAll(observer.mOnWindowFocusListeners);
+ } else {
+ mOnWindowFocusListeners = observer.mOnWindowFocusListeners;
+ }
+ }
+
+ if (observer.mOnGlobalFocusListeners != null) {
+ if (mOnGlobalFocusListeners != null) {
+ mOnGlobalFocusListeners.addAll(observer.mOnGlobalFocusListeners);
+ } else {
+ mOnGlobalFocusListeners = observer.mOnGlobalFocusListeners;
+ }
+ }
+
+ if (observer.mOnGlobalLayoutListeners != null) {
+ if (mOnGlobalLayoutListeners != null) {
+ mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
+ } else {
+ mOnGlobalLayoutListeners = observer.mOnGlobalLayoutListeners;
+ }
+ }
+
+ if (observer.mOnPreDrawListeners != null) {
+ if (mOnPreDrawListeners != null) {
+ mOnPreDrawListeners.addAll(observer.mOnPreDrawListeners);
+ } else {
+ mOnPreDrawListeners = observer.mOnPreDrawListeners;
+ }
+ }
+
+ if (observer.mOnDrawListeners != null) {
+ if (mOnDrawListeners != null) {
+ mOnDrawListeners.addAll(observer.mOnDrawListeners);
+ } else {
+ mOnDrawListeners = observer.mOnDrawListeners;
+ }
+ }
+
+ if (observer.mOnFrameCommitListeners != null) {
+ if (mOnFrameCommitListeners != null) {
+ mOnFrameCommitListeners.addAll(observer.captureFrameCommitCallbacks());
+ } else {
+ mOnFrameCommitListeners = observer.captureFrameCommitCallbacks();
+ }
+ }
+
+ if (observer.mOnTouchModeChangeListeners != null) {
+ if (mOnTouchModeChangeListeners != null) {
+ mOnTouchModeChangeListeners.addAll(observer.mOnTouchModeChangeListeners);
+ } else {
+ mOnTouchModeChangeListeners = observer.mOnTouchModeChangeListeners;
+ }
+ }
+
+ if (observer.mOnComputeInternalInsetsListeners != null) {
+ if (mOnComputeInternalInsetsListeners != null) {
+ mOnComputeInternalInsetsListeners.addAll(observer.mOnComputeInternalInsetsListeners);
+ } else {
+ mOnComputeInternalInsetsListeners = observer.mOnComputeInternalInsetsListeners;
+ }
+ }
+
+ if (observer.mOnScrollChangedListeners != null) {
+ if (mOnScrollChangedListeners != null) {
+ mOnScrollChangedListeners.addAll(observer.mOnScrollChangedListeners);
+ } else {
+ mOnScrollChangedListeners = observer.mOnScrollChangedListeners;
+ }
+ }
+
+ if (observer.mOnWindowShownListeners != null) {
+ if (mOnWindowShownListeners != null) {
+ mOnWindowShownListeners.addAll(observer.mOnWindowShownListeners);
+ } else {
+ mOnWindowShownListeners = observer.mOnWindowShownListeners;
+ }
+ }
+
+ if (observer.mGestureExclusionListeners != null) {
+ if (mGestureExclusionListeners != null) {
+ mGestureExclusionListeners.addAll(observer.mGestureExclusionListeners);
+ } else {
+ mGestureExclusionListeners = observer.mGestureExclusionListeners;
+ }
+ }
+
+ observer.kill();
+ }
+
+ /**
+ * Register a callback to be invoked when the view hierarchy is attached to a window.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnWindowAttachListener(OnWindowAttachListener listener) {
+ checkIsAlive();
+
+ if (mOnWindowAttachListeners == null) {
+ mOnWindowAttachListeners
+ = new CopyOnWriteArrayList<OnWindowAttachListener>();
+ }
+
+ mOnWindowAttachListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed window attach callback.
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnWindowAttachListener(android.view.ViewTreeObserver.OnWindowAttachListener)
+ */
+ public void removeOnWindowAttachListener(OnWindowAttachListener victim) {
+ checkIsAlive();
+ if (mOnWindowAttachListeners == null) {
+ return;
+ }
+ mOnWindowAttachListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the window focus state within the view tree changes.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
+ checkIsAlive();
+
+ if (mOnWindowFocusListeners == null) {
+ mOnWindowFocusListeners
+ = new CopyOnWriteArrayList<OnWindowFocusChangeListener>();
+ }
+
+ mOnWindowFocusListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed window focus change callback.
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnWindowFocusChangeListener(android.view.ViewTreeObserver.OnWindowFocusChangeListener)
+ */
+ public void removeOnWindowFocusChangeListener(OnWindowFocusChangeListener victim) {
+ checkIsAlive();
+ if (mOnWindowFocusListeners == null) {
+ return;
+ }
+ mOnWindowFocusListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the focus state within the view tree changes.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnGlobalFocusChangeListener(OnGlobalFocusChangeListener listener) {
+ checkIsAlive();
+
+ if (mOnGlobalFocusListeners == null) {
+ mOnGlobalFocusListeners = new CopyOnWriteArrayList<OnGlobalFocusChangeListener>();
+ }
+
+ mOnGlobalFocusListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed focus change callback.
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnGlobalFocusChangeListener(OnGlobalFocusChangeListener)
+ */
+ public void removeOnGlobalFocusChangeListener(OnGlobalFocusChangeListener victim) {
+ checkIsAlive();
+ if (mOnGlobalFocusListeners == null) {
+ return;
+ }
+ mOnGlobalFocusListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the global layout state or the visibility of views
+ * within the view tree changes
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
+ checkIsAlive();
+
+ if (mOnGlobalLayoutListeners == null) {
+ mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
+ }
+
+ mOnGlobalLayoutListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed global layout callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @deprecated Use #removeOnGlobalLayoutListener instead
+ *
+ * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
+ */
+ @Deprecated
+ public void removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) {
+ removeOnGlobalLayoutListener(victim);
+ }
+
+ /**
+ * Remove a previously installed global layout callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
+ */
+ public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
+ checkIsAlive();
+ if (mOnGlobalLayoutListeners == null) {
+ return;
+ }
+ mOnGlobalLayoutListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the view tree is about to be drawn
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnPreDrawListener(OnPreDrawListener listener) {
+ checkIsAlive();
+
+ if (mOnPreDrawListeners == null) {
+ mOnPreDrawListeners = new CopyOnWriteArray<OnPreDrawListener>();
+ }
+
+ mOnPreDrawListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed pre-draw callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnPreDrawListener(OnPreDrawListener)
+ */
+ public void removeOnPreDrawListener(OnPreDrawListener victim) {
+ checkIsAlive();
+ if (mOnPreDrawListeners == null) {
+ return;
+ }
+ mOnPreDrawListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the view tree window has been shown
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ * @hide
+ */
+ public void addOnWindowShownListener(OnWindowShownListener listener) {
+ checkIsAlive();
+
+ if (mOnWindowShownListeners == null) {
+ mOnWindowShownListeners = new CopyOnWriteArray<OnWindowShownListener>();
+ }
+
+ mOnWindowShownListeners.add(listener);
+ if (mWindowShown) {
+ listener.onWindowShown();
+ }
+ }
+
+ /**
+ * Remove a previously installed window shown callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnWindowShownListener(OnWindowShownListener)
+ * @hide
+ */
+ public void removeOnWindowShownListener(OnWindowShownListener victim) {
+ checkIsAlive();
+ if (mOnWindowShownListeners == null) {
+ return;
+ }
+ mOnWindowShownListeners.remove(victim);
+ }
+
+ /**
+ * <p>Register a callback to be invoked when the view tree is about to be drawn.</p>
+ * <p><strong>Note:</strong> this method <strong>cannot</strong> be invoked from
+ * {@link android.view.ViewTreeObserver.OnDrawListener#onDraw()}.</p>
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnDrawListener(OnDrawListener listener) {
+ checkIsAlive();
+
+ if (mOnDrawListeners == null) {
+ mOnDrawListeners = new ArrayList<OnDrawListener>();
+ }
+
+ if (mInDispatchOnDraw) {
+ IllegalStateException ex = new IllegalStateException(
+ "Cannot call addOnDrawListener inside of onDraw");
+ if (sIllegalOnDrawModificationIsFatal) {
+ throw ex;
+ } else {
+ Log.e("ViewTreeObserver", ex.getMessage(), ex);
+ }
+ }
+ mOnDrawListeners.add(listener);
+ }
+
+ /**
+ * <p>Remove a previously installed pre-draw callback.</p>
+ * <p><strong>Note:</strong> this method <strong>cannot</strong> be invoked from
+ * {@link android.view.ViewTreeObserver.OnDrawListener#onDraw()}.</p>
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnDrawListener(OnDrawListener)
+ */
+ public void removeOnDrawListener(OnDrawListener victim) {
+ checkIsAlive();
+ if (mOnDrawListeners == null) {
+ return;
+ }
+ if (mInDispatchOnDraw) {
+ IllegalStateException ex = new IllegalStateException(
+ "Cannot call removeOnDrawListener inside of onDraw");
+ if (sIllegalOnDrawModificationIsFatal) {
+ throw ex;
+ } else {
+ Log.e("ViewTreeObserver", ex.getMessage(), ex);
+ }
+ }
+ mOnDrawListeners.remove(victim);
+ }
+
+ /**
+ * Adds a frame commit callback. This callback will be invoked when the current rendering
+ * content has been rendered into a frame and submitted to the swap chain. The frame may
+ * not currently be visible on the display when this is invoked, but it has been submitted.
+ * This callback is useful in combination with {@link PixelCopy} to capture the current
+ * rendered content of the UI reliably.
+ *
+ * Note: Only works with hardware rendering. Does nothing otherwise.
+ *
+ * @param callback The callback to invoke when the frame is committed.
+ */
+ public void registerFrameCommitCallback(@NonNull Runnable callback) {
+ checkIsAlive();
+ if (mOnFrameCommitListeners == null) {
+ mOnFrameCommitListeners = new ArrayList<>();
+ }
+ mOnFrameCommitListeners.add(callback);
+ }
+
+ @Nullable ArrayList<Runnable> captureFrameCommitCallbacks() {
+ ArrayList<Runnable> ret = mOnFrameCommitListeners;
+ mOnFrameCommitListeners = null;
+ return ret;
+ }
+
+ /**
+ * Attempts to remove the given callback from the list of pending frame complete callbacks.
+ *
+ * @param callback The callback to remove
+ * @return Whether or not the callback was removed. If this returns true the callback will
+ * not be invoked. If false is returned then the callback was either never added
+ * or may already be pending execution and was unable to be removed
+ */
+ public boolean unregisterFrameCommitCallback(@NonNull Runnable callback) {
+ checkIsAlive();
+ if (mOnFrameCommitListeners == null) {
+ return false;
+ }
+ return mOnFrameCommitListeners.remove(callback);
+ }
+
+ /**
+ * Register a callback to be invoked when a view has been scrolled.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnScrollChangedListener(OnScrollChangedListener listener) {
+ checkIsAlive();
+
+ if (mOnScrollChangedListeners == null) {
+ mOnScrollChangedListeners = new CopyOnWriteArray<OnScrollChangedListener>();
+ }
+
+ mOnScrollChangedListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed scroll-changed callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnScrollChangedListener(OnScrollChangedListener)
+ */
+ public void removeOnScrollChangedListener(OnScrollChangedListener victim) {
+ checkIsAlive();
+ if (mOnScrollChangedListeners == null) {
+ return;
+ }
+ mOnScrollChangedListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the invoked when the touch mode changes.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnTouchModeChangeListener(OnTouchModeChangeListener listener) {
+ checkIsAlive();
+
+ if (mOnTouchModeChangeListeners == null) {
+ mOnTouchModeChangeListeners = new CopyOnWriteArrayList<OnTouchModeChangeListener>();
+ }
+
+ mOnTouchModeChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed touch mode change callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnTouchModeChangeListener(OnTouchModeChangeListener)
+ */
+ public void removeOnTouchModeChangeListener(OnTouchModeChangeListener victim) {
+ checkIsAlive();
+ if (mOnTouchModeChangeListeners == null) {
+ return;
+ }
+ mOnTouchModeChangeListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the invoked when it is time to
+ * compute the window's internal insets.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * We are not yet ready to commit to this API and support it, so
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void addOnComputeInternalInsetsListener(OnComputeInternalInsetsListener listener) {
+ checkIsAlive();
+
+ if (mOnComputeInternalInsetsListeners == null) {
+ mOnComputeInternalInsetsListeners =
+ new CopyOnWriteArray<OnComputeInternalInsetsListener>();
+ }
+
+ mOnComputeInternalInsetsListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed internal insets computation callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnComputeInternalInsetsListener(OnComputeInternalInsetsListener)
+ *
+ * We are not yet ready to commit to this API and support it, so
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void removeOnComputeInternalInsetsListener(OnComputeInternalInsetsListener victim) {
+ checkIsAlive();
+ if (mOnComputeInternalInsetsListeners == null) {
+ return;
+ }
+ mOnComputeInternalInsetsListeners.remove(victim);
+ }
+
+ /**
+ * @hide
+ */
+ public void addOnEnterAnimationCompleteListener(OnEnterAnimationCompleteListener listener) {
+ checkIsAlive();
+ if (mOnEnterAnimationCompleteListeners == null) {
+ mOnEnterAnimationCompleteListeners =
+ new CopyOnWriteArrayList<OnEnterAnimationCompleteListener>();
+ }
+ mOnEnterAnimationCompleteListeners.add(listener);
+ }
+
+ /**
+ * @hide
+ */
+ public void removeOnEnterAnimationCompleteListener(OnEnterAnimationCompleteListener listener) {
+ checkIsAlive();
+ if (mOnEnterAnimationCompleteListeners == null) {
+ return;
+ }
+ mOnEnterAnimationCompleteListeners.remove(listener);
+ }
+
+ /**
+ * Add a listener to be notified when the tree's <em>transformed</em> gesture exclusion rects
+ * change. This could be the result of an animation or other layout change, or a view calling
+ * {@link View#setSystemGestureExclusionRects(List)}.
+ *
+ * @param listener listener to add
+ * @see View#setSystemGestureExclusionRects(List)
+ */
+ public void addOnSystemGestureExclusionRectsChangedListener(
+ @NonNull Consumer<List<Rect>> listener) {
+ checkIsAlive();
+ if (mGestureExclusionListeners == null) {
+ mGestureExclusionListeners = new CopyOnWriteArray<>();
+ }
+ mGestureExclusionListeners.add(listener);
+ }
+
+ /**
+ * Unsubscribe the given listener from gesture exclusion rect changes.
+ * @see #addOnSystemGestureExclusionRectsChangedListener(Consumer)
+ * @see View#setSystemGestureExclusionRects(List)
+ */
+ public void removeOnSystemGestureExclusionRectsChangedListener(
+ @NonNull Consumer<List<Rect>> listener) {
+ checkIsAlive();
+ if (mGestureExclusionListeners == null) {
+ return;
+ }
+ mGestureExclusionListeners.remove(listener);
+ }
+
+ private void checkIsAlive() {
+ if (!mAlive) {
+ throw new IllegalStateException("This ViewTreeObserver is not alive, call "
+ + "getViewTreeObserver() again");
+ }
+ }
+
+ /**
+ * Indicates whether this ViewTreeObserver is alive. When an observer is not alive,
+ * any call to a method (except this one) will throw an exception.
+ *
+ * If an application keeps a long-lived reference to this ViewTreeObserver, it should
+ * always check for the result of this method before calling any other method.
+ *
+ * @return True if this object is alive and be used, false otherwise.
+ */
+ public boolean isAlive() {
+ return mAlive;
+ }
+
+ /**
+ * Marks this ViewTreeObserver as not alive. After invoking this method, invoking
+ * any other method but {@link #isAlive()} and {@link #kill()} will throw an Exception.
+ *
+ * @hide
+ */
+ private void kill() {
+ mAlive = false;
+ }
+
+ /**
+ * Notifies registered listeners that window has been attached/detached.
+ */
+ final void dispatchOnWindowAttachedChange(boolean attached) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArrayList<OnWindowAttachListener> listeners
+ = mOnWindowAttachListeners;
+ if (listeners != null && listeners.size() > 0) {
+ for (OnWindowAttachListener listener : listeners) {
+ if (attached) listener.onWindowAttached();
+ else listener.onWindowDetached();
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that window focus has changed.
+ */
+ final void dispatchOnWindowFocusChange(boolean hasFocus) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArrayList<OnWindowFocusChangeListener> listeners
+ = mOnWindowFocusListeners;
+ if (listeners != null && listeners.size() > 0) {
+ for (OnWindowFocusChangeListener listener : listeners) {
+ listener.onWindowFocusChanged(hasFocus);
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that focus has changed.
+ */
+ @UnsupportedAppUsage
+ final void dispatchOnGlobalFocusChange(View oldFocus, View newFocus) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArrayList<OnGlobalFocusChangeListener> listeners = mOnGlobalFocusListeners;
+ if (listeners != null && listeners.size() > 0) {
+ for (OnGlobalFocusChangeListener listener : listeners) {
+ listener.onGlobalFocusChanged(oldFocus, newFocus);
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that a global layout happened. This can be called
+ * manually if you are forcing a layout on a View or a hierarchy of Views that are
+ * not attached to a Window or in the GONE state.
+ */
+ public final void dispatchOnGlobalLayout() {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
+ if (listeners != null && listeners.size() > 0) {
+ CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
+ try {
+ int count = access.size();
+ for (int i = 0; i < count; i++) {
+ access.get(i).onGlobalLayout();
+ }
+ } finally {
+ listeners.end();
+ }
+ }
+ }
+
+ /**
+ * Returns whether there are listeners for on pre-draw events.
+ */
+ final boolean hasOnPreDrawListeners() {
+ return mOnPreDrawListeners != null && mOnPreDrawListeners.size() > 0;
+ }
+
+ /**
+ * Notifies registered listeners that the drawing pass is about to start. If a
+ * listener returns true, then the drawing pass is canceled and rescheduled. This can
+ * be called manually if you are forcing the drawing on a View or a hierarchy of Views
+ * that are not attached to a Window or in the GONE state.
+ *
+ * @return True if the current draw should be canceled and rescheduled, false otherwise.
+ */
+ @SuppressWarnings("unchecked")
+ public final boolean dispatchOnPreDraw() {
+ boolean cancelDraw = false;
+ final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
+ if (listeners != null && listeners.size() > 0) {
+ CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
+ try {
+ int count = access.size();
+ for (int i = 0; i < count; i++) {
+ cancelDraw |= !(access.get(i).onPreDraw());
+ }
+ } finally {
+ listeners.end();
+ }
+ }
+ return cancelDraw;
+ }
+
+ /**
+ * Notifies registered listeners that the window is now shown
+ * @hide
+ */
+ @SuppressWarnings("unchecked")
+ public final void dispatchOnWindowShown() {
+ mWindowShown = true;
+ final CopyOnWriteArray<OnWindowShownListener> listeners = mOnWindowShownListeners;
+ if (listeners != null && listeners.size() > 0) {
+ CopyOnWriteArray.Access<OnWindowShownListener> access = listeners.start();
+ try {
+ int count = access.size();
+ for (int i = 0; i < count; i++) {
+ access.get(i).onWindowShown();
+ }
+ } finally {
+ listeners.end();
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that the drawing pass is about to start.
+ */
+ public final void dispatchOnDraw() {
+ if (mOnDrawListeners != null) {
+ mInDispatchOnDraw = true;
+ final ArrayList<OnDrawListener> listeners = mOnDrawListeners;
+ int numListeners = listeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ listeners.get(i).onDraw();
+ }
+ mInDispatchOnDraw = false;
+ }
+ }
+
+ /**
+ * Notifies registered listeners that the touch mode has changed.
+ *
+ * @param inTouchMode True if the touch mode is now enabled, false otherwise.
+ */
+ @UnsupportedAppUsage
+ final void dispatchOnTouchModeChanged(boolean inTouchMode) {
+ final CopyOnWriteArrayList<OnTouchModeChangeListener> listeners =
+ mOnTouchModeChangeListeners;
+ if (listeners != null && listeners.size() > 0) {
+ for (OnTouchModeChangeListener listener : listeners) {
+ listener.onTouchModeChanged(inTouchMode);
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that something has scrolled.
+ */
+ @UnsupportedAppUsage
+ final void dispatchOnScrollChanged() {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArray<OnScrollChangedListener> listeners = mOnScrollChangedListeners;
+ if (listeners != null && listeners.size() > 0) {
+ CopyOnWriteArray.Access<OnScrollChangedListener> access = listeners.start();
+ try {
+ int count = access.size();
+ for (int i = 0; i < count; i++) {
+ access.get(i).onScrollChanged();
+ }
+ } finally {
+ listeners.end();
+ }
+ }
+ }
+
+ /**
+ * Returns whether there are listeners for computing internal insets.
+ */
+ @UnsupportedAppUsage
+ final boolean hasComputeInternalInsetsListeners() {
+ final CopyOnWriteArray<OnComputeInternalInsetsListener> listeners =
+ mOnComputeInternalInsetsListeners;
+ return (listeners != null && listeners.size() > 0);
+ }
+
+ /**
+ * Calls all listeners to compute the current insets.
+ */
+ @UnsupportedAppUsage
+ final void dispatchOnComputeInternalInsets(InternalInsetsInfo inoutInfo) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArray<OnComputeInternalInsetsListener> listeners =
+ mOnComputeInternalInsetsListeners;
+ if (listeners != null && listeners.size() > 0) {
+ CopyOnWriteArray.Access<OnComputeInternalInsetsListener> access = listeners.start();
+ try {
+ int count = access.size();
+ for (int i = 0; i < count; i++) {
+ access.get(i).onComputeInternalInsets(inoutInfo);
+ }
+ } finally {
+ listeners.end();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public final void dispatchOnEnterAnimationComplete() {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ final CopyOnWriteArrayList<OnEnterAnimationCompleteListener> listeners =
+ mOnEnterAnimationCompleteListeners;
+ if (listeners != null && !listeners.isEmpty()) {
+ for (OnEnterAnimationCompleteListener listener : listeners) {
+ listener.onEnterAnimationComplete();
+ }
+ }
+ }
+
+ void dispatchOnSystemGestureExclusionRectsChanged(@NonNull List<Rect> rects) {
+ final CopyOnWriteArray<Consumer<List<Rect>>> listeners = mGestureExclusionListeners;
+ if (listeners != null && listeners.size() > 0) {
+ CopyOnWriteArray.Access<Consumer<List<Rect>>> access = listeners.start();
+ try {
+ final int count = access.size();
+ for (int i = 0; i < count; i++) {
+ access.get(i).accept(rects);
+ }
+ } finally {
+ listeners.end();
+ }
+ }
+ }
+
+ /**
+ * Copy on write array. This array is not thread safe, and only one loop can
+ * iterate over this array at any given time. This class avoids allocations
+ * until a concurrent modification happens.
+ *
+ * Usage:
+ *
+ * CopyOnWriteArray.Access<MyData> access = array.start();
+ * try {
+ * for (int i = 0; i < access.size(); i++) {
+ * MyData d = access.get(i);
+ * }
+ * } finally {
+ * access.end();
+ * }
+ */
+ static class CopyOnWriteArray<T> {
+ private ArrayList<T> mData = new ArrayList<T>();
+ private ArrayList<T> mDataCopy;
+
+ private final Access<T> mAccess = new Access<T>();
+
+ private boolean mStart;
+
+ static class Access<T> {
+ private ArrayList<T> mData;
+ private int mSize;
+
+ T get(int index) {
+ return mData.get(index);
+ }
+
+ int size() {
+ return mSize;
+ }
+ }
+
+ CopyOnWriteArray() {
+ }
+
+ private ArrayList<T> getArray() {
+ if (mStart) {
+ if (mDataCopy == null) mDataCopy = new ArrayList<T>(mData);
+ return mDataCopy;
+ }
+ return mData;
+ }
+
+ Access<T> start() {
+ if (mStart) throw new IllegalStateException("Iteration already started");
+ mStart = true;
+ mDataCopy = null;
+ mAccess.mData = mData;
+ mAccess.mSize = mData.size();
+ return mAccess;
+ }
+
+ void end() {
+ if (!mStart) throw new IllegalStateException("Iteration not started");
+ mStart = false;
+ if (mDataCopy != null) {
+ mData = mDataCopy;
+ mAccess.mData.clear();
+ mAccess.mSize = 0;
+ }
+ mDataCopy = null;
+ }
+
+ int size() {
+ return getArray().size();
+ }
+
+ void add(T item) {
+ getArray().add(item);
+ }
+
+ void addAll(CopyOnWriteArray<T> array) {
+ getArray().addAll(array.mData);
+ }
+
+ void remove(T item) {
+ getArray().remove(item);
+ }
+
+ void clear() {
+ getArray().clear();
+ }
+ }
+}
diff --git a/android/view/View_Delegate.java b/android/view/View_Delegate.java
new file mode 100644
index 0000000..6d82e8a
--- /dev/null
+++ b/android/view/View_Delegate.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.IBinder;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link View}
+ *
+ * Through the layoutlib_create tool, the original methods of View have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class View_Delegate {
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isInEditMode(View thisView) {
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static IBinder getWindowToken(View thisView) {
+ Context baseContext = BridgeContext.getBaseContext(thisView.getContext());
+ if (baseContext instanceof BridgeContext) {
+ return ((BridgeContext) baseContext).getBinder();
+ }
+ return null;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void draw(View thisView, Canvas canvas) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ thisView.draw_Original(canvas);
+ } catch (Throwable t) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "View draw failed", t, null, null);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean draw(
+ View thisView, Canvas canvas, ViewGroup parent, long drawingTime) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ return thisView.draw_Original(canvas, parent, drawingTime);
+ } catch (Throwable t) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "View draw failed", t, null, null);
+ }
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void measure(View thisView, int widthMeasureSpec, int heightMeasureSpec) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ thisView.measure_Original(widthMeasureSpec, heightMeasureSpec);
+ } catch (Throwable t) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "View measure failed", t, null, null);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void layout(View thisView, int l, int t, int r, int b) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ thisView.layout_Original(l, t, r, b);
+ } catch (Throwable th) {
+ Bridge.getLog().error(ILayoutLog.TAG_BROKEN, "View layout failed", th, null, null);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void dispatchDetachedFromWindow(View thisView) {
+ try {
+ // This code is run within a try/catch to prevent components from throwing user-visible
+ // exceptions when being disposed.
+ thisView.dispatchDetachedFromWindow_Original();
+ } catch (Throwable t) {
+ Context context = BridgeContext.getBaseContext(thisView.getContext());
+ if (context instanceof BridgeContext) {
+ ((BridgeContext) context).warn("Exception while detaching " + thisView.getClass(), t);
+ }
+ }
+ }
+}
diff --git a/android/view/Window.java b/android/view/Window.java
new file mode 100644
index 0000000..aa9ea19
--- /dev/null
+++ b/android/view/Window.java
@@ -0,0 +1,2739 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import static android.Manifest.permission.HIDE_OVERLAY_WINDOWS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.StyleRes;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UiContext;
+import android.app.WindowConfiguration;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.media.session.MediaController;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.transition.Scene;
+import android.transition.Transition;
+import android.transition.TransitionManager;
+import android.util.Pair;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Abstract base class for a top-level window look and behavior policy. An
+ * instance of this class should be used as the top-level view added to the
+ * window manager. It provides standard UI policies such as a background, title
+ * area, default key processing, etc.
+ *
+ * <p>The only existing implementation of this abstract class is
+ * android.view.PhoneWindow, which you should instantiate when needing a
+ * Window.
+ */
+public abstract class Window {
+ /** Flag for the "options panel" feature. This is enabled by default. */
+ public static final int FEATURE_OPTIONS_PANEL = 0;
+ /** Flag for the "no title" feature, turning off the title at the top
+ * of the screen. */
+ public static final int FEATURE_NO_TITLE = 1;
+
+ /**
+ * Flag for the progress indicator feature.
+ *
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public static final int FEATURE_PROGRESS = 2;
+
+ /** Flag for having an icon on the left side of the title bar */
+ public static final int FEATURE_LEFT_ICON = 3;
+ /** Flag for having an icon on the right side of the title bar */
+ public static final int FEATURE_RIGHT_ICON = 4;
+
+ /**
+ * Flag for indeterminate progress.
+ *
+ * @deprecated No longer supported starting in API 21.
+ */
+ @Deprecated
+ public static final int FEATURE_INDETERMINATE_PROGRESS = 5;
+
+ /** Flag for the context menu. This is enabled by default. */
+ public static final int FEATURE_CONTEXT_MENU = 6;
+ /** Flag for custom title. You cannot combine this feature with other title features. */
+ public static final int FEATURE_CUSTOM_TITLE = 7;
+ /**
+ * Flag for enabling the Action Bar.
+ * This is enabled by default for some devices. The Action Bar
+ * replaces the title bar and provides an alternate location
+ * for an on-screen menu button on some devices.
+ */
+ public static final int FEATURE_ACTION_BAR = 8;
+ /**
+ * Flag for requesting an Action Bar that overlays window content.
+ * Normally an Action Bar will sit in the space above window content, but if this
+ * feature is requested along with {@link #FEATURE_ACTION_BAR} it will be layered over
+ * the window content itself. This is useful if you would like your app to have more control
+ * over how the Action Bar is displayed, such as letting application content scroll beneath
+ * an Action Bar with a transparent background or otherwise displaying a transparent/translucent
+ * Action Bar over application content.
+ *
+ * <p>This mode is especially useful with {@link View#SYSTEM_UI_FLAG_FULLSCREEN
+ * View.SYSTEM_UI_FLAG_FULLSCREEN}, which allows you to seamlessly hide the
+ * action bar in conjunction with other screen decorations.
+ *
+ * <p>As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, when an
+ * ActionBar is in this mode it will adjust the insets provided to
+ * {@link View#fitSystemWindows(android.graphics.Rect) View.fitSystemWindows(Rect)}
+ * to include the content covered by the action bar, so you can do layout within
+ * that space.
+ */
+ public static final int FEATURE_ACTION_BAR_OVERLAY = 9;
+ /**
+ * Flag for specifying the behavior of action modes when an Action Bar is not present.
+ * If overlay is enabled, the action mode UI will be allowed to cover existing window content.
+ */
+ public static final int FEATURE_ACTION_MODE_OVERLAY = 10;
+ /**
+ * Flag for requesting a decoration-free window that is dismissed by swiping from the left.
+ *
+ * @deprecated Swipe-to-dismiss isn't functional anymore.
+ */
+ @Deprecated
+ public static final int FEATURE_SWIPE_TO_DISMISS = 11;
+ /**
+ * Flag for requesting that window content changes should be animated using a
+ * TransitionManager.
+ *
+ * <p>The TransitionManager is set using
+ * {@link #setTransitionManager(android.transition.TransitionManager)}. If none is set,
+ * a default TransitionManager will be used.</p>
+ *
+ * @see #setContentView
+ */
+ public static final int FEATURE_CONTENT_TRANSITIONS = 12;
+
+ /**
+ * Enables Activities to run Activity Transitions either through sending or receiving
+ * ActivityOptions bundle created with
+ * {@link android.app.ActivityOptions#makeSceneTransitionAnimation(android.app.Activity,
+ * android.util.Pair[])} or {@link android.app.ActivityOptions#makeSceneTransitionAnimation(
+ * android.app.Activity, View, String)}.
+ */
+ public static final int FEATURE_ACTIVITY_TRANSITIONS = 13;
+
+ /**
+ * Max value used as a feature ID
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int FEATURE_MAX = FEATURE_ACTIVITY_TRANSITIONS;
+
+ /**
+ * Flag for setting the progress bar's visibility to VISIBLE.
+ *
+ * @deprecated {@link #FEATURE_PROGRESS} and related methods are no longer
+ * supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_VISIBILITY_ON = -1;
+
+ /**
+ * Flag for setting the progress bar's visibility to GONE.
+ *
+ * @deprecated {@link #FEATURE_PROGRESS} and related methods are no longer
+ * supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_VISIBILITY_OFF = -2;
+
+ /**
+ * Flag for setting the progress bar's indeterminate mode on.
+ *
+ * @deprecated {@link #FEATURE_INDETERMINATE_PROGRESS} and related methods
+ * are no longer supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_INDETERMINATE_ON = -3;
+
+ /**
+ * Flag for setting the progress bar's indeterminate mode off.
+ *
+ * @deprecated {@link #FEATURE_INDETERMINATE_PROGRESS} and related methods
+ * are no longer supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_INDETERMINATE_OFF = -4;
+
+ /**
+ * Starting value for the (primary) progress.
+ *
+ * @deprecated {@link #FEATURE_PROGRESS} and related methods are no longer
+ * supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_START = 0;
+
+ /**
+ * Ending value for the (primary) progress.
+ *
+ * @deprecated {@link #FEATURE_PROGRESS} and related methods are no longer
+ * supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_END = 10000;
+
+ /**
+ * Lowest possible value for the secondary progress.
+ *
+ * @deprecated {@link #FEATURE_PROGRESS} and related methods are no longer
+ * supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_SECONDARY_START = 20000;
+
+ /**
+ * Highest possible value for the secondary progress.
+ *
+ * @deprecated {@link #FEATURE_PROGRESS} and related methods are no longer
+ * supported starting in API 21.
+ */
+ @Deprecated
+ public static final int PROGRESS_SECONDARY_END = 30000;
+
+ /**
+ * The transitionName for the status bar background View when a custom background is used.
+ * @see android.view.Window#setStatusBarColor(int)
+ */
+ public static final String STATUS_BAR_BACKGROUND_TRANSITION_NAME = "android:status:background";
+
+ /**
+ * The transitionName for the navigation bar background View when a custom background is used.
+ * @see android.view.Window#setNavigationBarColor(int)
+ */
+ public static final String NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME =
+ "android:navigation:background";
+
+ /**
+ * The default features enabled.
+ * @deprecated use {@link #getDefaultFeatures(android.content.Context)} instead.
+ */
+ @Deprecated
+ @SuppressWarnings({"PointlessBitwiseExpression"})
+ protected static final int DEFAULT_FEATURES = (1 << FEATURE_OPTIONS_PANEL) |
+ (1 << FEATURE_CONTEXT_MENU);
+
+ /**
+ * The ID that the main layout in the XML layout file should have.
+ */
+ public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
+
+ /**
+ * Flag for letting the theme drive the color of the window caption controls. Use with
+ * {@link #setDecorCaptionShade(int)}. This is the default value.
+ */
+ public static final int DECOR_CAPTION_SHADE_AUTO = 0;
+ /**
+ * Flag for setting light-color controls on the window caption. Use with
+ * {@link #setDecorCaptionShade(int)}.
+ */
+ public static final int DECOR_CAPTION_SHADE_LIGHT = 1;
+ /**
+ * Flag for setting dark-color controls on the window caption. Use with
+ * {@link #setDecorCaptionShade(int)}.
+ */
+ public static final int DECOR_CAPTION_SHADE_DARK = 2;
+
+ @UnsupportedAppUsage
+ @UiContext
+ private final Context mContext;
+
+ @UnsupportedAppUsage
+ private TypedArray mWindowStyle;
+ @UnsupportedAppUsage
+ private Callback mCallback;
+ private OnWindowDismissedCallback mOnWindowDismissedCallback;
+ private OnWindowSwipeDismissedCallback mOnWindowSwipeDismissedCallback;
+ private WindowControllerCallback mWindowControllerCallback;
+ private OnRestrictedCaptionAreaChangedListener mOnRestrictedCaptionAreaChangedListener;
+ private Rect mRestrictedCaptionAreaRect;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private WindowManager mWindowManager;
+ @UnsupportedAppUsage
+ private IBinder mAppToken;
+ @UnsupportedAppUsage
+ private String mAppName;
+ @UnsupportedAppUsage
+ private boolean mHardwareAccelerated;
+ private Window mContainer;
+ private Window mActiveChild;
+ private boolean mIsActive = false;
+ private boolean mHasChildren = false;
+ private boolean mCloseOnTouchOutside = false;
+ private boolean mSetCloseOnTouchOutside = false;
+ private int mForcedWindowFlags = 0;
+
+ @UnsupportedAppUsage
+ private int mFeatures;
+ @UnsupportedAppUsage
+ private int mLocalFeatures;
+
+ private boolean mHaveWindowFormat = false;
+ private boolean mHaveDimAmount = false;
+ private int mDefaultWindowFormat = PixelFormat.OPAQUE;
+
+ private boolean mHasSoftInputMode = false;
+
+ @UnsupportedAppUsage
+ private boolean mDestroyed;
+
+ private boolean mOverlayWithDecorCaptionEnabled = true;
+ private boolean mCloseOnSwipeEnabled = false;
+
+ // The current window attributes.
+ @UnsupportedAppUsage
+ private final WindowManager.LayoutParams mWindowAttributes =
+ new WindowManager.LayoutParams();
+
+ /**
+ * API from a Window back to its caller. This allows the client to
+ * intercept key dispatching, panels and menus, etc.
+ */
+ public interface Callback {
+ /**
+ * Called to process key events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchKeyEvent} to do the
+ * standard key processing.
+ *
+ * @param event The key event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event);
+
+ /**
+ * Called to process a key shortcut event.
+ * At the very least your implementation must call
+ * {@link android.view.Window#superDispatchKeyShortcutEvent} to do the
+ * standard key shortcut processing.
+ *
+ * @param event The key shortcut event.
+ * @return True if this event was consumed.
+ */
+ public boolean dispatchKeyShortcutEvent(KeyEvent event);
+
+ /**
+ * Called to process touch screen events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchTouchEvent} to do the
+ * standard touch screen processing.
+ *
+ * @param event The touch screen event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTouchEvent(MotionEvent event);
+
+ /**
+ * Called to process trackball events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchTrackballEvent} to do the
+ * standard trackball processing.
+ *
+ * @param event The trackball event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent event);
+
+ /**
+ * Called to process generic motion events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchGenericMotionEvent} to do the
+ * standard processing.
+ *
+ * @param event The generic motion event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchGenericMotionEvent(MotionEvent event);
+
+ /**
+ * Called to process population of {@link AccessibilityEvent}s.
+ *
+ * @param event The event.
+ *
+ * @return boolean Return true if event population was completed.
+ */
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+
+ /**
+ * Instantiate the view to display in the panel for 'featureId'.
+ * You can return null, in which case the default content (typically
+ * a menu) will be created for you.
+ *
+ * @param featureId Which panel is being created.
+ *
+ * @return view The top-level view to place in the panel.
+ *
+ * @see #onPreparePanel
+ */
+ @Nullable
+ public View onCreatePanelView(int featureId);
+
+ /**
+ * Initialize the contents of the menu for panel 'featureId'. This is
+ * called if onCreatePanelView() returns null, giving you a standard
+ * menu in which you can place your items. It is only called once for
+ * the panel, the first time it is shown.
+ *
+ * <p>You can safely hold on to <var>menu</var> (and any items created
+ * from it), making modifications to it as desired, until the next
+ * time onCreatePanelMenu() is called for this feature.
+ *
+ * @param featureId The panel being created.
+ * @param menu The menu inside the panel.
+ *
+ * @return boolean You must return true for the panel to be displayed;
+ * if you return false it will not be shown.
+ */
+ boolean onCreatePanelMenu(int featureId, @NonNull Menu menu);
+
+ /**
+ * Prepare a panel to be displayed. This is called right before the
+ * panel window is shown, every time it is shown.
+ *
+ * @param featureId The panel that is being displayed.
+ * @param view The View that was returned by onCreatePanelView().
+ * @param menu If onCreatePanelView() returned null, this is the Menu
+ * being displayed in the panel.
+ *
+ * @return boolean You must return true for the panel to be displayed;
+ * if you return false it will not be shown.
+ *
+ * @see #onCreatePanelView
+ */
+ boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu);
+
+ /**
+ * Called when a panel's menu is opened by the user. This may also be
+ * called when the menu is changing from one type to another (for
+ * example, from the icon menu to the expanded menu).
+ *
+ * @param featureId The panel that the menu is in.
+ * @param menu The menu that is opened.
+ * @return Return true to allow the menu to open, or false to prevent
+ * the menu from opening.
+ */
+ boolean onMenuOpened(int featureId, @NonNull Menu menu);
+
+ /**
+ * Called when a panel's menu item has been selected by the user.
+ *
+ * @param featureId The panel that the menu is in.
+ * @param item The menu item that was selected.
+ *
+ * @return boolean Return true to finish processing of selection, or
+ * false to perform the normal menu handling (calling its
+ * Runnable or sending a Message to its target Handler).
+ */
+ boolean onMenuItemSelected(int featureId, @NonNull MenuItem item);
+
+ /**
+ * This is called whenever the current window attributes change.
+ *
+ */
+ public void onWindowAttributesChanged(WindowManager.LayoutParams attrs);
+
+ /**
+ * This hook is called whenever the content view of the screen changes
+ * (due to a call to
+ * {@link Window#setContentView(View, android.view.ViewGroup.LayoutParams)
+ * Window.setContentView} or
+ * {@link Window#addContentView(View, android.view.ViewGroup.LayoutParams)
+ * Window.addContentView}).
+ */
+ public void onContentChanged();
+
+ /**
+ * This hook is called whenever the window focus changes. See
+ * {@link View#onWindowFocusChanged(boolean)
+ * View.onWindowFocusChangedNotLocked(boolean)} for more information.
+ *
+ * @param hasFocus Whether the window now has focus.
+ */
+ public void onWindowFocusChanged(boolean hasFocus);
+
+ /**
+ * Called when the window has been attached to the window manager.
+ * See {@link View#onAttachedToWindow() View.onAttachedToWindow()}
+ * for more information.
+ */
+ public void onAttachedToWindow();
+
+ /**
+ * Called when the window has been detached from the window manager.
+ * See {@link View#onDetachedFromWindow() View.onDetachedFromWindow()}
+ * for more information.
+ */
+ public void onDetachedFromWindow();
+
+ /**
+ * Called when a panel is being closed. If another logical subsequent
+ * panel is being opened (and this panel is being closed to make room for the subsequent
+ * panel), this method will NOT be called.
+ *
+ * @param featureId The panel that is being displayed.
+ * @param menu If onCreatePanelView() returned null, this is the Menu
+ * being displayed in the panel.
+ */
+ void onPanelClosed(int featureId, @NonNull Menu menu);
+
+ /**
+ * Called when the user signals the desire to start a search.
+ *
+ * @return true if search launched, false if activity refuses (blocks)
+ *
+ * @see android.app.Activity#onSearchRequested()
+ */
+ public boolean onSearchRequested();
+
+ /**
+ * Called when the user signals the desire to start a search.
+ *
+ * @param searchEvent A {@link SearchEvent} describing the signal to
+ * start a search.
+ * @return true if search launched, false if activity refuses (blocks)
+ */
+ public boolean onSearchRequested(SearchEvent searchEvent);
+
+ /**
+ * Called when an action mode is being started for this window. Gives the
+ * callback an opportunity to handle the action mode in its own unique and
+ * beautiful way. If this method returns null the system can choose a way
+ * to present the mode or choose not to start the mode at all. This is equivalent
+ * to {@link #onWindowStartingActionMode(android.view.ActionMode.Callback, int)}
+ * with type {@link ActionMode#TYPE_PRIMARY}.
+ *
+ * @param callback Callback to control the lifecycle of this action mode
+ * @return The ActionMode that was started, or null if the system should present it
+ */
+ @Nullable
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback);
+
+ /**
+ * Called when an action mode is being started for this window. Gives the
+ * callback an opportunity to handle the action mode in its own unique and
+ * beautiful way. If this method returns null the system can choose a way
+ * to present the mode or choose not to start the mode at all.
+ *
+ * @param callback Callback to control the lifecycle of this action mode
+ * @param type One of {@link ActionMode#TYPE_PRIMARY} or {@link ActionMode#TYPE_FLOATING}.
+ * @return The ActionMode that was started, or null if the system should present it
+ */
+ @Nullable
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type);
+
+ /**
+ * Called when an action mode has been started. The appropriate mode callback
+ * method will have already been invoked.
+ *
+ * @param mode The new mode that has just been started.
+ */
+ public void onActionModeStarted(ActionMode mode);
+
+ /**
+ * Called when an action mode has been finished. The appropriate mode callback
+ * method will have already been invoked.
+ *
+ * @param mode The mode that was just finished.
+ */
+ public void onActionModeFinished(ActionMode mode);
+
+ /**
+ * Called when Keyboard Shortcuts are requested for the current window.
+ *
+ * @param data The data list to populate with shortcuts.
+ * @param menu The current menu, which may be null.
+ * @param deviceId The id for the connected device the shortcuts should be provided for.
+ */
+ default public void onProvideKeyboardShortcuts(
+ List<KeyboardShortcutGroup> data, @Nullable Menu menu, int deviceId) { };
+
+ /**
+ * Called when pointer capture is enabled or disabled for the current window.
+ *
+ * @param hasCapture True if the window has pointer capture.
+ */
+ default public void onPointerCaptureChanged(boolean hasCapture) { };
+ }
+
+ /** @hide */
+ public interface OnWindowDismissedCallback {
+ /**
+ * Called when a window is dismissed. This informs the callback that the
+ * window is gone, and it should finish itself.
+ * @param finishTask True if the task should also be finished.
+ * @param suppressWindowTransition True if the resulting exit and enter window transition
+ * animations should be suppressed.
+ */
+ void onWindowDismissed(boolean finishTask, boolean suppressWindowTransition);
+ }
+
+ /** @hide */
+ public interface OnWindowSwipeDismissedCallback {
+ /**
+ * Called when a window is swipe dismissed. This informs the callback that the
+ * window is gone, and it should finish itself.
+ * @param finishTask True if the task should also be finished.
+ * @param suppressWindowTransition True if the resulting exit and enter window transition
+ * animations should be suppressed.
+ */
+ void onWindowSwipeDismissed();
+ }
+
+ /** @hide */
+ public interface WindowControllerCallback {
+ /**
+ * Moves the activity between {@link WindowConfiguration#WINDOWING_MODE_FREEFORM} windowing
+ * mode and {@link WindowConfiguration#WINDOWING_MODE_FULLSCREEN}.
+ */
+ void toggleFreeformWindowingMode();
+
+ /**
+ * Puts the activity in picture-in-picture mode if the activity supports.
+ * @see android.R.attr#supportsPictureInPicture
+ */
+ void enterPictureInPictureModeIfPossible();
+
+ /** Returns whether the window belongs to the task root. */
+ boolean isTaskRoot();
+
+ /**
+ * Update the status bar color to a forced one.
+ */
+ void updateStatusBarColor(int color);
+
+ /**
+ * Update the navigation bar color to a forced one.
+ */
+ void updateNavigationBarColor(int color);
+ }
+
+ /**
+ * Callback for clients that want to be aware of where caption draws content.
+ */
+ public interface OnRestrictedCaptionAreaChangedListener {
+ /**
+ * Called when the area where caption draws content changes.
+ *
+ * @param rect The area where caption content is positioned, relative to the top view.
+ */
+ void onRestrictedCaptionAreaChanged(Rect rect);
+ }
+
+ /**
+ * Callback for clients that want frame timing information for each
+ * frame rendered by the Window.
+ */
+ public interface OnFrameMetricsAvailableListener {
+ /**
+ * Called when information is available for the previously rendered frame.
+ *
+ * Reports can be dropped if this callback takes too
+ * long to execute, as the report producer cannot wait for the consumer to
+ * complete.
+ *
+ * It is highly recommended that clients copy the passed in FrameMetrics
+ * via {@link FrameMetrics#FrameMetrics(FrameMetrics)} within this method and defer
+ * additional computation or storage to another thread to avoid unnecessarily
+ * dropping reports.
+ *
+ * @param window The {@link Window} on which the frame was displayed.
+ * @param frameMetrics the available metrics. This object is reused on every call
+ * and thus <strong>this reference is not valid outside the scope of this method</strong>.
+ * @param dropCountSinceLastInvocation the number of reports dropped since the last time
+ * this callback was invoked.
+ */
+ void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics,
+ int dropCountSinceLastInvocation);
+ }
+
+ /**
+ * Listener for applying window insets on the content of a window. Used only by the framework to
+ * fit content according to legacy SystemUI flags.
+ *
+ * @hide
+ */
+ public interface OnContentApplyWindowInsetsListener {
+
+ /**
+ * Called when the window needs to apply insets on the container of its content view which
+ * are set by calling {@link #setContentView}. The method should determine what insets to
+ * apply on the container of the root level content view and what should be dispatched to
+ * the content view's
+ * {@link View#setOnApplyWindowInsetsListener(OnApplyWindowInsetsListener)} through the view
+ * hierarchy.
+ *
+ * @param view The view for which to apply insets. Must not be directly modified.
+ * @param insets The root level insets that are about to be dispatched
+ * @return A pair, with the first element containing the insets to apply as margin to the
+ * root-level content views, and the second element determining what should be
+ * dispatched to the content view.
+ */
+ @NonNull
+ Pair<Insets, WindowInsets> onContentApplyWindowInsets(@NonNull View view,
+ @NonNull WindowInsets insets);
+ }
+
+
+ public Window(@UiContext Context context) {
+ mContext = context;
+ mFeatures = mLocalFeatures = getDefaultFeatures(context);
+ }
+
+ /**
+ * Return the Context this window policy is running in, for retrieving
+ * resources and other information.
+ *
+ * @return Context The Context that was supplied to the constructor.
+ */
+ @UiContext
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the {@link android.R.styleable#Window} attributes from this
+ * window's theme.
+ */
+ public final TypedArray getWindowStyle() {
+ synchronized (this) {
+ if (mWindowStyle == null) {
+ mWindowStyle = mContext.obtainStyledAttributes(
+ com.android.internal.R.styleable.Window);
+ }
+ return mWindowStyle;
+ }
+ }
+
+ /**
+ * Set the container for this window. If not set, the DecorWindow
+ * operates as a top-level window; otherwise, it negotiates with the
+ * container to display itself appropriately.
+ *
+ * @param container The desired containing Window.
+ */
+ public void setContainer(Window container) {
+ mContainer = container;
+ if (container != null) {
+ // Embedded screens never have a title.
+ mFeatures |= 1<<FEATURE_NO_TITLE;
+ mLocalFeatures |= 1<<FEATURE_NO_TITLE;
+ container.mHasChildren = true;
+ }
+ }
+
+ /**
+ * Return the container for this Window.
+ *
+ * @return Window The containing window, or null if this is a
+ * top-level window.
+ */
+ public final Window getContainer() {
+ return mContainer;
+ }
+
+ public final boolean hasChildren() {
+ return mHasChildren;
+ }
+
+ /** @hide */
+ public final void destroy() {
+ mDestroyed = true;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public final boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ /**
+ * Set the window manager for use by this Window to, for example,
+ * display panels. This is <em>not</em> used for displaying the
+ * Window itself -- that must be done by the client.
+ *
+ * @param wm The window manager for adding new windows.
+ */
+ public void setWindowManager(WindowManager wm, IBinder appToken, String appName) {
+ setWindowManager(wm, appToken, appName, false);
+ }
+
+ /**
+ * Set the window manager for use by this Window to, for example,
+ * display panels. This is <em>not</em> used for displaying the
+ * Window itself -- that must be done by the client.
+ *
+ * @param wm The window manager for adding new windows.
+ */
+ public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
+ boolean hardwareAccelerated) {
+ mAppToken = appToken;
+ mAppName = appName;
+ mHardwareAccelerated = hardwareAccelerated;
+ if (wm == null) {
+ wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+ }
+ mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
+ }
+
+ void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
+ CharSequence curTitle = wp.getTitle();
+ if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
+ wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
+ if (wp.token == null) {
+ View decor = peekDecorView();
+ if (decor != null) {
+ wp.token = decor.getWindowToken();
+ }
+ }
+ if (curTitle == null || curTitle.length() == 0) {
+ final StringBuilder title = new StringBuilder(32);
+ if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
+ title.append("Media");
+ } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) {
+ title.append("MediaOvr");
+ } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
+ title.append("Panel");
+ } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) {
+ title.append("SubPanel");
+ } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL) {
+ title.append("AboveSubPanel");
+ } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
+ title.append("AtchDlg");
+ } else {
+ title.append(wp.type);
+ }
+ if (mAppName != null) {
+ title.append(":").append(mAppName);
+ }
+ wp.setTitle(title);
+ }
+ } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
+ wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
+ // We don't set the app token to this system window because the life cycles should be
+ // independent. If an app creates a system window and then the app goes to the stopped
+ // state, the system window should not be affected (can still show and receive input
+ // events).
+ if (curTitle == null || curTitle.length() == 0) {
+ final StringBuilder title = new StringBuilder(32);
+ title.append("Sys").append(wp.type);
+ if (mAppName != null) {
+ title.append(":").append(mAppName);
+ }
+ wp.setTitle(title);
+ }
+ } else {
+ if (wp.token == null) {
+ wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
+ }
+ if ((curTitle == null || curTitle.length() == 0)
+ && mAppName != null) {
+ wp.setTitle(mAppName);
+ }
+ }
+ if (wp.packageName == null) {
+ wp.packageName = mContext.getPackageName();
+ }
+ if (mHardwareAccelerated ||
+ (mWindowAttributes.flags & FLAG_HARDWARE_ACCELERATED) != 0) {
+ wp.flags |= FLAG_HARDWARE_ACCELERATED;
+ }
+ }
+
+ /**
+ * Return the window manager allowing this Window to display its own
+ * windows.
+ *
+ * @return WindowManager The ViewManager.
+ */
+ public WindowManager getWindowManager() {
+ return mWindowManager;
+ }
+
+ /**
+ * Set the Callback interface for this window, used to intercept key
+ * events and other dynamic operations in the window.
+ *
+ * @param callback The desired Callback interface.
+ */
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Return the current Callback interface for this window.
+ */
+ public final Callback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * Set an observer to collect frame stats for each frame rendered in this window.
+ *
+ * Must be in hardware rendering mode.
+ */
+ public final void addOnFrameMetricsAvailableListener(
+ @NonNull OnFrameMetricsAvailableListener listener,
+ Handler handler) {
+ final View decorView = getDecorView();
+ if (decorView == null) {
+ throw new IllegalStateException("can't observe a Window without an attached view");
+ }
+
+ if (listener == null) {
+ throw new NullPointerException("listener cannot be null");
+ }
+
+ decorView.addFrameMetricsListener(this, listener, handler);
+ }
+
+ /**
+ * Remove observer and stop listening to frame stats for this window.
+ */
+ public final void removeOnFrameMetricsAvailableListener(OnFrameMetricsAvailableListener listener) {
+ final View decorView = getDecorView();
+ if (decorView != null) {
+ getDecorView().removeFrameMetricsListener(listener);
+ }
+ }
+
+ /** @hide */
+ public final void setOnWindowDismissedCallback(OnWindowDismissedCallback dcb) {
+ mOnWindowDismissedCallback = dcb;
+ }
+
+ /** @hide */
+ public final void dispatchOnWindowDismissed(
+ boolean finishTask, boolean suppressWindowTransition) {
+ if (mOnWindowDismissedCallback != null) {
+ mOnWindowDismissedCallback.onWindowDismissed(finishTask, suppressWindowTransition);
+ }
+ }
+
+ /** @hide */
+ public final void setOnWindowSwipeDismissedCallback(OnWindowSwipeDismissedCallback sdcb) {
+ mOnWindowSwipeDismissedCallback = sdcb;
+ }
+
+ /** @hide */
+ public final void dispatchOnWindowSwipeDismissed() {
+ if (mOnWindowSwipeDismissedCallback != null) {
+ mOnWindowSwipeDismissedCallback.onWindowSwipeDismissed();
+ }
+ }
+
+ /** @hide */
+ public final void setWindowControllerCallback(WindowControllerCallback wccb) {
+ mWindowControllerCallback = wccb;
+ }
+
+ /** @hide */
+ public final WindowControllerCallback getWindowControllerCallback() {
+ return mWindowControllerCallback;
+ }
+
+ /**
+ * Set a callback for changes of area where caption will draw its content.
+ *
+ * @param listener Callback that will be called when the area changes.
+ */
+ public final void setRestrictedCaptionAreaListener(OnRestrictedCaptionAreaChangedListener listener) {
+ mOnRestrictedCaptionAreaChangedListener = listener;
+ mRestrictedCaptionAreaRect = listener != null ? new Rect() : null;
+ }
+
+ /**
+ * Prevent non-system overlay windows from being drawn on top of this window.
+ *
+ * @param hide whether non-system overlay windows should be hidden.
+ */
+ @RequiresPermission(HIDE_OVERLAY_WINDOWS)
+ public final void setHideOverlayWindows(boolean hide) {
+ // This permission check is here to throw early and let the developer know that they need
+ // to hold HIDE_OVERLAY_WINDOWS for the flag to have any effect. The WM verifies that the
+ // owner of the window has the permission before applying the flag, but this is done
+ // asynchronously.
+ if (mContext.checkSelfPermission(HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != PERMISSION_GRANTED
+ && mContext.checkSelfPermission(HIDE_OVERLAY_WINDOWS) != PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Permission denial: setHideOverlayWindows: HIDE_OVERLAY_WINDOWS");
+ }
+ setPrivateFlags(hide ? SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS : 0,
+ SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ }
+
+ /**
+ * Take ownership of this window's surface. The window's view hierarchy
+ * will no longer draw into the surface, though it will otherwise continue
+ * to operate (such as for receiving input events). The given SurfaceHolder
+ * callback will be used to tell you about state changes to the surface.
+ */
+ public abstract void takeSurface(SurfaceHolder.Callback2 callback);
+
+ /**
+ * Take ownership of this window's InputQueue. The window will no
+ * longer read and dispatch input events from the queue; it is your
+ * responsibility to do so.
+ */
+ public abstract void takeInputQueue(InputQueue.Callback callback);
+
+ /**
+ * Return whether this window is being displayed with a floating style
+ * (based on the {@link android.R.attr#windowIsFloating} attribute in
+ * the style/theme).
+ *
+ * @return Returns true if the window is configured to be displayed floating
+ * on top of whatever is behind it.
+ */
+ public abstract boolean isFloating();
+
+ /**
+ * Set the width and height layout parameters of the window. The default
+ * for both of these is MATCH_PARENT; you can change them to WRAP_CONTENT
+ * or an absolute value to make a window that is not full-screen.
+ *
+ * @param width The desired layout width of the window.
+ * @param height The desired layout height of the window.
+ *
+ * @see ViewGroup.LayoutParams#height
+ * @see ViewGroup.LayoutParams#width
+ */
+ public void setLayout(int width, int height) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.width = width;
+ attrs.height = height;
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Set the gravity of the window, as per the Gravity constants. This
+ * controls how the window manager is positioned in the overall window; it
+ * is only useful when using WRAP_CONTENT for the layout width or height.
+ *
+ * @param gravity The desired gravity constant.
+ *
+ * @see Gravity
+ * @see #setLayout
+ */
+ public void setGravity(int gravity)
+ {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.gravity = gravity;
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Set the type of the window, as per the WindowManager.LayoutParams
+ * types.
+ *
+ * @param type The new window type (see WindowManager.LayoutParams).
+ */
+ public void setType(int type) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.type = type;
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Set the format of window, as per the PixelFormat types. This overrides
+ * the default format that is selected by the Window based on its
+ * window decorations.
+ *
+ * @param format The new window format (see PixelFormat). Use
+ * PixelFormat.UNKNOWN to allow the Window to select
+ * the format.
+ *
+ * @see PixelFormat
+ */
+ public void setFormat(int format) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ if (format != PixelFormat.UNKNOWN) {
+ attrs.format = format;
+ mHaveWindowFormat = true;
+ } else {
+ attrs.format = mDefaultWindowFormat;
+ mHaveWindowFormat = false;
+ }
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Specify custom animations to use for the window, as per
+ * {@link WindowManager.LayoutParams#windowAnimations
+ * WindowManager.LayoutParams.windowAnimations}. Providing anything besides
+ * 0 here will override the animations the window would
+ * normally retrieve from its theme.
+ */
+ public void setWindowAnimations(@StyleRes int resId) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.windowAnimations = resId;
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Specify an explicit soft input mode to use for the window, as per
+ * {@link WindowManager.LayoutParams#softInputMode
+ * WindowManager.LayoutParams.softInputMode}. Providing anything besides
+ * "unspecified" here will override the input mode the window would
+ * normally retrieve from its theme.
+ */
+ public void setSoftInputMode(int mode) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ if (mode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
+ attrs.softInputMode = mode;
+ mHasSoftInputMode = true;
+ } else {
+ mHasSoftInputMode = false;
+ }
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Convenience function to set the flag bits as specified in flags, as
+ * per {@link #setFlags}.
+ * @param flags The flag bits to be set.
+ * @see #setFlags
+ * @see #clearFlags
+ */
+ public void addFlags(int flags) {
+ setFlags(flags, flags);
+ }
+
+ /**
+ * Add private flag bits.
+ *
+ * <p>Refer to the individual flags for the permissions needed.
+ *
+ * @param flags The flag bits to add.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void addPrivateFlags(int flags) {
+ setPrivateFlags(flags, flags);
+ }
+
+ /**
+ * Add system flag bits.
+ *
+ * <p>Refer to the individual flags for the permissions needed.
+ *
+ * <p>Note: Only for updateable system components (aka. mainline modules)
+ *
+ * @param flags The flag bits to add.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void addSystemFlags(@WindowManager.LayoutParams.SystemFlags int flags) {
+ addPrivateFlags(flags);
+ }
+
+ /**
+ * Convenience function to clear the flag bits as specified in flags, as
+ * per {@link #setFlags}.
+ * @param flags The flag bits to be cleared.
+ * @see #setFlags
+ * @see #addFlags
+ */
+ public void clearFlags(int flags) {
+ setFlags(0, flags);
+ }
+
+ /**
+ * Set the flags of the window, as per the
+ * {@link WindowManager.LayoutParams WindowManager.LayoutParams}
+ * flags.
+ *
+ * <p>Note that some flags must be set before the window decoration is
+ * created (by the first call to
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)} or
+ * {@link #getDecorView()}:
+ * {@link WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN} and
+ * {@link WindowManager.LayoutParams#FLAG_LAYOUT_INSET_DECOR}. These
+ * will be set for you based on the {@link android.R.attr#windowIsFloating}
+ * attribute.
+ *
+ * @param flags The new window flags (see WindowManager.LayoutParams).
+ * @param mask Which of the window flag bits to modify.
+ * @see #addFlags
+ * @see #clearFlags
+ */
+ public void setFlags(int flags, int mask) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.flags = (attrs.flags&~mask) | (flags&mask);
+ mForcedWindowFlags |= mask;
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ private void setPrivateFlags(int flags, int mask) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.privateFlags = (attrs.privateFlags & ~mask) | (flags & mask);
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * {@hide}
+ */
+ protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) {
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+
+ /**
+ * <p>Sets the requested color mode of the window. The requested the color mode might
+ * override the window's pixel {@link WindowManager.LayoutParams#format format}.</p>
+ *
+ * <p>The requested color mode must be one of {@link ActivityInfo#COLOR_MODE_DEFAULT},
+ * {@link ActivityInfo#COLOR_MODE_WIDE_COLOR_GAMUT} or {@link ActivityInfo#COLOR_MODE_HDR}.</p>
+ *
+ * <p>The requested color mode is not guaranteed to be honored. Please refer to
+ * {@link #getColorMode()} for more information.</p>
+ *
+ * @see #getColorMode()
+ * @see Display#isWideColorGamut()
+ * @see Configuration#isScreenWideColorGamut()
+ */
+ public void setColorMode(@ActivityInfo.ColorMode int colorMode) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.setColorMode(colorMode);
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * If {@code isPreferred} is true, this method requests that the connected display does minimal
+ * post processing when this window is visible on the screen. Otherwise, it requests that the
+ * display switches back to standard image processing.
+ *
+ * <p> By default, the display does not do minimal post processing and if this is desired, this
+ * method should not be used. It should be used with {@code isPreferred=true} when low
+ * latency has a higher priority than image enhancement processing (e.g. for games or video
+ * conferencing). The display will automatically go back into standard image processing mode
+ * when no window requesting minimal posst processing is visible on screen anymore.
+ * {@code setPreferMinimalPostProcessing(false)} can be used if
+ * {@code setPreferMinimalPostProcessing(true)} was previously called for this window and
+ * minimal post processing is no longer required.
+ *
+ * <p>If the Display sink is connected via HDMI, the device will begin to send infoframes with
+ * Auto Low Latency Mode enabled and Game Content Type. This will switch the connected display
+ * to a minimal image processing mode (if available), which reduces latency, improving the user
+ * experience for gaming or video conferencing applications. For more information, see HDMI 2.1
+ * specification.
+ *
+ * <p>If the Display sink has an internal connection or uses some other protocol than HDMI,
+ * effects may be similar but implementation-defined.
+ *
+ * <p>The ability to switch to a mode with minimal post proessing may be disabled by a user
+ * setting in the system settings menu. In that case, this method does nothing.
+ *
+ * @see android.content.pm.ActivityInfo#FLAG_PREFER_MINIMAL_POST_PROCESSING
+ * @see android.view.Display#isMinimalPostProcessingSupported
+ * @see android.view.WindowManager.LayoutParams#preferMinimalPostProcessing
+ *
+ * @param isPreferred Indicates whether minimal post processing is preferred for this window
+ * ({@code isPreferred=true}) or not ({@code isPreferred=false}).
+ */
+ public void setPreferMinimalPostProcessing(boolean isPreferred) {
+ mWindowAttributes.preferMinimalPostProcessing = isPreferred;
+ dispatchWindowAttributesChanged(mWindowAttributes);
+ }
+
+ /**
+ * Returns the requested color mode of the window, one of
+ * {@link ActivityInfo#COLOR_MODE_DEFAULT}, {@link ActivityInfo#COLOR_MODE_WIDE_COLOR_GAMUT}
+ * or {@link ActivityInfo#COLOR_MODE_HDR}. If {@link ActivityInfo#COLOR_MODE_WIDE_COLOR_GAMUT}
+ * was requested it is possible the window will not be put in wide color gamut mode depending
+ * on device and display support for that mode. Use {@link #isWideColorGamut} to determine
+ * if the window is currently in wide color gamut mode.
+ *
+ * @see #setColorMode(int)
+ * @see Display#isWideColorGamut()
+ * @see Configuration#isScreenWideColorGamut()
+ */
+ @ActivityInfo.ColorMode
+ public int getColorMode() {
+ return getAttributes().getColorMode();
+ }
+
+ /**
+ * Returns true if this window's color mode is {@link ActivityInfo#COLOR_MODE_WIDE_COLOR_GAMUT},
+ * the display has a wide color gamut and this device supports wide color gamut rendering.
+ *
+ * @see Display#isWideColorGamut()
+ * @see Configuration#isScreenWideColorGamut()
+ */
+ public boolean isWideColorGamut() {
+ return getColorMode() == ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
+ && getContext().getResources().getConfiguration().isScreenWideColorGamut();
+ }
+
+ /**
+ * Set the amount of dim behind the window when using
+ * {@link WindowManager.LayoutParams#FLAG_DIM_BEHIND}. This overrides
+ * the default dim amount of that is selected by the Window based on
+ * its theme.
+ *
+ * @param amount The new dim amount, from 0 for no dim to 1 for full dim.
+ */
+ public void setDimAmount(float amount) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.dimAmount = amount;
+ mHaveDimAmount = true;
+ dispatchWindowAttributesChanged(attrs);
+ }
+
+ /**
+ * Sets whether the decor view should fit root-level content views for {@link WindowInsets}.
+ * <p>
+ * If set to {@code true}, the framework will inspect the now deprecated
+ * {@link View#SYSTEM_UI_LAYOUT_FLAGS} as well the
+ * {@link WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE} flag and fits content according
+ * to these flags.
+ * </p>
+ * <p>
+ * If set to {@code false}, the framework will not fit the content view to the insets and will
+ * just pass through the {@link WindowInsets} to the content view.
+ * </p>
+ * @param decorFitsSystemWindows Whether the decor view should fit root-level content views for
+ * insets.
+ */
+ public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
+ }
+
+ /** @hide */
+ public boolean decorFitsSystemWindows() {
+ return false;
+ }
+
+ /**
+ * Specify custom window attributes. <strong>PLEASE NOTE:</strong> the
+ * layout params you give here should generally be from values previously
+ * retrieved with {@link #getAttributes()}; you probably do not want to
+ * blindly create and apply your own, since this will blow away any values
+ * set by the framework that you are not interested in.
+ *
+ * @param a The new window attributes, which will completely override any
+ * current values.
+ */
+ public void setAttributes(WindowManager.LayoutParams a) {
+ mWindowAttributes.copyFrom(a);
+ dispatchWindowAttributesChanged(mWindowAttributes);
+ }
+
+ /**
+ * Retrieve the current window attributes associated with this panel.
+ *
+ * @return WindowManager.LayoutParams Either the existing window
+ * attributes object, or a freshly created one if there is none.
+ */
+ public final WindowManager.LayoutParams getAttributes() {
+ return mWindowAttributes;
+ }
+
+ /**
+ * Return the window flags that have been explicitly set by the client,
+ * so will not be modified by {@link #getDecorView}.
+ */
+ protected final int getForcedWindowFlags() {
+ return mForcedWindowFlags;
+ }
+
+ /**
+ * Has the app specified their own soft input mode?
+ */
+ protected final boolean hasSoftInputMode() {
+ return mHasSoftInputMode;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public void setCloseOnTouchOutside(boolean close) {
+ mCloseOnTouchOutside = close;
+ mSetCloseOnTouchOutside = true;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public void setCloseOnTouchOutsideIfNotSet(boolean close) {
+ if (!mSetCloseOnTouchOutside) {
+ mCloseOnTouchOutside = close;
+ mSetCloseOnTouchOutside = true;
+ }
+ }
+
+ /** @hide */
+ @SuppressWarnings("HiddenAbstractMethod")
+ @UnsupportedAppUsage
+ public abstract void alwaysReadCloseOnTouchAttr();
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
+ final boolean isOutside =
+ event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
+ || event.getAction() == MotionEvent.ACTION_OUTSIDE;
+ if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
+ return true;
+ }
+ return false;
+ }
+
+ /* Sets the Sustained Performance requirement for the calling window.
+ * @param enable disables or enables the mode.
+ */
+ public void setSustainedPerformanceMode(boolean enable) {
+ setPrivateFlags(enable
+ ? WindowManager.LayoutParams.PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE : 0,
+ WindowManager.LayoutParams.PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE);
+ }
+
+ private boolean isOutOfBounds(Context context, MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
+ final View decorView = getDecorView();
+ return (x < -slop) || (y < -slop)
+ || (x > (decorView.getWidth()+slop))
+ || (y > (decorView.getHeight()+slop));
+ }
+
+ /**
+ * Enable extended screen features. This must be called before
+ * setContentView(). May be called as many times as desired as long as it
+ * is before setContentView(). If not called, no extended features
+ * will be available. You can not turn off a feature once it is requested.
+ * You canot use other title features with {@link #FEATURE_CUSTOM_TITLE}.
+ *
+ * @param featureId The desired features, defined as constants by Window.
+ * @return The features that are now set.
+ */
+ public boolean requestFeature(int featureId) {
+ final int flag = 1<<featureId;
+ mFeatures |= flag;
+ mLocalFeatures |= mContainer != null ? (flag&~mContainer.mFeatures) : flag;
+ return (mFeatures&flag) != 0;
+ }
+
+ /**
+ * @hide Used internally to help resolve conflicting features.
+ */
+ protected void removeFeature(int featureId) {
+ final int flag = 1<<featureId;
+ mFeatures &= ~flag;
+ mLocalFeatures &= ~(mContainer != null ? (flag&~mContainer.mFeatures) : flag);
+ }
+
+ public final void makeActive() {
+ if (mContainer != null) {
+ if (mContainer.mActiveChild != null) {
+ mContainer.mActiveChild.mIsActive = false;
+ }
+ mContainer.mActiveChild = this;
+ }
+ mIsActive = true;
+ onActive();
+ }
+
+ public final boolean isActive()
+ {
+ return mIsActive;
+ }
+
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link android.app.Activity#onCreate}.
+ * <p>
+ * This will implicitly call {@link #getDecorView} with all of the associated side-effects.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID if found, or {@code null} otherwise
+ * @see View#findViewById(int)
+ * @see Window#requireViewById(int)
+ */
+ @Nullable
+ public <T extends View> T findViewById(@IdRes int id) {
+ return getDecorView().findViewById(id);
+ }
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link android.app.Activity#onCreate}, or throws an
+ * IllegalArgumentException if the ID is invalid, or there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Window#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Window");
+ }
+ return view;
+ }
+
+ /**
+ * Convenience for
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
+ * to set the screen content from a layout resource. The resource will be
+ * inflated, adding all top-level views to the screen.
+ *
+ * @param layoutResID Resource ID to be inflated.
+ * @see #setContentView(View, android.view.ViewGroup.LayoutParams)
+ */
+ public abstract void setContentView(@LayoutRes int layoutResID);
+
+ /**
+ * Convenience for
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
+ * set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarhcy.
+ *
+ * @param view The desired content to display.
+ * @see #setContentView(View, android.view.ViewGroup.LayoutParams)
+ */
+ public abstract void setContentView(View view);
+
+ /**
+ * Set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarchy.
+ *
+ * <p>Note that calling this function "locks in" various characteristics
+ * of the window that can not, from this point forward, be changed: the
+ * features that have been requested with {@link #requestFeature(int)},
+ * and certain window flags as described in {@link #setFlags(int, int)}.</p>
+ *
+ * <p>If {@link #FEATURE_CONTENT_TRANSITIONS} is set, the window's
+ * TransitionManager will be used to animate content from the current
+ * content View to view.</p>
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ * @see #getTransitionManager()
+ * @see #setTransitionManager(android.transition.TransitionManager)
+ */
+ public abstract void setContentView(View view, ViewGroup.LayoutParams params);
+
+ /**
+ * Variation on
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
+ * to add an additional content view to the screen. Added after any existing
+ * ones in the screen -- existing views are NOT removed.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public abstract void addContentView(View view, ViewGroup.LayoutParams params);
+
+ /**
+ * Remove the view that was used as the screen content.
+ *
+ * @hide
+ */
+ @SuppressWarnings("HiddenAbstractMethod")
+ public abstract void clearContentView();
+
+ /**
+ * Return the view in this Window that currently has focus, or null if
+ * there are none. Note that this does not look in any containing
+ * Window.
+ *
+ * @return View The current View with focus or null.
+ */
+ @Nullable
+ public abstract View getCurrentFocus();
+
+ /**
+ * Quick access to the {@link LayoutInflater} instance that this Window
+ * retrieved from its Context.
+ *
+ * @return LayoutInflater The shared LayoutInflater.
+ */
+ @NonNull
+ public abstract LayoutInflater getLayoutInflater();
+
+ public abstract void setTitle(CharSequence title);
+
+ @Deprecated
+ public abstract void setTitleColor(@ColorInt int textColor);
+
+ public abstract void openPanel(int featureId, KeyEvent event);
+
+ public abstract void closePanel(int featureId);
+
+ public abstract void togglePanel(int featureId, KeyEvent event);
+
+ public abstract void invalidatePanelMenu(int featureId);
+
+ public abstract boolean performPanelShortcut(int featureId,
+ int keyCode,
+ KeyEvent event,
+ int flags);
+ public abstract boolean performPanelIdentifierAction(int featureId,
+ int id,
+ int flags);
+
+ public abstract void closeAllPanels();
+
+ public abstract boolean performContextMenuIdentifierAction(int id, int flags);
+
+ /**
+ * Should be called when the configuration is changed.
+ *
+ * @param newConfig The new configuration.
+ */
+ public abstract void onConfigurationChanged(Configuration newConfig);
+
+ /**
+ * Sets the window elevation.
+ * <p>
+ * Changes to this property take effect immediately and will cause the
+ * window surface to be recreated. This is an expensive operation and as a
+ * result, this property should not be animated.
+ *
+ * @param elevation The window elevation.
+ * @see View#setElevation(float)
+ * @see android.R.styleable#Window_windowElevation
+ */
+ public void setElevation(float elevation) {}
+
+ /**
+ * Gets the window elevation.
+ *
+ * @hide
+ */
+ public float getElevation() {
+ return 0.0f;
+ }
+
+ /**
+ * Sets whether window content should be clipped to the outline of the
+ * window background.
+ *
+ * @param clipToOutline Whether window content should be clipped to the
+ * outline of the window background.
+ * @see View#setClipToOutline(boolean)
+ * @see android.R.styleable#Window_windowClipToOutline
+ */
+ public void setClipToOutline(boolean clipToOutline) {}
+
+ /**
+ * Change the background of this window to a Drawable resource. Setting the
+ * background to null will make the window be opaque. To make the window
+ * transparent, you can use an empty drawable (for instance a ColorDrawable
+ * with the color 0 or the system drawable android:drawable/empty.)
+ *
+ * @param resId The resource identifier of a drawable resource which will
+ * be installed as the new background.
+ */
+ public void setBackgroundDrawableResource(@DrawableRes int resId) {
+ setBackgroundDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * Change the background of this window to a custom Drawable. Setting the
+ * background to null will make the window be opaque. To make the window
+ * transparent, you can use an empty drawable (for instance a ColorDrawable
+ * with the color 0 or the system drawable android:drawable/empty.)
+ *
+ * @param drawable The new Drawable to use for this window's background.
+ */
+ public abstract void setBackgroundDrawable(Drawable drawable);
+
+ /**
+ * <p>
+ * Blurs the screen behind the window within the bounds of the window.
+ * </p><p>
+ * The density of the blur is set by the blur radius. The radius defines the size
+ * of the neighbouring area, from which pixels will be averaged to form the final
+ * color for each pixel. The operation approximates a Gaussian blur.
+ * A radius of 0 means no blur. The higher the radius, the denser the blur.
+ * </p><p>
+ * The window background drawable is drawn on top of the blurred region. The blur
+ * region bounds and rounded corners will mimic those of the background drawable.
+ * </p><p>
+ * For the blur region to be visible, the window has to be translucent
+ * (see {@link android.R.attr#windowIsTranslucent}) and floating
+ * (see {@link android.R.attr#windowIsFloating}).
+ * </p><p>
+ * Note the difference with {@link WindowManager.LayoutParams#setBlurBehindRadius},
+ * which blurs the whole screen behind the window. Background blur blurs the screen behind
+ * only within the bounds of the window.
+ * </p><p>
+ * Some devices might not support cross-window blur due to GPU limitations. It can also be
+ * disabled at runtime, e.g. during battery saving mode, when multimedia tunneling is used or
+ * when minimal post processing is requested. In such situations, no blur will be computed or
+ * drawn, resulting in a transparent window background. To avoid this, the app might want to
+ * change its theme to one that does not use blurs. To listen for cross-window blur
+ * enabled/disabled events, use {@link WindowManager#addCrossWindowBlurEnabledListener}.
+ * </p>
+ *
+ * @param blurRadius The blur radius to use for window background blur in pixels
+ *
+ * @see android.R.styleable#Window_windowBackgroundBlurRadius
+ * @see WindowManager.LayoutParams#setBlurBehindRadius
+ * @see WindowManager#addCrossWindowBlurEnabledListener
+ */
+ public void setBackgroundBlurRadius(int blurRadius) {}
+
+ /**
+ * Set the value for a drawable feature of this window, from a resource
+ * identifier. You must have called requestFeature(featureId) before
+ * calling this function.
+ *
+ * @see android.content.res.Resources#getDrawable(int)
+ *
+ * @param featureId The desired drawable feature to change, defined as a
+ * constant by Window.
+ * @param resId Resource identifier of the desired image.
+ */
+ public abstract void setFeatureDrawableResource(int featureId, @DrawableRes int resId);
+
+ /**
+ * Set the value for a drawable feature of this window, from a URI. You
+ * must have called requestFeature(featureId) before calling this
+ * function.
+ *
+ * <p>The only URI currently supported is "content:", specifying an image
+ * in a content provider.
+ *
+ * @see android.widget.ImageView#setImageURI
+ *
+ * @param featureId The desired drawable feature to change. Features are
+ * constants defined by Window.
+ * @param uri The desired URI.
+ */
+ public abstract void setFeatureDrawableUri(int featureId, Uri uri);
+
+ /**
+ * Set an explicit Drawable value for feature of this window. You must
+ * have called requestFeature(featureId) before calling this function.
+ *
+ * @param featureId The desired drawable feature to change. Features are
+ * constants defined by Window.
+ * @param drawable A Drawable object to display.
+ */
+ public abstract void setFeatureDrawable(int featureId, Drawable drawable);
+
+ /**
+ * Set a custom alpha value for the given drawable feature, controlling how
+ * much the background is visible through it.
+ *
+ * @param featureId The desired drawable feature to change. Features are
+ * constants defined by Window.
+ * @param alpha The alpha amount, 0 is completely transparent and 255 is
+ * completely opaque.
+ */
+ public abstract void setFeatureDrawableAlpha(int featureId, int alpha);
+
+ /**
+ * Set the integer value for a feature. The range of the value depends on
+ * the feature being set. For {@link #FEATURE_PROGRESS}, it should go from
+ * 0 to 10000. At 10000 the progress is complete and the indicator hidden.
+ *
+ * @param featureId The desired feature to change. Features are constants
+ * defined by Window.
+ * @param value The value for the feature. The interpretation of this
+ * value is feature-specific.
+ */
+ public abstract void setFeatureInt(int featureId, int value);
+
+ /**
+ * Request that key events come to this activity. Use this if your
+ * activity has no views with focus, but the activity still wants
+ * a chance to process key events.
+ */
+ public abstract void takeKeyEvents(boolean get);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the key press event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchKeyEvent(KeyEvent event);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the key shortcut press event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchKeyShortcutEvent(KeyEvent event);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the touch screen event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchTouchEvent(MotionEvent event);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the trackball event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchTrackballEvent(MotionEvent event);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the generic motion event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchGenericMotionEvent(MotionEvent event);
+
+ /**
+ * Retrieve the top-level window decor view (containing the standard
+ * window frame/decorations and the client's content inside of that), which
+ * can be added as a window to the window manager.
+ *
+ * <p><em>Note that calling this function for the first time "locks in"
+ * various window characteristics as described in
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}.</em></p>
+ *
+ * @return Returns the top-level window decor view.
+ */
+ public abstract @NonNull View getDecorView();
+
+ /**
+ * @return the status bar background view or null.
+ * @hide
+ */
+ @TestApi
+ public @Nullable View getStatusBarBackgroundView() {
+ return null;
+ }
+
+ /**
+ * @return the navigation bar background view or null.
+ * @hide
+ */
+ @TestApi
+ public @Nullable View getNavigationBarBackgroundView() {
+ return null;
+ }
+
+ /**
+ * Retrieve the current decor view, but only if it has already been created;
+ * otherwise returns null.
+ *
+ * @return Returns the top-level window decor or null.
+ * @see #getDecorView
+ */
+ public abstract View peekDecorView();
+
+ public abstract Bundle saveHierarchyState();
+
+ public abstract void restoreHierarchyState(Bundle savedInstanceState);
+
+ protected abstract void onActive();
+
+ /**
+ * Return the feature bits that are enabled. This is the set of features
+ * that were given to requestFeature(), and are being handled by this
+ * Window itself or its container. That is, it is the set of
+ * requested features that you can actually use.
+ *
+ * <p>To do: add a public version of this API that allows you to check for
+ * features by their feature ID.
+ *
+ * @return int The feature bits.
+ */
+ protected final int getFeatures()
+ {
+ return mFeatures;
+ }
+
+ /**
+ * Return the feature bits set by default on a window.
+ * @param context The context used to access resources
+ */
+ public static int getDefaultFeatures(Context context) {
+ int features = 0;
+
+ final Resources res = context.getResources();
+ if (res.getBoolean(com.android.internal.R.bool.config_defaultWindowFeatureOptionsPanel)) {
+ features |= 1 << FEATURE_OPTIONS_PANEL;
+ }
+
+ if (res.getBoolean(com.android.internal.R.bool.config_defaultWindowFeatureContextMenu)) {
+ features |= 1 << FEATURE_CONTEXT_MENU;
+ }
+
+ return features;
+ }
+
+ /**
+ * Query for the availability of a certain feature.
+ *
+ * @param feature The feature ID to check
+ * @return true if the feature is enabled, false otherwise.
+ */
+ public boolean hasFeature(int feature) {
+ return (getFeatures() & (1 << feature)) != 0;
+ }
+
+ /**
+ * Return the feature bits that are being implemented by this Window.
+ * This is the set of features that were given to requestFeature(), and are
+ * being handled by only this Window itself, not by its containers.
+ *
+ * @return int The feature bits.
+ */
+ protected final int getLocalFeatures()
+ {
+ return mLocalFeatures;
+ }
+
+ /**
+ * Set the default format of window, as per the PixelFormat types. This
+ * is the format that will be used unless the client specifies in explicit
+ * format with setFormat();
+ *
+ * @param format The new window format (see PixelFormat).
+ *
+ * @see #setFormat
+ * @see PixelFormat
+ */
+ protected void setDefaultWindowFormat(int format) {
+ mDefaultWindowFormat = format;
+ if (!mHaveWindowFormat) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.format = format;
+ dispatchWindowAttributesChanged(attrs);
+ }
+ }
+
+ /** @hide */
+ protected boolean haveDimAmount() {
+ return mHaveDimAmount;
+ }
+
+ public abstract void setChildDrawable(int featureId, Drawable drawable);
+
+ public abstract void setChildInt(int featureId, int value);
+
+ /**
+ * Is a keypress one of the defined shortcut keys for this window.
+ * @param keyCode the key code from {@link android.view.KeyEvent} to check.
+ * @param event the {@link android.view.KeyEvent} to use to help check.
+ */
+ public abstract boolean isShortcutKey(int keyCode, KeyEvent event);
+
+ /**
+ * @see android.app.Activity#setVolumeControlStream(int)
+ */
+ public abstract void setVolumeControlStream(int streamType);
+
+ /**
+ * @see android.app.Activity#getVolumeControlStream()
+ */
+ public abstract int getVolumeControlStream();
+
+ /**
+ * Sets a {@link MediaController} to send media keys and volume changes to.
+ * If set, this should be preferred for all media keys and volume requests
+ * sent to this window.
+ *
+ * @param controller The controller for the session which should receive
+ * media keys and volume changes.
+ * @see android.app.Activity#setMediaController(android.media.session.MediaController)
+ */
+ public void setMediaController(MediaController controller) {
+ }
+
+ /**
+ * Gets the {@link MediaController} that was previously set.
+ *
+ * @return The controller which should receive events.
+ * @see #setMediaController(android.media.session.MediaController)
+ * @see android.app.Activity#getMediaController()
+ */
+ public MediaController getMediaController() {
+ return null;
+ }
+
+ /**
+ * Set extra options that will influence the UI for this window.
+ * @param uiOptions Flags specifying extra options for this window.
+ */
+ public void setUiOptions(int uiOptions) { }
+
+ /**
+ * Set extra options that will influence the UI for this window.
+ * Only the bits filtered by mask will be modified.
+ * @param uiOptions Flags specifying extra options for this window.
+ * @param mask Flags specifying which options should be modified. Others will remain unchanged.
+ */
+ public void setUiOptions(int uiOptions, int mask) { }
+
+ /**
+ * Set the primary icon for this window.
+ *
+ * @param resId resource ID of a drawable to set
+ */
+ public void setIcon(@DrawableRes int resId) { }
+
+ /**
+ * Set the default icon for this window.
+ * This will be overridden by any other icon set operation which could come from the
+ * theme or another explicit set.
+ *
+ * @hide
+ */
+ public void setDefaultIcon(@DrawableRes int resId) { }
+
+ /**
+ * Set the logo for this window. A logo is often shown in place of an
+ * {@link #setIcon(int) icon} but is generally wider and communicates window title information
+ * as well.
+ *
+ * @param resId resource ID of a drawable to set
+ */
+ public void setLogo(@DrawableRes int resId) { }
+
+ /**
+ * Set the default logo for this window.
+ * This will be overridden by any other logo set operation which could come from the
+ * theme or another explicit set.
+ *
+ * @hide
+ */
+ public void setDefaultLogo(@DrawableRes int resId) { }
+
+ /**
+ * Set focus locally. The window should have the
+ * {@link WindowManager.LayoutParams#FLAG_LOCAL_FOCUS_MODE} flag set already.
+ * @param hasFocus Whether this window has focus or not.
+ * @param inTouchMode Whether this window is in touch mode or not.
+ */
+ public void setLocalFocus(boolean hasFocus, boolean inTouchMode) { }
+
+ /**
+ * Inject an event to window locally.
+ * @param event A key or touch event to inject to this window.
+ */
+ public void injectInputEvent(InputEvent event) { }
+
+ /**
+ * Retrieve the {@link TransitionManager} responsible for for default transitions
+ * in this window. Requires {@link #FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * <p>This method will return non-null after content has been initialized (e.g. by using
+ * {@link #setContentView}) if {@link #FEATURE_CONTENT_TRANSITIONS} has been granted.</p>
+ *
+ * @return This window's content TransitionManager or null if none is set.
+ * @attr ref android.R.styleable#Window_windowContentTransitionManager
+ */
+ public TransitionManager getTransitionManager() {
+ return null;
+ }
+
+ /**
+ * Set the {@link TransitionManager} to use for default transitions in this window.
+ * Requires {@link #FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * @param tm The TransitionManager to use for scene changes.
+ * @attr ref android.R.styleable#Window_windowContentTransitionManager
+ */
+ public void setTransitionManager(TransitionManager tm) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Retrieve the {@link Scene} representing this window's current content.
+ * Requires {@link #FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * <p>This method will return null if the current content is not represented by a Scene.</p>
+ *
+ * @return Current Scene being shown or null
+ */
+ public Scene getContentScene() {
+ return null;
+ }
+
+ /**
+ * Sets the Transition that will be used to move Views into the initial scene. The entering
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#INVISIBLE} to {@link View#VISIBLE}. If <code>transition</code> is null,
+ * entering Views will remain unaffected.
+ *
+ * @param transition The Transition to use to move Views into the initial Scene.
+ * @attr ref android.R.styleable#Window_windowEnterTransition
+ */
+ public void setEnterTransition(Transition transition) {}
+
+ /**
+ * Sets the Transition that will be used to move Views out of the scene when the Window is
+ * preparing to close, for example after a call to
+ * {@link android.app.Activity#finishAfterTransition()}. The exiting
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#VISIBLE} to {@link View#INVISIBLE}. If <code>transition</code> is null,
+ * entering Views will remain unaffected. If nothing is set, the default will be to
+ * use the same value as set in {@link #setEnterTransition(android.transition.Transition)}.
+ *
+ * @param transition The Transition to use to move Views out of the Scene when the Window
+ * is preparing to close.
+ * @attr ref android.R.styleable#Window_windowReturnTransition
+ */
+ public void setReturnTransition(Transition transition) {}
+
+ /**
+ * Sets the Transition that will be used to move Views out of the scene when starting a
+ * new Activity. The exiting Views will be those that are regular Views or ViewGroups that
+ * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as exiting is governed by changing visibility
+ * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will
+ * remain unaffected. Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param transition The Transition to use to move Views out of the scene when calling a
+ * new Activity.
+ * @attr ref android.R.styleable#Window_windowExitTransition
+ */
+ public void setExitTransition(Transition transition) {}
+
+ /**
+ * Sets the Transition that will be used to move Views in to the scene when returning from
+ * a previously-started Activity. The entering Views will be those that are regular Views
+ * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions
+ * will extend {@link android.transition.Visibility} as exiting is governed by changing
+ * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null,
+ * the views will remain unaffected. If nothing is set, the default will be to use the same
+ * transition as {@link #setExitTransition(android.transition.Transition)}.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param transition The Transition to use to move Views into the scene when reentering from a
+ * previously-started Activity.
+ * @attr ref android.R.styleable#Window_windowReenterTransition
+ */
+ public void setReenterTransition(Transition transition) {}
+
+ /**
+ * Returns the transition used to move Views into the initial scene. The entering
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#INVISIBLE} to {@link View#VISIBLE}. If <code>transition</code> is null,
+ * entering Views will remain unaffected. Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return the Transition to use to move Views into the initial Scene.
+ * @attr ref android.R.styleable#Window_windowEnterTransition
+ */
+ public Transition getEnterTransition() { return null; }
+
+ /**
+ * Returns the Transition that will be used to move Views out of the scene when the Window is
+ * preparing to close, for example after a call to
+ * {@link android.app.Activity#finishAfterTransition()}. The exiting
+ * Views will be those that are regular Views or ViewGroups that have
+ * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as entering is governed by changing visibility from
+ * {@link View#VISIBLE} to {@link View#INVISIBLE}.
+ *
+ * @return The Transition to use to move Views out of the Scene when the Window
+ * is preparing to close.
+ * @attr ref android.R.styleable#Window_windowReturnTransition
+ */
+ public Transition getReturnTransition() { return null; }
+
+ /**
+ * Returns the Transition that will be used to move Views out of the scene when starting a
+ * new Activity. The exiting Views will be those that are regular Views or ViewGroups that
+ * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend
+ * {@link android.transition.Visibility} as exiting is governed by changing visibility
+ * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will
+ * remain unaffected. Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return the Transition to use to move Views out of the scene when calling a
+ * new Activity.
+ * @attr ref android.R.styleable#Window_windowExitTransition
+ */
+ public Transition getExitTransition() { return null; }
+
+ /**
+ * Returns the Transition that will be used to move Views in to the scene when returning from
+ * a previously-started Activity. The entering Views will be those that are regular Views
+ * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions
+ * will extend {@link android.transition.Visibility} as exiting is governed by changing
+ * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return The Transition to use to move Views into the scene when reentering from a
+ * previously-started Activity.
+ * @attr ref android.R.styleable#Window_windowReenterTransition
+ */
+ public Transition getReenterTransition() { return null; }
+
+ /**
+ * Sets the Transition that will be used for shared elements transferred into the content
+ * Scene. Typical Transitions will affect size and location, such as
+ * {@link android.transition.ChangeBounds}. A null
+ * value will cause transferred shared elements to blink to the final position.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param transition The Transition to use for shared elements transferred into the content
+ * Scene.
+ * @attr ref android.R.styleable#Window_windowSharedElementEnterTransition
+ */
+ public void setSharedElementEnterTransition(Transition transition) {}
+
+ /**
+ * Sets the Transition that will be used for shared elements transferred back to a
+ * calling Activity. Typical Transitions will affect size and location, such as
+ * {@link android.transition.ChangeBounds}. A null
+ * value will cause transferred shared elements to blink to the final position.
+ * If no value is set, the default will be to use the same value as
+ * {@link #setSharedElementEnterTransition(android.transition.Transition)}.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param transition The Transition to use for shared elements transferred out of the content
+ * Scene.
+ * @attr ref android.R.styleable#Window_windowSharedElementReturnTransition
+ */
+ public void setSharedElementReturnTransition(Transition transition) {}
+
+ /**
+ * Returns the Transition that will be used for shared elements transferred into the content
+ * Scene. Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return Transition to use for sharend elements transferred into the content Scene.
+ * @attr ref android.R.styleable#Window_windowSharedElementEnterTransition
+ */
+ public Transition getSharedElementEnterTransition() { return null; }
+
+ /**
+ * Returns the Transition that will be used for shared elements transferred back to a
+ * calling Activity. Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return Transition to use for sharend elements transferred into the content Scene.
+ * @attr ref android.R.styleable#Window_windowSharedElementReturnTransition
+ */
+ public Transition getSharedElementReturnTransition() { return null; }
+
+ /**
+ * Sets the Transition that will be used for shared elements after starting a new Activity
+ * before the shared elements are transferred to the called Activity. If the shared elements
+ * must animate during the exit transition, this Transition should be used. Upon completion,
+ * the shared elements may be transferred to the started Activity.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param transition The Transition to use for shared elements in the launching Window
+ * prior to transferring to the launched Activity's Window.
+ * @attr ref android.R.styleable#Window_windowSharedElementExitTransition
+ */
+ public void setSharedElementExitTransition(Transition transition) {}
+
+ /**
+ * Sets the Transition that will be used for shared elements reentering from a started
+ * Activity after it has returned the shared element to it start location. If no value
+ * is set, this will default to
+ * {@link #setSharedElementExitTransition(android.transition.Transition)}.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @param transition The Transition to use for shared elements in the launching Window
+ * after the shared element has returned to the Window.
+ * @attr ref android.R.styleable#Window_windowSharedElementReenterTransition
+ */
+ public void setSharedElementReenterTransition(Transition transition) {}
+
+ /**
+ * Returns the Transition to use for shared elements in the launching Window prior
+ * to transferring to the launched Activity's Window.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return the Transition to use for shared elements in the launching Window prior
+ * to transferring to the launched Activity's Window.
+ * @attr ref android.R.styleable#Window_windowSharedElementExitTransition
+ */
+ public Transition getSharedElementExitTransition() { return null; }
+
+ /**
+ * Returns the Transition that will be used for shared elements reentering from a started
+ * Activity after it has returned the shared element to it start location.
+ * Requires {@link #FEATURE_ACTIVITY_TRANSITIONS}.
+ *
+ * @return the Transition that will be used for shared elements reentering from a started
+ * Activity after it has returned the shared element to it start location.
+ * @attr ref android.R.styleable#Window_windowSharedElementReenterTransition
+ */
+ public Transition getSharedElementReenterTransition() { return null; }
+
+ /**
+ * Controls how the transition set in
+ * {@link #setEnterTransition(android.transition.Transition)} overlaps with the exit
+ * transition of the calling Activity. When true, the transition will start as soon as possible.
+ * When false, the transition will wait until the remote exiting transition completes before
+ * starting. The default value is true.
+ *
+ * @param allow true to start the enter transition when possible or false to
+ * wait until the exiting transition completes.
+ * @attr ref android.R.styleable#Window_windowAllowEnterTransitionOverlap
+ */
+ public void setAllowEnterTransitionOverlap(boolean allow) {}
+
+ /**
+ * Returns how the transition set in
+ * {@link #setEnterTransition(android.transition.Transition)} overlaps with the exit
+ * transition of the calling Activity. When true, the transition will start as soon as possible.
+ * When false, the transition will wait until the remote exiting transition completes before
+ * starting. The default value is true.
+ *
+ * @return true when the enter transition should start as soon as possible or false to
+ * when it should wait until the exiting transition completes.
+ * @attr ref android.R.styleable#Window_windowAllowEnterTransitionOverlap
+ */
+ public boolean getAllowEnterTransitionOverlap() { return true; }
+
+ /**
+ * Controls how the transition set in
+ * {@link #setExitTransition(android.transition.Transition)} overlaps with the exit
+ * transition of the called Activity when reentering after if finishes. When true,
+ * the transition will start as soon as possible. When false, the transition will wait
+ * until the called Activity's exiting transition completes before starting.
+ * The default value is true.
+ *
+ * @param allow true to start the transition when possible or false to wait until the
+ * called Activity's exiting transition completes.
+ * @attr ref android.R.styleable#Window_windowAllowReturnTransitionOverlap
+ */
+ public void setAllowReturnTransitionOverlap(boolean allow) {}
+
+ /**
+ * Returns how the transition set in
+ * {@link #setExitTransition(android.transition.Transition)} overlaps with the exit
+ * transition of the called Activity when reentering after if finishes. When true,
+ * the transition will start as soon as possible. When false, the transition will wait
+ * until the called Activity's exiting transition completes before starting.
+ * The default value is true.
+ *
+ * @return true when the transition should start when possible or false when it should wait
+ * until the called Activity's exiting transition completes.
+ * @attr ref android.R.styleable#Window_windowAllowReturnTransitionOverlap
+ */
+ public boolean getAllowReturnTransitionOverlap() { return true; }
+
+ /**
+ * Returns the duration, in milliseconds, of the window background fade
+ * when transitioning into or away from an Activity when called with an Activity Transition.
+ * <p>When executing the enter transition, the background starts transparent
+ * and fades in. This requires {@link #FEATURE_ACTIVITY_TRANSITIONS}. The default is
+ * 300 milliseconds.</p>
+ *
+ * @return The duration of the window background fade to opaque during enter transition.
+ * @see #getEnterTransition()
+ * @attr ref android.R.styleable#Window_windowTransitionBackgroundFadeDuration
+ */
+ public long getTransitionBackgroundFadeDuration() { return 0; }
+
+ /**
+ * Sets the duration, in milliseconds, of the window background fade
+ * when transitioning into or away from an Activity when called with an Activity Transition.
+ * <p>When executing the enter transition, the background starts transparent
+ * and fades in. This requires {@link #FEATURE_ACTIVITY_TRANSITIONS}. The default is
+ * 300 milliseconds.</p>
+ *
+ * @param fadeDurationMillis The duration of the window background fade to or from opaque
+ * during enter transition.
+ * @see #setEnterTransition(android.transition.Transition)
+ * @attr ref android.R.styleable#Window_windowTransitionBackgroundFadeDuration
+ */
+ public void setTransitionBackgroundFadeDuration(long fadeDurationMillis) { }
+
+ /**
+ * Returns <code>true</code> when shared elements should use an Overlay during
+ * shared element transitions or <code>false</code> when they should animate as
+ * part of the normal View hierarchy. The default value is true.
+ *
+ * @return <code>true</code> when shared elements should use an Overlay during
+ * shared element transitions or <code>false</code> when they should animate as
+ * part of the normal View hierarchy.
+ * @attr ref android.R.styleable#Window_windowSharedElementsUseOverlay
+ */
+ public boolean getSharedElementsUseOverlay() { return true; }
+
+ /**
+ * Sets whether or not shared elements should use an Overlay during shared element transitions.
+ * The default value is true.
+ *
+ * @param sharedElementsUseOverlay <code>true</code> indicates that shared elements should
+ * be transitioned with an Overlay or <code>false</code>
+ * to transition within the normal View hierarchy.
+ * @attr ref android.R.styleable#Window_windowSharedElementsUseOverlay
+ */
+ public void setSharedElementsUseOverlay(boolean sharedElementsUseOverlay) { }
+
+ /**
+ * @return the color of the status bar.
+ */
+ @ColorInt
+ public abstract int getStatusBarColor();
+
+ /**
+ * Sets the color of the status bar to {@code color}.
+ *
+ * For this to take effect,
+ * the window must be drawing the system bar backgrounds with
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set.
+ *
+ * If {@code color} is not opaque, consider setting
+ * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
+ * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
+ * <p>
+ * The transitionName for the view background will be "android:status:background".
+ * </p>
+ */
+ public abstract void setStatusBarColor(@ColorInt int color);
+
+ /**
+ * @return the color of the navigation bar.
+ */
+ @ColorInt
+ public abstract int getNavigationBarColor();
+
+ /**
+ * Sets the color of the navigation bar to {@param color}.
+ *
+ * For this to take effect,
+ * the window must be drawing the system bar backgrounds with
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
+ *
+ * If {@param color} is not opaque, consider setting
+ * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
+ * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
+ * <p>
+ * The transitionName for the view background will be "android:navigation:background".
+ * </p>
+ * @attr ref android.R.styleable#Window_navigationBarColor
+ */
+ public abstract void setNavigationBarColor(@ColorInt int color);
+
+ /**
+ * Shows a thin line of the specified color between the navigation bar and the app
+ * content.
+ * <p>
+ * For this to take effect,
+ * the window must be drawing the system bar backgrounds with
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
+ *
+ * @param dividerColor The color of the thin line.
+ * @attr ref android.R.styleable#Window_navigationBarDividerColor
+ */
+ public void setNavigationBarDividerColor(@ColorInt int dividerColor) {
+ }
+
+ /**
+ * Retrieves the color of the navigation bar divider.
+ *
+ * @return The color of the navigation bar divider color.
+ * @see #setNavigationBarColor(int)
+ * @attr ref android.R.styleable#Window_navigationBarDividerColor
+ */
+ public @ColorInt int getNavigationBarDividerColor() {
+ return 0;
+ }
+
+ /**
+ * Sets whether the system should ensure that the status bar has enough
+ * contrast when a fully transparent background is requested.
+ *
+ * <p>If set to this value, the system will determine whether a scrim is necessary
+ * to ensure that the status bar has enough contrast with the contents of
+ * this app, and set an appropriate effective bar background color accordingly.
+ *
+ * <p>When the status bar color has a non-zero alpha value, the value of this
+ * property has no effect.
+ *
+ * @see android.R.attr#enforceStatusBarContrast
+ * @see #isStatusBarContrastEnforced
+ * @see #setStatusBarColor
+ */
+ public void setStatusBarContrastEnforced(boolean ensureContrast) {
+ }
+
+ /**
+ * Returns whether the system is ensuring that the status bar has enough contrast when a
+ * fully transparent background is requested.
+ *
+ * <p>When the status bar color has a non-zero alpha value, the value of this
+ * property has no effect.
+ *
+ * @return true, if the system is ensuring contrast, false otherwise.
+ * @see android.R.attr#enforceStatusBarContrast
+ * @see #setStatusBarContrastEnforced
+ * @see #setStatusBarColor
+ */
+ public boolean isStatusBarContrastEnforced() {
+ return false;
+ }
+
+ /**
+ * Sets whether the system should ensure that the navigation bar has enough
+ * contrast when a fully transparent background is requested.
+ *
+ * <p>If set to this value, the system will determine whether a scrim is necessary
+ * to ensure that the navigation bar has enough contrast with the contents of
+ * this app, and set an appropriate effective bar background color accordingly.
+ *
+ * <p>When the navigation bar color has a non-zero alpha value, the value of this
+ * property has no effect.
+ *
+ * @see android.R.attr#enforceNavigationBarContrast
+ * @see #isNavigationBarContrastEnforced
+ * @see #setNavigationBarColor
+ */
+ public void setNavigationBarContrastEnforced(boolean enforceContrast) {
+ }
+
+ /**
+ * Returns whether the system is ensuring that the navigation bar has enough contrast when a
+ * fully transparent background is requested.
+ *
+ * <p>When the navigation bar color has a non-zero alpha value, the value of this
+ * property has no effect.
+ *
+ * @return true, if the system is ensuring contrast, false otherwise.
+ * @see android.R.attr#enforceNavigationBarContrast
+ * @see #setNavigationBarContrastEnforced
+ * @see #setNavigationBarColor
+ */
+ public boolean isNavigationBarContrastEnforced() {
+ return false;
+ }
+
+ /**
+ * Sets a list of areas within this window's coordinate space where the system should not
+ * intercept touch or other pointing device gestures.
+ *
+ * <p>This method should be used by apps that make use of
+ * {@link #takeSurface(SurfaceHolder.Callback2)} and do not have a view hierarchy available.
+ * Apps that do have a view hierarchy should use
+ * {@link View#setSystemGestureExclusionRects(List)} instead. This method does not modify or
+ * replace the gesture exclusion rects populated by individual views in this window's view
+ * hierarchy using {@link View#setSystemGestureExclusionRects(List)}.</p>
+ *
+ * <p>Use this to tell the system which specific sub-areas of a view need to receive gesture
+ * input in order to function correctly in the presence of global system gestures that may
+ * conflict. For example, if the system wishes to capture swipe-in-from-screen-edge gestures
+ * to provide system-level navigation functionality, a view such as a navigation drawer
+ * container can mark the left (or starting) edge of itself as requiring gesture capture
+ * priority using this API. The system may then choose to relax its own gesture recognition
+ * to allow the app to consume the user's gesture. It is not necessary for an app to register
+ * exclusion rects for broadly spanning regions such as the entirety of a
+ * <code>ScrollView</code> or for simple press and release click targets such as
+ * <code>Button</code>. Mark an exclusion rect when interacting with a view requires
+ * a precision touch gesture in a small area in either the X or Y dimension, such as
+ * an edge swipe or dragging a <code>SeekBar</code> thumb.</p>
+ *
+ * <p>Do not modify the provided list after this method is called.</p>
+ *
+ * @param rects A list of precision gesture regions that this window needs to function correctly
+ */
+ @SuppressWarnings("unused")
+ public void setSystemGestureExclusionRects(@NonNull List<Rect> rects) {
+ throw new UnsupportedOperationException("window does not support gesture exclusion rects");
+ }
+
+ /**
+ * Retrieve the list of areas within this window's coordinate space where the system should not
+ * intercept touch or other pointing device gestures. This is the list as set by
+ * {@link #setSystemGestureExclusionRects(List)} or an empty list if
+ * {@link #setSystemGestureExclusionRects(List)} has not been called. It does not include
+ * exclusion rects set by this window's view hierarchy.
+ *
+ * @return a list of system gesture exclusion rects specific to this window
+ */
+ @NonNull
+ public List<Rect> getSystemGestureExclusionRects() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * System request to begin scroll capture.
+ *
+ * @param listener to receive the response
+ * @hide
+ */
+ public void requestScrollCapture(IScrollCaptureResponseListener listener) {
+ }
+
+ /**
+ * Used to provide scroll capture support for an arbitrary window. This registeres the given
+ * callback with the root view of the window.
+ *
+ * @param callback the callback to add
+ */
+ public void registerScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+ }
+
+ /**
+ * Unregisters a {@link ScrollCaptureCallback} previously registered with this window.
+ *
+ * @param callback the callback to remove
+ */
+ public void unregisterScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+ }
+
+ /** @hide */
+ public void setTheme(int resId) {
+ }
+
+ /**
+ * Whether the caption should be displayed directly on the content rather than push the content
+ * down. This affects only freeform windows since they display the caption.
+ * @hide
+ */
+ public void setOverlayWithDecorCaptionEnabled(boolean enabled) {
+ mOverlayWithDecorCaptionEnabled = enabled;
+ }
+
+ /** @hide */
+ public boolean isOverlayWithDecorCaptionEnabled() {
+ return mOverlayWithDecorCaptionEnabled;
+ }
+
+ /** @hide */
+ public void notifyRestrictedCaptionAreaCallback(int left, int top, int right, int bottom) {
+ if (mOnRestrictedCaptionAreaChangedListener != null) {
+ mRestrictedCaptionAreaRect.set(left, top, right, bottom);
+ mOnRestrictedCaptionAreaChangedListener.onRestrictedCaptionAreaChanged(
+ mRestrictedCaptionAreaRect);
+ }
+ }
+
+ /**
+ * Set what color should the caption controls be. By default the system will try to determine
+ * the color from the theme. You can overwrite this by using {@link #DECOR_CAPTION_SHADE_DARK},
+ * {@link #DECOR_CAPTION_SHADE_LIGHT}, or {@link #DECOR_CAPTION_SHADE_AUTO}.
+ * @see #DECOR_CAPTION_SHADE_DARK
+ * @see #DECOR_CAPTION_SHADE_LIGHT
+ * @see #DECOR_CAPTION_SHADE_AUTO
+ */
+ public abstract void setDecorCaptionShade(int decorCaptionShade);
+
+ /**
+ * Set the drawable that is drawn underneath the caption during the resizing.
+ *
+ * During the resizing the caption might not be drawn fast enough to match the new dimensions.
+ * There is a second caption drawn underneath it that will be fast enough. By default the
+ * caption is constructed from the theme. You can provide a drawable, that will be drawn instead
+ * to better match your application.
+ */
+ public abstract void setResizingCaptionDrawable(Drawable drawable);
+
+ /**
+ * Called when the activity changes from fullscreen mode to multi-window mode and visa-versa.
+ * @hide
+ */
+ @SuppressWarnings("HiddenAbstractMethod")
+ public abstract void onMultiWindowModeChanged();
+
+ /**
+ * Called when the activity changes to/from picture-in-picture mode.
+ * @hide
+ */
+ @SuppressWarnings("HiddenAbstractMethod")
+ public abstract void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
+
+ /**
+ * Called when the activity just relaunched.
+ * @hide
+ */
+ @SuppressWarnings("HiddenAbstractMethod")
+ public abstract void reportActivityRelaunched();
+
+ /**
+ * @return The {@link WindowInsetsController} associated with this window
+ * @see View#getWindowInsetsController()
+ */
+ public @Nullable WindowInsetsController getInsetsController() {
+ return null;
+ }
+
+ /**
+ * This will be null before a content view is added, e.g. via
+ * {@link #setContentView} or {@link #addContentView}. See
+ * {@link android.view.View#getRootSurfaceControl}.
+ *
+ * @return The {@link android.view.AttachedSurfaceControl} interface for this Window
+ */
+ public @Nullable AttachedSurfaceControl getRootSurfaceControl() {
+ return null;
+ }
+}
diff --git a/android/view/WindowAnimationFrameStats.java b/android/view/WindowAnimationFrameStats.java
new file mode 100644
index 0000000..251ae9b
--- /dev/null
+++ b/android/view/WindowAnimationFrameStats.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class contains window animation frame statistics. For example, a window
+ * animation is usually performed when the application is transitioning from one
+ * activity to another. The frame statistics are a snapshot for the time interval
+ * from {@link #getStartTimeNano()} to {@link #getEndTimeNano()}.
+ * <p>
+ * The key idea is that in order to provide a smooth user experience the system should
+ * run window animations at a specific time interval obtained by calling {@link
+ * #getRefreshPeriodNano()}. If the system does not render a frame every refresh
+ * period the user will see irregular window transitions. The time when the frame was
+ * actually presented on the display by calling {@link #getFramePresentedTimeNano(int)}.
+ */
+public final class WindowAnimationFrameStats extends FrameStats implements Parcelable {
+ /**
+ * @hide
+ */
+ public WindowAnimationFrameStats() {
+ /* do nothing */
+ }
+
+ /**
+ * Initializes this isntance.
+ *
+ * @param refreshPeriodNano The display refresh period.
+ * @param framesPresentedTimeNano The presented frame times.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void init(long refreshPeriodNano, long[] framesPresentedTimeNano) {
+ mRefreshPeriodNano = refreshPeriodNano;
+ mFramesPresentedTimeNano = framesPresentedTimeNano;
+ }
+
+ private WindowAnimationFrameStats(Parcel parcel) {
+ mRefreshPeriodNano = parcel.readLong();
+ mFramesPresentedTimeNano = parcel.createLongArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeLong(mRefreshPeriodNano);
+ parcel.writeLongArray(mFramesPresentedTimeNano);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("WindowAnimationFrameStats[");
+ builder.append("frameCount:" + getFrameCount());
+ builder.append(", fromTimeNano:" + getStartTimeNano());
+ builder.append(", toTimeNano:" + getEndTimeNano());
+ builder.append(']');
+ return builder.toString();
+ }
+
+ public static final @android.annotation.NonNull Creator<WindowAnimationFrameStats> CREATOR =
+ new Creator<WindowAnimationFrameStats>() {
+ @Override
+ public WindowAnimationFrameStats createFromParcel(Parcel parcel) {
+ return new WindowAnimationFrameStats(parcel);
+ }
+
+ @Override
+ public WindowAnimationFrameStats[] newArray(int size) {
+ return new WindowAnimationFrameStats[size];
+ }
+ };
+}
diff --git a/android/view/WindowCallback.java b/android/view/WindowCallback.java
new file mode 100644
index 0000000..1ea8a9f
--- /dev/null
+++ b/android/view/WindowCallback.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.view.ActionMode.Callback;
+import android.view.WindowManager.LayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.List;
+
+/**
+ * An empty implementation of {@link Window.Callback} that always returns null/false.
+ */
+public class WindowCallback implements Window.Callback {
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ return false;
+ }
+
+ @Override
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ @Override
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onWindowAttributesChanged(LayoutParams attrs) {
+
+ }
+
+ @Override
+ public void onContentChanged() {
+
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+
+ }
+
+ @Override
+ public void onPanelClosed(int featureId, Menu menu) {
+
+ }
+
+ @Override
+ public boolean onSearchRequested(SearchEvent searchEvent) {
+ return onSearchRequested();
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ return false;
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(Callback callback) {
+ return null;
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(Callback callback, int type) {
+ return null;
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+
+ }
+}
diff --git a/android/view/WindowCallbackWrapper.java b/android/view/WindowCallbackWrapper.java
new file mode 100644
index 0000000..02c8945
--- /dev/null
+++ b/android/view/WindowCallbackWrapper.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.view;
+
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.List;
+
+/**
+ * A simple decorator stub for Window.Callback that passes through any calls
+ * to the wrapped instance as a base implementation. Call super.foo() to call into
+ * the wrapped callback for any subclasses.
+ *
+ * @hide for internal use
+ */
+public class WindowCallbackWrapper implements Window.Callback {
+ private Window.Callback mWrapped;
+
+ public WindowCallbackWrapper(Window.Callback wrapped) {
+ if (wrapped == null) {
+ throw new IllegalArgumentException("Window callback may not be null");
+ }
+ mWrapped = wrapped;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mWrapped.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return mWrapped.dispatchKeyShortcutEvent(event);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ return mWrapped.dispatchTouchEvent(event);
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ return mWrapped.dispatchTrackballEvent(event);
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ return mWrapped.dispatchGenericMotionEvent(event);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ return mWrapped.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public View onCreatePanelView(int featureId) {
+ return mWrapped.onCreatePanelView(featureId);
+ }
+
+ @Override
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ return mWrapped.onCreatePanelMenu(featureId, menu);
+ }
+
+ @Override
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ return mWrapped.onPreparePanel(featureId, view, menu);
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return mWrapped.onMenuOpened(featureId, menu);
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ return mWrapped.onMenuItemSelected(featureId, item);
+ }
+
+ @Override
+ public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) {
+ mWrapped.onWindowAttributesChanged(attrs);
+ }
+
+ @Override
+ public void onContentChanged() {
+ mWrapped.onContentChanged();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ mWrapped.onWindowFocusChanged(hasFocus);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ mWrapped.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mWrapped.onDetachedFromWindow();
+ }
+
+ @Override
+ public void onPanelClosed(int featureId, Menu menu) {
+ mWrapped.onPanelClosed(featureId, menu);
+ }
+
+ @Override
+ public boolean onSearchRequested(SearchEvent searchEvent) {
+ return mWrapped.onSearchRequested(searchEvent);
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ return mWrapped.onSearchRequested();
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
+ return mWrapped.onWindowStartingActionMode(callback);
+ }
+
+ @Override
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) {
+ return mWrapped.onWindowStartingActionMode(callback, type);
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ mWrapped.onActionModeStarted(mode);
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ mWrapped.onActionModeFinished(mode);
+ }
+
+ @Override
+ public void onProvideKeyboardShortcuts(
+ List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
+ mWrapped.onProvideKeyboardShortcuts(data, menu, deviceId);
+ }
+
+ @Override
+ public void onPointerCaptureChanged(boolean hasCapture) {
+ mWrapped.onPointerCaptureChanged(hasCapture);
+ }
+}
+
diff --git a/android/view/WindowCallbacks.java b/android/view/WindowCallbacks.java
new file mode 100644
index 0000000..a997302
--- /dev/null
+++ b/android/view/WindowCallbacks.java
@@ -0,0 +1,87 @@
+/*
+ * 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.view;
+
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+
+/**
+ * These callbacks are used to communicate window configuration changes while the user is performing
+ * window changes.
+ * Note: Note that at the time of onWindowDragResizeStart the content size isn't known. A consumer
+ * should therfore not draw anything before the additional onContentDraw call has arrived.
+ * @hide
+ */
+public interface WindowCallbacks {
+
+ public static final int RESIZE_MODE_INVALID = -1;
+ public static final int RESIZE_MODE_FREEFORM = 0;
+ public static final int RESIZE_MODE_DOCKED_DIVIDER = 1;
+
+ /**
+ * Called by the system when the window got changed by the user, before the layouter got called.
+ * It also gets called when the insets changed, or when the window switched between a fullscreen
+ * layout or a non-fullscreen layout. It can be used to perform a "quick and dirty" resize which
+ * should never take more then 4ms to complete.
+ *
+ * <p>At the time the layouting has not happened yet.
+ *
+ * @param newBounds The new window frame bounds.
+ * @param fullscreen Whether the window is currently drawing in fullscreen.
+ * @param systemInsets The current visible system insets for the window.
+ * @param stableInsets The stable insets for the window.
+ */
+ void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+ Rect stableInsets);
+
+ /**
+ * Called when a drag resize starts.
+ *
+ * @param initialBounds The initial bounds where the window will be.
+ * @param fullscreen Whether the window is currently drawing in fullscreen.
+ * @param systemInsets The current visible system insets for the window.
+ * @param stableInsets The stable insets for the window.
+ */
+ void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+ Rect stableInsets, int resizeMode);
+
+ /**
+ * Called when a drag resize ends.
+ */
+ void onWindowDragResizeEnd();
+
+ /**
+ * The content will now be drawn to these bounds. Returns true if
+ * a draw should be requested after the next content draw.
+ */
+ boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY);
+
+ /**
+ * Called to request the window to draw one frame.
+ * @param reportNextDraw Whether it should report when the requested draw finishes.
+ */
+ void onRequestDraw(boolean reportNextDraw);
+
+ /**
+ * Called after all the content has drawn and the callback now has the ability to draw something
+ * on top of everything. Call {@link ViewRootImpl#requestInvalidateRootRenderNode} when this
+ * content needs to be redrawn.
+ *
+ * @param canvas The canvas to draw on.
+ */
+ void onPostDraw(RecordingCanvas canvas);
+}
diff --git a/android/view/WindowContentFrameStats.java b/android/view/WindowContentFrameStats.java
new file mode 100644
index 0000000..c788346
--- /dev/null
+++ b/android/view/WindowContentFrameStats.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class contains window content frame statistics. For example, a window content
+ * is rendred in frames when a view is scrolled. The frame statistics are a snapshot
+ * for the time interval from {@link #getStartTimeNano()} to {@link #getEndTimeNano()}.
+ * <p>
+ * The key idea is that in order to provide a smooth user experience an application
+ * has to draw a frame at a specific time interval obtained by calling {@link
+ * #getRefreshPeriodNano()}. If the application does not render a frame every refresh
+ * period the user will see irregular UI transitions.
+ * </p>
+ * <p>
+ * An application posts a frame for presentation by synchronously rendering its contents
+ * in a buffer which is then posted or posting a buffer to which the application is
+ * asychronously rendering the content via GL. After the frame is posted and rendered
+ * (potentially asynchronosly) it is presented to the user. The time a frame was posted
+ * can be obtained via {@link #getFramePostedTimeNano(int)}, the time a frame content
+ * was rendered and ready for dsiplay (GL case) via {@link #getFrameReadyTimeNano(int)},
+ * and the time a frame was presented on the screen via {@link #getFramePresentedTimeNano(int)}.
+ * </p>
+ */
+public final class WindowContentFrameStats extends FrameStats implements Parcelable {
+ private long[] mFramesPostedTimeNano;
+ private long[] mFramesReadyTimeNano;
+
+ /**
+ * @hide
+ */
+ public WindowContentFrameStats() {
+ /* do nothing */
+ }
+
+ /**
+ * Initializes this isntance.
+ *
+ * @param refreshPeriodNano The display refresh period.
+ * @param framesPostedTimeNano The times in milliseconds for when the frame contents were posted.
+ * @param framesPresentedTimeNano The times in milliseconds for when the frame contents were presented.
+ * @param framesReadyTimeNano The times in milliseconds for when the frame contents were ready to be presented.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void init(long refreshPeriodNano, long[] framesPostedTimeNano,
+ long[] framesPresentedTimeNano, long[] framesReadyTimeNano) {
+ mRefreshPeriodNano = refreshPeriodNano;
+ mFramesPostedTimeNano = framesPostedTimeNano;
+ mFramesPresentedTimeNano = framesPresentedTimeNano;
+ mFramesReadyTimeNano = framesReadyTimeNano;
+ }
+
+ private WindowContentFrameStats(Parcel parcel) {
+ mRefreshPeriodNano = parcel.readLong();
+ mFramesPostedTimeNano = parcel.createLongArray();
+ mFramesPresentedTimeNano = parcel.createLongArray();
+ mFramesReadyTimeNano = parcel.createLongArray();
+ }
+
+ /**
+ * Get the time a frame at a given index was posted by the producer (e.g. the application).
+ * It is either explicitly set or defaulted to the time when the render buffer was posted.
+ * <p>
+ * <strong>Note:</strong> A frame can be posted and still it contents being rendered
+ * asynchronously in GL. To get the time the frame content was completely rendered and
+ * ready to display call {@link #getFrameReadyTimeNano(int)}.
+ * </p>
+ *
+ * @param index The frame index.
+ * @return The posted time in nanoseconds.
+ */
+ public long getFramePostedTimeNano(int index) {
+ if (mFramesPostedTimeNano == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mFramesPostedTimeNano[index];
+ }
+
+ /**
+ * Get the time a frame at a given index was ready for presentation.
+ * <p>
+ * <strong>Note:</strong> A frame can be posted and still it contents being rendered
+ * asynchronously in GL. In such a case this is the time when the frame contents were
+ * completely rendered.
+ * </p>
+ *
+ * @param index The frame index.
+ * @return The ready time in nanoseconds or {@link #UNDEFINED_TIME_NANO}
+ * if the frame is not ready yet.
+ */
+ public long getFrameReadyTimeNano(int index) {
+ if (mFramesReadyTimeNano == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mFramesReadyTimeNano[index];
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeLong(mRefreshPeriodNano);
+ parcel.writeLongArray(mFramesPostedTimeNano);
+ parcel.writeLongArray(mFramesPresentedTimeNano);
+ parcel.writeLongArray(mFramesReadyTimeNano);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("WindowContentFrameStats[");
+ builder.append("frameCount:" + getFrameCount());
+ builder.append(", fromTimeNano:" + getStartTimeNano());
+ builder.append(", toTimeNano:" + getEndTimeNano());
+ builder.append(']');
+ return builder.toString();
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<WindowContentFrameStats> CREATOR =
+ new Creator<WindowContentFrameStats>() {
+ @Override
+ public WindowContentFrameStats createFromParcel(Parcel parcel) {
+ return new WindowContentFrameStats(parcel);
+ }
+
+ @Override
+ public WindowContentFrameStats[] newArray(int size) {
+ return new WindowContentFrameStats[size];
+ }
+ };
+}
diff --git a/android/view/WindowId.java b/android/view/WindowId.java
new file mode 100644
index 0000000..26d3405
--- /dev/null
+++ b/android/view/WindowId.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import java.util.HashMap;
+
+/**
+ * Safe identifier for a window. This currently allows you to retrieve and observe
+ * the input focus state of the window. Most applications will
+ * not use this, instead relying on the simpler (and more efficient) methods available
+ * on {@link View}. This classes is useful when window input interactions need to be
+ * done across processes: the class itself is a Parcelable that can be passed to other
+ * processes for them to interact with your window, and it provides a limited safe API
+ * that doesn't allow the other process to negatively harm your window.
+ */
+public class WindowId implements Parcelable {
+ @NonNull
+ private final IWindowId mToken;
+
+ /**
+ * Subclass for observing changes to the focus state of an {@link WindowId}.
+ * You should use the same instance of this class for observing multiple
+ * {@link WindowId} objects, since this class is fairly heavy-weight -- the
+ * base class includes all of the mechanisms for connecting to and receiving updates
+ * from the window.
+ */
+ public static abstract class FocusObserver {
+ final IWindowFocusObserver.Stub mIObserver = new IWindowFocusObserver.Stub() {
+
+ @Override
+ public void focusGained(IBinder inputToken) {
+ WindowId token;
+ synchronized (mRegistrations) {
+ token = mRegistrations.get(inputToken);
+ }
+ if (mHandler != null) {
+ mHandler.sendMessage(mHandler.obtainMessage(1, token));
+ } else {
+ onFocusGained(token);
+ }
+ }
+
+ @Override
+ public void focusLost(IBinder inputToken) {
+ WindowId token;
+ synchronized (mRegistrations) {
+ token = mRegistrations.get(inputToken);
+ }
+ if (mHandler != null) {
+ mHandler.sendMessage(mHandler.obtainMessage(2, token));
+ } else {
+ onFocusLost(token);
+ }
+ }
+ };
+
+ final HashMap<IBinder, WindowId> mRegistrations = new HashMap<>();
+
+ class H extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case 1:
+ onFocusGained((WindowId)msg.obj);
+ break;
+ case 2:
+ onFocusLost((WindowId)msg.obj);
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+ }
+
+ final Handler mHandler;
+
+ /**
+ * Construct a new observer. This observer will be configured so that all
+ * of its callbacks are dispatched on the current calling thread.
+ */
+ public FocusObserver() {
+ mHandler = new H();
+ }
+
+ /**
+ * Called when one of the monitored windows gains input focus.
+ */
+ public abstract void onFocusGained(WindowId token);
+
+ /**
+ * Called when one of the monitored windows loses input focus.
+ */
+ public abstract void onFocusLost(WindowId token);
+ }
+
+ /**
+ * Retrieve the current focus state of the associated window.
+ */
+ public boolean isFocused() {
+ try {
+ return mToken.isFocused();
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Start monitoring for changes in the focus state of the window.
+ */
+ public void registerFocusObserver(FocusObserver observer) {
+ synchronized (observer.mRegistrations) {
+ if (observer.mRegistrations.containsKey(mToken.asBinder())) {
+ throw new IllegalStateException(
+ "Focus observer already registered with input token");
+ }
+ observer.mRegistrations.put(mToken.asBinder(), this);
+ try {
+ mToken.registerFocusObserver(observer.mIObserver);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Stop monitoring changes in the focus state of the window.
+ */
+ public void unregisterFocusObserver(FocusObserver observer) {
+ synchronized (observer.mRegistrations) {
+ if (observer.mRegistrations.remove(mToken.asBinder()) == null) {
+ throw new IllegalStateException("Focus observer not registered with input token");
+ }
+ try {
+ mToken.unregisterFocusObserver(observer.mIObserver);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Comparison operator on two IntentSender objects, such that true
+ * is returned then they both represent the same operation from the
+ * same package.
+ */
+ @Override
+ public boolean equals(@Nullable Object otherObj) {
+ if (otherObj instanceof WindowId) {
+ return mToken.asBinder().equals(((WindowId) otherObj).mToken.asBinder());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mToken.asBinder().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("IntentSender{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(": ");
+ sb.append(mToken.asBinder());
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStrongBinder(mToken.asBinder());
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<WindowId> CREATOR = new Parcelable.Creator<WindowId>() {
+ @Override
+ public WindowId createFromParcel(Parcel in) {
+ IBinder target = in.readStrongBinder();
+ return target != null ? new WindowId(target) : null;
+ }
+
+ @Override
+ public WindowId[] newArray(int size) {
+ return new WindowId[size];
+ }
+ };
+
+ /** @hide */
+ @NonNull
+ public IWindowId getTarget() {
+ return mToken;
+ }
+
+ /** @hide */
+ public WindowId(@NonNull IWindowId target) {
+ mToken = target;
+ }
+
+ /** @hide */
+ public WindowId(@NonNull IBinder target) {
+ mToken = IWindowId.Stub.asInterface(target);
+ }
+}
diff --git a/android/view/WindowInfo.java b/android/view/WindowInfo.java
new file mode 100644
index 0000000..57dfc62
--- /dev/null
+++ b/android/view/WindowInfo.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Region;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pools;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents information about a window from the
+ * window manager to another part of the system.
+ *
+ * @hide
+ */
+public class WindowInfo implements Parcelable {
+ private static final int MAX_POOL_SIZE = 10;
+
+ private static final Pools.SynchronizedPool<WindowInfo> sPool =
+ new Pools.SynchronizedPool<WindowInfo>(MAX_POOL_SIZE);
+
+ public int type;
+ public int layer;
+ public IBinder token;
+ public IBinder parentToken;
+ public IBinder activityToken;
+ public boolean focused;
+ public Region regionInScreen = new Region();
+ public List<IBinder> childTokens;
+ public CharSequence title;
+ public long accessibilityIdOfAnchor = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+ public boolean inPictureInPicture;
+ public boolean hasFlagWatchOutsideTouch;
+ public int displayId = Display.INVALID_DISPLAY;
+
+ private WindowInfo() {
+ /* do nothing - hide constructor */
+ }
+
+ public static WindowInfo obtain() {
+ WindowInfo window = sPool.acquire();
+ if (window == null) {
+ window = new WindowInfo();
+ }
+ return window;
+ }
+
+ public static WindowInfo obtain(WindowInfo other) {
+ WindowInfo window = obtain();
+ window.displayId = other.displayId;
+ window.type = other.type;
+ window.layer = other.layer;
+ window.token = other.token;
+ window.parentToken = other.parentToken;
+ window.activityToken = other.activityToken;
+ window.focused = other.focused;
+ window.regionInScreen.set(other.regionInScreen);
+ window.title = other.title;
+ window.accessibilityIdOfAnchor = other.accessibilityIdOfAnchor;
+ window.inPictureInPicture = other.inPictureInPicture;
+ window.hasFlagWatchOutsideTouch = other.hasFlagWatchOutsideTouch;
+
+ if (other.childTokens != null && !other.childTokens.isEmpty()) {
+ if (window.childTokens == null) {
+ window.childTokens = new ArrayList<IBinder>(other.childTokens);
+ } else {
+ window.childTokens.addAll(other.childTokens);
+ }
+ }
+
+ return window;
+ }
+
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(displayId);
+ parcel.writeInt(type);
+ parcel.writeInt(layer);
+ parcel.writeStrongBinder(token);
+ parcel.writeStrongBinder(parentToken);
+ parcel.writeStrongBinder(activityToken);
+ parcel.writeInt(focused ? 1 : 0);
+ regionInScreen.writeToParcel(parcel, flags);
+ parcel.writeCharSequence(title);
+ parcel.writeLong(accessibilityIdOfAnchor);
+ parcel.writeInt(inPictureInPicture ? 1 : 0);
+ parcel.writeInt(hasFlagWatchOutsideTouch ? 1 : 0);
+
+ if (childTokens != null && !childTokens.isEmpty()) {
+ parcel.writeInt(1);
+ parcel.writeBinderList(childTokens);
+ } else {
+ parcel.writeInt(0);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("WindowInfo[");
+ builder.append("title=").append(title);
+ builder.append(", displayId=").append(displayId);
+ builder.append(", type=").append(type);
+ builder.append(", layer=").append(layer);
+ builder.append(", token=").append(token);
+ builder.append(", region=").append(regionInScreen);
+ builder.append(", bounds=").append(regionInScreen.getBounds());
+ builder.append(", parent=").append(parentToken);
+ builder.append(", focused=").append(focused);
+ builder.append(", children=").append(childTokens);
+ builder.append(", accessibility anchor=").append(accessibilityIdOfAnchor);
+ builder.append(", pictureInPicture=").append(inPictureInPicture);
+ builder.append(", watchOutsideTouch=").append(hasFlagWatchOutsideTouch);
+ builder.append(']');
+ return builder.toString();
+ }
+
+ private void initFromParcel(Parcel parcel) {
+ displayId = parcel.readInt();
+ type = parcel.readInt();
+ layer = parcel.readInt();
+ token = parcel.readStrongBinder();
+ parentToken = parcel.readStrongBinder();
+ activityToken = parcel.readStrongBinder();
+ focused = (parcel.readInt() == 1);
+ regionInScreen = Region.CREATOR.createFromParcel(parcel);
+ title = parcel.readCharSequence();
+ accessibilityIdOfAnchor = parcel.readLong();
+ inPictureInPicture = (parcel.readInt() == 1);
+ hasFlagWatchOutsideTouch = (parcel.readInt() == 1);
+
+ final boolean hasChildren = (parcel.readInt() == 1);
+ if (hasChildren) {
+ if (childTokens == null) {
+ childTokens = new ArrayList<IBinder>();
+ }
+ parcel.readBinderList(childTokens);
+ }
+ }
+
+ private void clear() {
+ displayId = Display.INVALID_DISPLAY;
+ type = 0;
+ layer = 0;
+ token = null;
+ parentToken = null;
+ activityToken = null;
+ focused = false;
+ regionInScreen.setEmpty();
+ if (childTokens != null) {
+ childTokens.clear();
+ }
+ inPictureInPicture = false;
+ hasFlagWatchOutsideTouch = false;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<WindowInfo> CREATOR =
+ new Creator<WindowInfo>() {
+ @Override
+ public WindowInfo createFromParcel(Parcel parcel) {
+ WindowInfo window = obtain();
+ window.initFromParcel(parcel);
+ return window;
+ }
+
+ @Override
+ public WindowInfo[] newArray(int size) {
+ return new WindowInfo[size];
+ }
+ };
+}
diff --git a/android/view/WindowInsets.java b/android/view/WindowInsets.java
new file mode 100644
index 0000000..0f1a9d9
--- /dev/null
+++ b/android/view/WindowInsets.java
@@ -0,0 +1,1624 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.view;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.WindowInsets.Type.DISPLAY_CUTOUT;
+import static android.view.WindowInsets.Type.FIRST;
+import static android.view.WindowInsets.Type.IME;
+import static android.view.WindowInsets.Type.LAST;
+import static android.view.WindowInsets.Type.MANDATORY_SYSTEM_GESTURES;
+import static android.view.WindowInsets.Type.NAVIGATION_BARS;
+import static android.view.WindowInsets.Type.SIZE;
+import static android.view.WindowInsets.Type.STATUS_BARS;
+import static android.view.WindowInsets.Type.SYSTEM_GESTURES;
+import static android.view.WindowInsets.Type.TAPPABLE_ELEMENT;
+import static android.view.WindowInsets.Type.all;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.indexOf;
+import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Intent;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethod;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Describes a set of insets for window content.
+ *
+ * <p>WindowInsets are immutable and may be expanded to include more inset types in the future.
+ * To adjust insets, use one of the supplied clone methods to obtain a new WindowInsets instance
+ * with the adjusted properties.</p>
+ *
+ * <p>Note: Before {@link android.os.Build.VERSION_CODES#P P}, WindowInsets instances were only
+ * immutable during a single layout pass (i.e. would return the same values between
+ * {@link View#onApplyWindowInsets} and {@link View#onLayout}, but could return other values
+ * otherwise). Starting with {@link android.os.Build.VERSION_CODES#P P}, WindowInsets are
+ * always immutable and implement equality.
+ *
+ * @see View.OnApplyWindowInsetsListener
+ * @see View#onApplyWindowInsets(WindowInsets)
+ */
+public final class WindowInsets {
+
+ private final Insets[] mTypeInsetsMap;
+ private final Insets[] mTypeMaxInsetsMap;
+ private final boolean[] mTypeVisibilityMap;
+
+ @Nullable private Rect mTempRect;
+ private final boolean mIsRound;
+ @Nullable private final DisplayCutout mDisplayCutout;
+ @Nullable private final RoundedCorners mRoundedCorners;
+ @Nullable private final PrivacyIndicatorBounds mPrivacyIndicatorBounds;
+
+ /**
+ * In multi-window we force show the navigation bar. Because we don't want that the surface size
+ * changes in this mode, we instead have a flag whether the navigation bar size should always
+ * be consumed, so the app is treated like there is no virtual navigation bar at all.
+ */
+ private final boolean mAlwaysConsumeSystemBars;
+
+ private final boolean mSystemWindowInsetsConsumed;
+ private final boolean mStableInsetsConsumed;
+ private final boolean mDisplayCutoutConsumed;
+
+ private final int mCompatInsetsTypes;
+ private final boolean mCompatIgnoreVisibility;
+
+ /**
+ * A {@link WindowInsets} instance for which {@link #isConsumed()} returns {@code true}.
+ * <p>
+ * This can be used during insets dispatch in the view hierarchy by returning this value from
+ * {@link View#onApplyWindowInsets(WindowInsets)} or
+ * {@link OnApplyWindowInsetsListener#onApplyWindowInsets(View, WindowInsets)} to stop dispatch
+ * the insets to its children to avoid traversing the entire view hierarchy.
+ * <p>
+ * The application should return this instance once it has taken care of all insets on a certain
+ * level in the view hierarchy, and doesn't need to dispatch to its children anymore for better
+ * performance.
+ *
+ * @see #isConsumed()
+ */
+ public static final @NonNull WindowInsets CONSUMED;
+
+ static {
+ CONSUMED = new WindowInsets((Rect) null, null, false, false, null);
+ }
+
+ /**
+ * Construct a new WindowInsets from individual insets.
+ *
+ * A {@code null} inset indicates that the respective inset is consumed.
+ *
+ * @hide
+ * @deprecated Use {@link WindowInsets(SparseArray, SparseArray, boolean, boolean, DisplayCutout)}
+ */
+ @Deprecated
+ public WindowInsets(Rect systemWindowInsetsRect, Rect stableInsetsRect, boolean isRound,
+ boolean alwaysConsumeSystemBars, DisplayCutout displayCutout) {
+ this(createCompatTypeMap(systemWindowInsetsRect), createCompatTypeMap(stableInsetsRect),
+ createCompatVisibilityMap(createCompatTypeMap(systemWindowInsetsRect)),
+ isRound, alwaysConsumeSystemBars, displayCutout, null, null,
+ systemBars(), false /* compatIgnoreVisibility */);
+ }
+
+ /**
+ * Construct a new WindowInsets from individual insets.
+ *
+ * {@code typeInsetsMap} and {@code typeMaxInsetsMap} are a map of indexOf(type) -> insets that
+ * contain the information what kind of system bars causes how much insets. The insets in this
+ * map are non-additive; i.e. they have the same origin. In other words: If two system bars
+ * overlap on one side, the insets of the larger bar will also include the insets of the smaller
+ * bar.
+ *
+ * {@code null} type inset map indicates that the respective inset is fully consumed.
+ * @hide
+ */
+ public WindowInsets(@Nullable Insets[] typeInsetsMap,
+ @Nullable Insets[] typeMaxInsetsMap,
+ boolean[] typeVisibilityMap,
+ boolean isRound,
+ boolean alwaysConsumeSystemBars, DisplayCutout displayCutout,
+ RoundedCorners roundedCorners,
+ PrivacyIndicatorBounds privacyIndicatorBounds,
+ @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility) {
+ mSystemWindowInsetsConsumed = typeInsetsMap == null;
+ mTypeInsetsMap = mSystemWindowInsetsConsumed
+ ? new Insets[SIZE]
+ : typeInsetsMap.clone();
+
+ mStableInsetsConsumed = typeMaxInsetsMap == null;
+ mTypeMaxInsetsMap = mStableInsetsConsumed
+ ? new Insets[SIZE]
+ : typeMaxInsetsMap.clone();
+
+ mTypeVisibilityMap = typeVisibilityMap;
+ mIsRound = isRound;
+ mAlwaysConsumeSystemBars = alwaysConsumeSystemBars;
+ mCompatInsetsTypes = compatInsetsTypes;
+ mCompatIgnoreVisibility = compatIgnoreVisibility;
+
+ mDisplayCutoutConsumed = displayCutout == null;
+ mDisplayCutout = (mDisplayCutoutConsumed || displayCutout.isEmpty())
+ ? null : displayCutout;
+
+ mRoundedCorners = roundedCorners;
+ mPrivacyIndicatorBounds = privacyIndicatorBounds;
+ }
+
+ /**
+ * Construct a new WindowInsets, copying all values from a source WindowInsets.
+ *
+ * @param src Source to copy insets from
+ */
+ public WindowInsets(WindowInsets src) {
+ this(src.mSystemWindowInsetsConsumed ? null : src.mTypeInsetsMap,
+ src.mStableInsetsConsumed ? null : src.mTypeMaxInsetsMap,
+ src.mTypeVisibilityMap, src.mIsRound,
+ src.mAlwaysConsumeSystemBars, displayCutoutCopyConstructorArgument(src),
+ src.mRoundedCorners,
+ src.mPrivacyIndicatorBounds,
+ src.mCompatInsetsTypes,
+ src.mCompatIgnoreVisibility);
+ }
+
+ private static DisplayCutout displayCutoutCopyConstructorArgument(WindowInsets w) {
+ if (w.mDisplayCutoutConsumed) {
+ return null;
+ } else if (w.mDisplayCutout == null) {
+ return DisplayCutout.NO_CUTOUT;
+ } else {
+ return w.mDisplayCutout;
+ }
+ }
+
+ /**
+ * @return The insets that include system bars indicated by {@code typeMask}, taken from
+ * {@code typeInsetsMap}.
+ */
+ static Insets getInsets(Insets[] typeInsetsMap, @InsetsType int typeMask) {
+ Insets result = null;
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ Insets insets = typeInsetsMap[indexOf(i)];
+ if (insets == null) {
+ continue;
+ }
+ if (result == null) {
+ result = insets;
+ } else {
+ result = Insets.max(result, insets);
+ }
+ }
+ return result == null ? Insets.NONE : result;
+ }
+
+ /**
+ * Sets all entries in {@code typeInsetsMap} that belong to {@code typeMask} to {@code insets},
+ */
+ private static void setInsets(Insets[] typeInsetsMap, @InsetsType int typeMask, Insets insets) {
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ typeInsetsMap[indexOf(i)] = insets;
+ }
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public WindowInsets(Rect systemWindowInsets) {
+ this(createCompatTypeMap(systemWindowInsets), null, new boolean[SIZE], false, false, null,
+ null, null, systemBars(), false /* compatIgnoreVisibility */);
+ }
+
+ /**
+ * Creates a indexOf(type) -> inset map for which the {@code insets} is just mapped to
+ * {@link Type#statusBars()} and {@link Type#navigationBars()}, depending on the
+ * location of the inset.
+ */
+ private static Insets[] createCompatTypeMap(@Nullable Rect insets) {
+ if (insets == null) {
+ return null;
+ }
+ Insets[] typeInsetsMap = new Insets[SIZE];
+ assignCompatInsets(typeInsetsMap, insets);
+ return typeInsetsMap;
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static void assignCompatInsets(Insets[] typeInsetsMap, Rect insets) {
+ typeInsetsMap[indexOf(STATUS_BARS)] = Insets.of(0, insets.top, 0, 0);
+ typeInsetsMap[indexOf(NAVIGATION_BARS)] =
+ Insets.of(insets.left, 0, insets.right, insets.bottom);
+ }
+
+ private static boolean[] createCompatVisibilityMap(@Nullable Insets[] typeInsetsMap) {
+ boolean[] typeVisibilityMap = new boolean[SIZE];
+ if (typeInsetsMap == null) {
+ return typeVisibilityMap;
+ }
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ int index = indexOf(i);
+ if (!Insets.NONE.equals(typeInsetsMap[index])) {
+ typeVisibilityMap[index] = true;
+ }
+ }
+ return typeVisibilityMap;
+ }
+
+ /**
+ * Used to provide a safe copy of the system window insets to pass through
+ * to the existing fitSystemWindows method and other similar internals.
+ * @hide
+ *
+ * @deprecated use {@link #getSystemWindowInsets()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Rect getSystemWindowInsetsAsRect() {
+ if (mTempRect == null) {
+ mTempRect = new Rect();
+ }
+ Insets insets = getSystemWindowInsets();
+ mTempRect.set(insets.left, insets.top, insets.right, insets.bottom);
+ return mTempRect;
+ }
+
+ /**
+ * Returns the system window insets in pixels.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system windows.
+ * </p>
+ *
+ * @return The system window insets
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ @NonNull
+ public Insets getSystemWindowInsets() {
+ Insets result = mCompatIgnoreVisibility
+ ? getInsetsIgnoringVisibility(mCompatInsetsTypes & ~ime())
+ : getInsets(mCompatInsetsTypes);
+
+ // We can't query max insets for IME, so we need to add it manually after.
+ if ((mCompatInsetsTypes & ime()) != 0 && mCompatIgnoreVisibility) {
+ result = Insets.max(result, getInsets(ime()));
+ }
+ return result;
+ }
+
+ /**
+ * Returns the insets of a specific set of windows causing insets, denoted by the
+ * {@code typeMask} bit mask of {@link Type}s.
+ *
+ * @param typeMask Bit mask of {@link Type}s to query the insets for.
+ * @return The insets.
+ */
+ @NonNull
+ public Insets getInsets(@InsetsType int typeMask) {
+ return getInsets(mTypeInsetsMap, typeMask);
+ }
+
+ /**
+ * Returns the insets a specific set of windows can cause, denoted by the
+ * {@code typeMask} bit mask of {@link Type}s, regardless of whether that type is
+ * currently visible or not.
+ *
+ * <p>The insets represents the area of a a window that that <b>may</b> be partially
+ * or fully obscured by the system window identified by {@code type}. This value does not
+ * change based on the visibility state of those elements. For example, if the status bar is
+ * normally shown, but temporarily hidden, the inset returned here will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @param typeMask Bit mask of {@link Type}s to query the insets for.
+ * @return The insets.
+ *
+ * @throws IllegalArgumentException If the caller tries to query {@link Type#ime()}. Insets are
+ * not available if the IME isn't visible as the height of the
+ * IME is dynamic depending on the {@link EditorInfo} of the
+ * currently focused view, as well as the UI state of the IME.
+ */
+ @NonNull
+ public Insets getInsetsIgnoringVisibility(@InsetsType int typeMask) {
+ if ((typeMask & IME) != 0) {
+ throw new IllegalArgumentException("Unable to query the maximum insets for IME");
+ }
+ return getInsets(mTypeMaxInsetsMap, typeMask);
+ }
+
+ /**
+ * Returns whether a set of windows that may cause insets is currently visible on screen,
+ * regardless of whether it actually overlaps with this window.
+ *
+ * @param typeMask Bit mask of {@link Type}s to query visibility status.
+ * @return {@code true} if and only if all windows included in {@code typeMask} are currently
+ * visible on screen.
+ */
+ public boolean isVisible(@InsetsType int typeMask) {
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ if (!mTypeVisibilityMap[indexOf(i)]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the left system window inset in pixels.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system windows.
+ * </p>
+ *
+ * @return The left system window inset
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getSystemWindowInsetLeft() {
+ return getSystemWindowInsets().left;
+ }
+
+ /**
+ * Returns the top system window inset in pixels.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system windows.
+ * </p>
+ *
+ * @return The top system window inset
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getSystemWindowInsetTop() {
+ return getSystemWindowInsets().top;
+ }
+
+ /**
+ * Returns the right system window inset in pixels.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system windows.
+ * </p>
+ *
+ * @return The right system window inset
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getSystemWindowInsetRight() {
+ return getSystemWindowInsets().right;
+ }
+
+ /**
+ * Returns the bottom system window inset in pixels.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system windows.
+ * </p>
+ *
+ * @return The bottom system window inset
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getSystemWindowInsetBottom() {
+ return getSystemWindowInsets().bottom;
+ }
+
+ /**
+ * Returns true if this WindowInsets has nonzero system window insets.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system windows.
+ * </p>
+ *
+ * @return true if any of the system window inset values are nonzero
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public boolean hasSystemWindowInsets() {
+ return !getSystemWindowInsets().equals(Insets.NONE);
+ }
+
+ /**
+ * Returns true if this WindowInsets has any nonzero insets.
+ *
+ * @return true if any inset values are nonzero
+ */
+ public boolean hasInsets() {
+ return !getInsets(mTypeInsetsMap, all()).equals(Insets.NONE)
+ || !getInsets(mTypeMaxInsetsMap, all()).equals(Insets.NONE)
+ || mDisplayCutout != null || mRoundedCorners != null;
+ }
+
+ /**
+ * Returns the display cutout if there is one.
+ *
+ * <p>Note: the display cutout will already be {@link #consumeDisplayCutout consumed} during
+ * dispatch to {@link View#onApplyWindowInsets}, unless the window has requested a
+ * {@link WindowManager.LayoutParams#layoutInDisplayCutoutMode} other than
+ * {@link WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER never} or
+ * {@link WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT default}.
+ *
+ * @return the display cutout or null if there is none
+ * @see DisplayCutout
+ */
+ @Nullable
+ public DisplayCutout getDisplayCutout() {
+ return mDisplayCutout;
+ }
+
+ /**
+ * Returns the {@link RoundedCorner} of the given position if there is one.
+ *
+ * @param position the position of the rounded corner on the display. The value should be one of
+ * the following:
+ * {@link RoundedCorner#POSITION_TOP_LEFT},
+ * {@link RoundedCorner#POSITION_TOP_RIGHT},
+ * {@link RoundedCorner#POSITION_BOTTOM_RIGHT},
+ * {@link RoundedCorner#POSITION_BOTTOM_LEFT}.
+ * @return the rounded corner of the given position. Returns {@code null} if there is none or
+ * the rounded corner area is not inside the application's bounds.
+ */
+ @Nullable
+ public RoundedCorner getRoundedCorner(@RoundedCorner.Position int position) {
+ return mRoundedCorners == null ? null : mRoundedCorners.getRoundedCorner(position);
+ }
+
+ /**
+ * Returns the {@link Rect} of the maximum bounds of the system privacy indicator, for the
+ * current orientation, in relative coordinates, or null if the bounds have not been loaded yet.
+ * The privacy indicator shows over apps when an app uses the microphone or camera permissions,
+ * while an app is in immersive mode.
+ *
+ * @return A rectangle representing the maximum bounds of the indicator
+ */
+ public @Nullable Rect getPrivacyIndicatorBounds() {
+ return mPrivacyIndicatorBounds == null ? null
+ : mPrivacyIndicatorBounds.getStaticPrivacyIndicatorBounds();
+ }
+
+ /**
+ * Returns a copy of this WindowInsets with the cutout fully consumed.
+ *
+ * @return A modified copy of this WindowInsets
+ * @deprecated Consuming of different parts individually of a {@link WindowInsets} instance is
+ * deprecated, since {@link WindowInsets} contains many different insets. Use {@link #CONSUMED}
+ * instead to stop dispatching insets.
+ */
+ @Deprecated
+ @NonNull
+ public WindowInsets consumeDisplayCutout() {
+ return new WindowInsets(mSystemWindowInsetsConsumed ? null : mTypeInsetsMap,
+ mStableInsetsConsumed ? null : mTypeMaxInsetsMap,
+ mTypeVisibilityMap,
+ mIsRound, mAlwaysConsumeSystemBars,
+ null /* displayCutout */, mRoundedCorners, mPrivacyIndicatorBounds,
+ mCompatInsetsTypes, mCompatIgnoreVisibility);
+ }
+
+
+ /**
+ * Check if these insets have been fully consumed.
+ *
+ * <p>Insets are considered "consumed" if the applicable <code>consume*</code> methods
+ * have been called such that all insets have been set to zero. This affects propagation of
+ * insets through the view hierarchy; insets that have not been fully consumed will continue
+ * to propagate down to child views.</p>
+ *
+ * <p>The result of this method is equivalent to the return value of
+ * {@link View#fitSystemWindows(android.graphics.Rect)}.</p>
+ *
+ * @return true if the insets have been fully consumed.
+ */
+ public boolean isConsumed() {
+ return mSystemWindowInsetsConsumed && mStableInsetsConsumed
+ && mDisplayCutoutConsumed;
+ }
+
+ /**
+ * Returns true if the associated window has a round shape.
+ *
+ * <p>A round window's left, top, right and bottom edges reach all the way to the
+ * associated edges of the window but the corners may not be visible. Views responding
+ * to round insets should take care to not lay out critical elements within the corners
+ * where they may not be accessible.</p>
+ *
+ * @return True if the window is round
+ */
+ public boolean isRound() {
+ return mIsRound;
+ }
+
+ /**
+ * Returns a copy of this WindowInsets with the system window insets fully consumed.
+ *
+ * @return A modified copy of this WindowInsets
+ * @deprecated Consuming of different parts individually of a {@link WindowInsets} instance is
+ * deprecated, since {@link WindowInsets} contains many different insets. Use {@link #CONSUMED}
+ * instead to stop dispatching insets.
+ */
+ @Deprecated
+ @NonNull
+ public WindowInsets consumeSystemWindowInsets() {
+ return new WindowInsets(null, null,
+ mTypeVisibilityMap,
+ mIsRound, mAlwaysConsumeSystemBars,
+ displayCutoutCopyConstructorArgument(this),
+ mRoundedCorners, mPrivacyIndicatorBounds, mCompatInsetsTypes,
+ mCompatIgnoreVisibility);
+ }
+
+ // TODO(b/119190588): replace @code with @link below
+ /**
+ * Returns a copy of this WindowInsets with selected system window insets replaced
+ * with new values.
+ *
+ * <p>Note: If the system window insets are already consumed, this method will return them
+ * unchanged on {@link android.os.Build.VERSION_CODES#Q Q} and later. Prior to
+ * {@link android.os.Build.VERSION_CODES#Q Q}, the new values were applied regardless of
+ * whether they were consumed, and this method returns invalid non-zero consumed insets.
+ *
+ * @param left New left inset in pixels
+ * @param top New top inset in pixels
+ * @param right New right inset in pixels
+ * @param bottom New bottom inset in pixels
+ * @return A modified copy of this WindowInsets
+ * @deprecated use {@code Builder#Builder(WindowInsets)} with
+ * {@link Builder#setSystemWindowInsets(Insets)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public WindowInsets replaceSystemWindowInsets(int left, int top, int right, int bottom) {
+ // Compat edge case: what should this do if the insets have already been consumed?
+ // On platforms prior to Q, the behavior was to override the insets with non-zero values,
+ // but leave them consumed, which is invalid (consumed insets must be zero).
+ // The behavior is now keeping them consumed and discarding the new insets.
+ if (mSystemWindowInsetsConsumed) {
+ return this;
+ }
+ return new Builder(this).setSystemWindowInsets(Insets.of(left, top, right, bottom)).build();
+ }
+
+ // TODO(b/119190588): replace @code with @link below
+ /**
+ * Returns a copy of this WindowInsets with selected system window insets replaced
+ * with new values.
+ *
+ * <p>Note: If the system window insets are already consumed, this method will return them
+ * unchanged on {@link android.os.Build.VERSION_CODES#Q Q} and later. Prior to
+ * {@link android.os.Build.VERSION_CODES#Q Q}, the new values were applied regardless of
+ * whether they were consumed, and this method returns invalid non-zero consumed insets.
+ *
+ * @param systemWindowInsets New system window insets. Each field is the inset in pixels
+ * for that edge
+ * @return A modified copy of this WindowInsets
+ * @deprecated use {@code Builder#Builder(WindowInsets)} with
+ * {@link Builder#setSystemWindowInsets(Insets)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public WindowInsets replaceSystemWindowInsets(Rect systemWindowInsets) {
+ return replaceSystemWindowInsets(systemWindowInsets.left, systemWindowInsets.top,
+ systemWindowInsets.right, systemWindowInsets.bottom);
+ }
+
+ /**
+ * Returns the stable insets in pixels.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @return The stable insets
+ * @deprecated Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ @NonNull
+ public Insets getStableInsets() {
+ return getInsets(mTypeMaxInsetsMap, mCompatInsetsTypes);
+ }
+
+ /**
+ * Returns the top stable inset in pixels.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @return The top stable inset
+ * @deprecated Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getStableInsetTop() {
+ return getStableInsets().top;
+ }
+
+ /**
+ * Returns the left stable inset in pixels.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @return The left stable inset
+ * @deprecated Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getStableInsetLeft() {
+ return getStableInsets().left;
+ }
+
+ /**
+ * Returns the right stable inset in pixels.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @return The right stable inset
+ * @deprecated Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getStableInsetRight() {
+ return getStableInsets().right;
+ }
+
+ /**
+ * Returns the bottom stable inset in pixels.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @return The bottom stable inset
+ * @deprecated Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public int getStableInsetBottom() {
+ return getStableInsets().bottom;
+ }
+
+ /**
+ * Returns true if this WindowInsets has nonzero stable insets.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @return true if any of the stable inset values are nonzero
+ * @deprecated Use {@link #getInsetsIgnoringVisibility(int)} with {@link Type#systemBars()}
+ * instead.
+ */
+ @Deprecated
+ public boolean hasStableInsets() {
+ return !getStableInsets().equals(Insets.NONE);
+ }
+
+ /**
+ * Returns the system gesture insets.
+ *
+ * <p>The system gesture insets represent the area of a window where system gestures have
+ * priority and may consume some or all touch input, e.g. due to the a system bar
+ * occupying it, or it being reserved for touch-only gestures.
+ *
+ * <p>An app can declare priority over system gestures with
+ * {@link View#setSystemGestureExclusionRects} outside of the
+ * {@link #getMandatorySystemGestureInsets() mandatory system gesture insets}.
+ *
+ * <p>Note: the system will put a limit of <code>200dp</code> on the vertical extent of the
+ * exclusions it takes into account. The limit does not apply while the navigation
+ * bar is {@link View#SYSTEM_UI_FLAG_IMMERSIVE_STICKY stickily} hidden, nor to the
+ * {@link android.inputmethodservice.InputMethodService input method} and
+ * {@link Intent#CATEGORY_HOME home activity}.
+ * </p>
+ *
+ *
+ * <p>Simple taps are guaranteed to reach the window even within the system gesture insets,
+ * as long as they are outside the {@link #getTappableElementInsets() system window insets}.
+ *
+ * <p>When {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} is requested, an inset will be returned
+ * even when the system gestures are inactive due to
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} or
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
+ *
+ * <p>This inset is consumed together with the {@link #getSystemWindowInsets()
+ * system window insets} by {@link #consumeSystemWindowInsets()}.
+ *
+ * @see #getMandatorySystemGestureInsets
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#systemGestures()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Insets getSystemGestureInsets() {
+ return getInsets(mTypeInsetsMap, SYSTEM_GESTURES);
+ }
+
+ /**
+ * Returns the mandatory system gesture insets.
+ *
+ * <p>The mandatory system gesture insets represent the area of a window where mandatory system
+ * gestures have priority and may consume some or all touch input, e.g. due to the a system bar
+ * occupying it, or it being reserved for touch-only gestures.
+ *
+ * <p>In contrast to {@link #getSystemGestureInsets regular system gestures}, <b>mandatory</b>
+ * system gestures cannot be overriden by {@link View#setSystemGestureExclusionRects}.
+ *
+ * <p>Simple taps are guaranteed to reach the window even within the system gesture insets,
+ * as long as they are outside the {@link #getTappableElementInsets() system window insets}.
+ *
+ * <p>When {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} is requested, an inset will be returned
+ * even when the system gestures are inactive due to
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} or
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
+ *
+ * <p>This inset is consumed together with the {@link #getSystemWindowInsets()
+ * system window insets} by {@link #consumeSystemWindowInsets()}.
+ *
+ * @see #getSystemGestureInsets
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#mandatorySystemGestures()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Insets getMandatorySystemGestureInsets() {
+ return getInsets(mTypeInsetsMap, MANDATORY_SYSTEM_GESTURES);
+ }
+
+ /**
+ * Returns the tappable element insets.
+ *
+ * <p>The tappable element insets represent how much tappable elements <b>must at least</b> be
+ * inset to remain both tappable and visually unobstructed by persistent system windows.
+ *
+ * <p>This may be smaller than {@link #getSystemWindowInsets()} if the system window is
+ * largely transparent and lets through simple taps (but not necessarily more complex gestures).
+ *
+ * <p>Note that generally, tappable elements <strong>should</strong> be aligned with the
+ * {@link #getSystemWindowInsets() system window insets} instead to avoid overlapping with the
+ * system bars.
+ *
+ * <p>When {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} is requested, an inset will be returned
+ * even when the area covered by the inset would be tappable due to
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} or
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
+ *
+ * <p>This inset is consumed together with the {@link #getSystemWindowInsets()
+ * system window insets} by {@link #consumeSystemWindowInsets()}.
+ *
+ * @deprecated Use {@link #getInsets(int)} with {@link Type#tappableElement()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Insets getTappableElementInsets() {
+ return getInsets(mTypeInsetsMap, TAPPABLE_ELEMENT);
+ }
+
+ /**
+ * Returns a copy of this WindowInsets with the stable insets fully consumed.
+ *
+ * @return A modified copy of this WindowInsets
+ * @deprecated Consuming of different parts individually of a {@link WindowInsets} instance is
+ * deprecated, since {@link WindowInsets} contains many different insets. Use {@link #CONSUMED}
+ * instead to stop dispatching insets. On {@link android.os.Build.VERSION_CODES#R R}, this
+ * method has no effect.
+ */
+ @Deprecated
+ @NonNull
+ public WindowInsets consumeStableInsets() {
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean shouldAlwaysConsumeSystemBars() {
+ return mAlwaysConsumeSystemBars;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder("WindowInsets{\n ");
+ for (int i = 0; i < SIZE; i++) {
+ Insets insets = mTypeInsetsMap[i];
+ Insets maxInsets = mTypeMaxInsetsMap[i];
+ boolean visible = mTypeVisibilityMap[i];
+ if (!Insets.NONE.equals(insets) || !Insets.NONE.equals(maxInsets) || visible) {
+ result.append(Type.toString(1 << i)).append("=").append(insets)
+ .append(" max=").append(maxInsets)
+ .append(" vis=").append(visible)
+ .append("\n ");
+ }
+ }
+
+ result.append(mDisplayCutout != null ? "cutout=" + mDisplayCutout : "");
+ result.append("\n ");
+ result.append(mRoundedCorners != null ? "roundedCorners=" + mRoundedCorners : "");
+ result.append("\n ");
+ result.append(mPrivacyIndicatorBounds != null ? "privacyIndicatorBounds="
+ + mPrivacyIndicatorBounds : "");
+ result.append("\n ");
+ result.append(isRound() ? "round" : "");
+ result.append("}");
+ return result.toString();
+ }
+
+ /**
+ * Returns a copy of this instance inset in the given directions.
+ *
+ * @see #inset(int, int, int, int)
+ * @deprecated use {@link #inset(Insets)}
+ * @hide
+ */
+ @Deprecated
+ @NonNull
+ public WindowInsets inset(Rect r) {
+ return inset(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Returns a copy of this instance inset in the given directions.
+ *
+ * This is intended for dispatching insets to areas of the window that are smaller than the
+ * current area.
+ *
+ * <p>Example:
+ * <pre>
+ * childView.dispatchApplyWindowInsets(insets.inset(childMargins));
+ * </pre>
+ *
+ * @param insets the amount of insets to remove from all sides.
+ *
+ * @see #inset(int, int, int, int)
+ */
+ @NonNull
+ public WindowInsets inset(@NonNull Insets insets) {
+ Objects.requireNonNull(insets);
+ return inset(insets.left, insets.top, insets.right, insets.bottom);
+ }
+
+ /**
+ * Returns a copy of this instance inset in the given directions.
+ *
+ * This is intended for dispatching insets to areas of the window that are smaller than the
+ * current area.
+ *
+ * <p>Example:
+ * <pre>
+ * childView.dispatchApplyWindowInsets(insets.inset(
+ * childMarginLeft, childMarginTop, childMarginBottom, childMarginRight));
+ * </pre>
+ *
+ * @param left the amount of insets to remove from the left. Must be non-negative.
+ * @param top the amount of insets to remove from the top. Must be non-negative.
+ * @param right the amount of insets to remove from the right. Must be non-negative.
+ * @param bottom the amount of insets to remove from the bottom. Must be non-negative.
+ *
+ * @return the inset insets
+ *
+ * @see #inset(Insets)
+ */
+ @NonNull
+ public WindowInsets inset(@IntRange(from = 0) int left, @IntRange(from = 0) int top,
+ @IntRange(from = 0) int right, @IntRange(from = 0) int bottom) {
+ Preconditions.checkArgumentNonnegative(left);
+ Preconditions.checkArgumentNonnegative(top);
+ Preconditions.checkArgumentNonnegative(right);
+ Preconditions.checkArgumentNonnegative(bottom);
+
+ return insetUnchecked(left, top, right, bottom);
+ }
+
+ /**
+ * @see #inset(int, int, int, int)
+ * @hide
+ */
+ @NonNull
+ public WindowInsets insetUnchecked(int left, int top, int right, int bottom) {
+ return new WindowInsets(
+ mSystemWindowInsetsConsumed
+ ? null
+ : insetInsets(mTypeInsetsMap, left, top, right, bottom),
+ mStableInsetsConsumed
+ ? null
+ : insetInsets(mTypeMaxInsetsMap, left, top, right, bottom),
+ mTypeVisibilityMap,
+ mIsRound, mAlwaysConsumeSystemBars,
+ mDisplayCutoutConsumed
+ ? null
+ : mDisplayCutout == null
+ ? DisplayCutout.NO_CUTOUT
+ : mDisplayCutout.inset(left, top, right, bottom),
+ mRoundedCorners == null
+ ? RoundedCorners.NO_ROUNDED_CORNERS
+ : mRoundedCorners.inset(left, top, right, bottom),
+ mPrivacyIndicatorBounds == null
+ ? null
+ : mPrivacyIndicatorBounds.inset(left, top, right, bottom),
+ mCompatInsetsTypes, mCompatIgnoreVisibility);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || !(o instanceof WindowInsets)) return false;
+ WindowInsets that = (WindowInsets) o;
+
+ return mIsRound == that.mIsRound
+ && mAlwaysConsumeSystemBars == that.mAlwaysConsumeSystemBars
+ && mSystemWindowInsetsConsumed == that.mSystemWindowInsetsConsumed
+ && mStableInsetsConsumed == that.mStableInsetsConsumed
+ && mDisplayCutoutConsumed == that.mDisplayCutoutConsumed
+ && Arrays.equals(mTypeInsetsMap, that.mTypeInsetsMap)
+ && Arrays.equals(mTypeMaxInsetsMap, that.mTypeMaxInsetsMap)
+ && Arrays.equals(mTypeVisibilityMap, that.mTypeVisibilityMap)
+ && Objects.equals(mDisplayCutout, that.mDisplayCutout)
+ && Objects.equals(mRoundedCorners, that.mRoundedCorners)
+ && Objects.equals(mPrivacyIndicatorBounds, that.mPrivacyIndicatorBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(mTypeInsetsMap), Arrays.hashCode(mTypeMaxInsetsMap),
+ Arrays.hashCode(mTypeVisibilityMap), mIsRound, mDisplayCutout, mRoundedCorners,
+ mAlwaysConsumeSystemBars, mSystemWindowInsetsConsumed, mStableInsetsConsumed,
+ mDisplayCutoutConsumed, mPrivacyIndicatorBounds);
+ }
+
+
+ /**
+ * Insets every inset in {@code typeInsetsMap} by the specified left, top, right, bottom.
+ *
+ * @return {@code typeInsetsMap} if no inset was modified; a copy of the map with the modified
+ * insets otherwise.
+ */
+ private static Insets[] insetInsets(
+ Insets[] typeInsetsMap, int left, int top, int right, int bottom) {
+ boolean cloned = false;
+ for (int i = 0; i < SIZE; i++) {
+ Insets insets = typeInsetsMap[i];
+ if (insets == null) {
+ continue;
+ }
+ Insets insetInsets = insetInsets(insets, left, top, right, bottom);
+ if (insetInsets != insets) {
+ if (!cloned) {
+ typeInsetsMap = typeInsetsMap.clone();
+ cloned = true;
+ }
+ typeInsetsMap[i] = insetInsets;
+ }
+ }
+ return typeInsetsMap;
+ }
+
+ static Insets insetInsets(Insets insets, int left, int top, int right, int bottom) {
+ int newLeft = Math.max(0, insets.left - left);
+ int newTop = Math.max(0, insets.top - top);
+ int newRight = Math.max(0, insets.right - right);
+ int newBottom = Math.max(0, insets.bottom - bottom);
+ if (newLeft == left && newTop == top && newRight == right && newBottom == bottom) {
+ return insets;
+ }
+ return Insets.of(newLeft, newTop, newRight, newBottom);
+ }
+
+ /**
+ * @return whether system window insets have been consumed.
+ */
+ boolean isSystemWindowInsetsConsumed() {
+ return mSystemWindowInsetsConsumed;
+ }
+
+ /**
+ * Builder for WindowInsets.
+ */
+ public static final class Builder {
+
+ private final Insets[] mTypeInsetsMap;
+ private final Insets[] mTypeMaxInsetsMap;
+ private final boolean[] mTypeVisibilityMap;
+ private boolean mSystemInsetsConsumed = true;
+ private boolean mStableInsetsConsumed = true;
+
+ private DisplayCutout mDisplayCutout;
+ private RoundedCorners mRoundedCorners = RoundedCorners.NO_ROUNDED_CORNERS;
+
+ private boolean mIsRound;
+ private boolean mAlwaysConsumeSystemBars;
+
+ private PrivacyIndicatorBounds mPrivacyIndicatorBounds = new PrivacyIndicatorBounds();
+
+ /**
+ * Creates a builder where all insets are initially consumed.
+ */
+ public Builder() {
+ mTypeInsetsMap = new Insets[SIZE];
+ mTypeMaxInsetsMap = new Insets[SIZE];
+ mTypeVisibilityMap = new boolean[SIZE];
+ }
+
+ /**
+ * Creates a builder where all insets are initialized from {@link WindowInsets}.
+ *
+ * @param insets the instance to initialize from.
+ */
+ public Builder(@NonNull WindowInsets insets) {
+ mTypeInsetsMap = insets.mTypeInsetsMap.clone();
+ mTypeMaxInsetsMap = insets.mTypeMaxInsetsMap.clone();
+ mTypeVisibilityMap = insets.mTypeVisibilityMap.clone();
+ mSystemInsetsConsumed = insets.mSystemWindowInsetsConsumed;
+ mStableInsetsConsumed = insets.mStableInsetsConsumed;
+ mDisplayCutout = displayCutoutCopyConstructorArgument(insets);
+ mRoundedCorners = insets.mRoundedCorners;
+ mIsRound = insets.mIsRound;
+ mAlwaysConsumeSystemBars = insets.mAlwaysConsumeSystemBars;
+ mPrivacyIndicatorBounds = insets.mPrivacyIndicatorBounds;
+ }
+
+ /**
+ * Sets system window insets in pixels.
+ *
+ * <p>The system window inset represents the area of a full-screen window that is
+ * partially or fully obscured by the status bar, navigation bar, IME or other system
+ * windows.</p>
+ *
+ * @see #getSystemWindowInsets()
+ * @return itself
+ * @deprecated Use {@link #setInsets(int, Insets)} with {@link Type#systemBars()}.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setSystemWindowInsets(@NonNull Insets systemWindowInsets) {
+ Preconditions.checkNotNull(systemWindowInsets);
+ assignCompatInsets(mTypeInsetsMap, systemWindowInsets.toRect());
+ mSystemInsetsConsumed = false;
+ return this;
+ }
+
+ /**
+ * Sets system gesture insets in pixels.
+ *
+ * <p>The system gesture insets represent the area of a window where system gestures have
+ * priority and may consume some or all touch input, e.g. due to the a system bar
+ * occupying it, or it being reserved for touch-only gestures.
+ *
+ * @see #getSystemGestureInsets()
+ * @return itself
+ * @deprecated Use {@link #setInsets(int, Insets)} with {@link Type#systemGestures()}.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setSystemGestureInsets(@NonNull Insets insets) {
+ WindowInsets.setInsets(mTypeInsetsMap, SYSTEM_GESTURES, insets);
+ return this;
+ }
+
+ /**
+ * Sets mandatory system gesture insets in pixels.
+ *
+ * <p>The mandatory system gesture insets represent the area of a window where mandatory
+ * system gestures have priority and may consume some or all touch input, e.g. due to the a
+ * system bar occupying it, or it being reserved for touch-only gestures.
+ *
+ * <p>In contrast to {@link #setSystemGestureInsets regular system gestures},
+ * <b>mandatory</b> system gestures cannot be overriden by
+ * {@link View#setSystemGestureExclusionRects}.
+ *
+ * @see #getMandatorySystemGestureInsets()
+ * @return itself
+ * @deprecated Use {@link #setInsets(int, Insets)} with
+ * {@link Type#mandatorySystemGestures()}.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setMandatorySystemGestureInsets(@NonNull Insets insets) {
+ WindowInsets.setInsets(mTypeInsetsMap, MANDATORY_SYSTEM_GESTURES, insets);
+ return this;
+ }
+
+ /**
+ * Sets tappable element insets in pixels.
+ *
+ * <p>The tappable element insets represent how much tappable elements <b>must at least</b>
+ * be inset to remain both tappable and visually unobstructed by persistent system windows.
+ *
+ * @see #getTappableElementInsets()
+ * @return itself
+ * @deprecated Use {@link #setInsets(int, Insets)} with {@link Type#tappableElement()}.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setTappableElementInsets(@NonNull Insets insets) {
+ WindowInsets.setInsets(mTypeInsetsMap, TAPPABLE_ELEMENT, insets);
+ return this;
+ }
+
+ /**
+ * Sets the insets of a specific window type in pixels.
+ *
+ * <p>The insets represents the area of a a window that is partially or fully obscured by
+ * the system windows identified by {@code typeMask}.
+ * </p>
+ *
+ * @see #getInsets(int)
+ *
+ * @param typeMask The bitmask of {@link Type} to set the insets for.
+ * @param insets The insets to set.
+ *
+ * @return itself
+ */
+ @NonNull
+ public Builder setInsets(@InsetsType int typeMask, @NonNull Insets insets) {
+ Preconditions.checkNotNull(insets);
+ WindowInsets.setInsets(mTypeInsetsMap, typeMask, insets);
+ mSystemInsetsConsumed = false;
+ return this;
+ }
+
+ /**
+ * Sets the insets a specific window type in pixels, while ignoring its visibility state.
+ *
+ * <p>The insets represents the area of a a window that that <b>may</b> be partially
+ * or fully obscured by the system window identified by {@code type}. This value does not
+ * change based on the visibility state of those elements. For example, if the status bar is
+ * normally shown, but temporarily hidden, the inset returned here will still provide the
+ * inset associated with the status bar being shown.</p>
+ *
+ * @see #getInsetsIgnoringVisibility(int)
+ *
+ * @param typeMask The bitmask of {@link Type} to set the insets for.
+ * @param insets The insets to set.
+ *
+ * @return itself
+ *
+ * @throws IllegalArgumentException If {@code typeMask} contains {@link Type#ime()}. Maximum
+ * insets are not available for this type as the height of
+ * the IME is dynamic depending on the {@link EditorInfo}
+ * of the currently focused view, as well as the UI
+ * state of the IME.
+ */
+ @NonNull
+ public Builder setInsetsIgnoringVisibility(@InsetsType int typeMask, @NonNull Insets insets)
+ throws IllegalArgumentException{
+ if (typeMask == IME) {
+ throw new IllegalArgumentException("Maximum inset not available for IME");
+ }
+ Preconditions.checkNotNull(insets);
+ WindowInsets.setInsets(mTypeMaxInsetsMap, typeMask, insets);
+ mStableInsetsConsumed = false;
+ return this;
+ }
+
+ /**
+ * Sets whether windows that can cause insets are currently visible on screen.
+ *
+ *
+ * @see #isVisible(int)
+ *
+ * @param typeMask The bitmask of {@link Type} to set the visibility for.
+ * @param visible Whether to mark the windows as visible or not.
+ *
+ * @return itself
+ */
+ @NonNull
+ public Builder setVisible(@InsetsType int typeMask, boolean visible) {
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ mTypeVisibilityMap[indexOf(i)] = visible;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the stable insets in pixels.
+ *
+ * <p>The stable inset represents the area of a full-screen window that <b>may</b> be
+ * partially or fully obscured by the system UI elements. This value does not change
+ * based on the visibility state of those elements; for example, if the status bar is
+ * normally shown, but temporarily hidden, the stable inset will still provide the inset
+ * associated with the status bar being shown.</p>
+ *
+ * @see #getStableInsets()
+ * @return itself
+ * @deprecated Use {@link #setInsetsIgnoringVisibility(int, Insets)} with
+ * {@link Type#systemBars()}.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setStableInsets(@NonNull Insets stableInsets) {
+ Preconditions.checkNotNull(stableInsets);
+ assignCompatInsets(mTypeMaxInsetsMap, stableInsets.toRect());
+ mStableInsetsConsumed = false;
+ return this;
+ }
+
+ /**
+ * Sets the display cutout.
+ *
+ * @see #getDisplayCutout()
+ * @param displayCutout the display cutout or null if there is none
+ * @return itself
+ */
+ @NonNull
+ public Builder setDisplayCutout(@Nullable DisplayCutout displayCutout) {
+ mDisplayCutout = displayCutout != null ? displayCutout : DisplayCutout.NO_CUTOUT;
+ if (!mDisplayCutout.isEmpty()) {
+ final Insets safeInsets = Insets.of(mDisplayCutout.getSafeInsets());
+ final int index = indexOf(DISPLAY_CUTOUT);
+ mTypeInsetsMap[index] = safeInsets;
+ mTypeMaxInsetsMap[index] = safeInsets;
+ mTypeVisibilityMap[index] = true;
+ }
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public Builder setRoundedCorners(RoundedCorners roundedCorners) {
+ mRoundedCorners = roundedCorners != null
+ ? roundedCorners : RoundedCorners.NO_ROUNDED_CORNERS;
+ return this;
+ }
+
+ /**
+ * Sets the rounded corner of given position.
+ *
+ * @see #getRoundedCorner(int)
+ * @param position the position of this rounded corner
+ * @param roundedCorner the rounded corner or null if there is none
+ * @return itself
+ */
+ @NonNull
+ public Builder setRoundedCorner(@RoundedCorner.Position int position,
+ @Nullable RoundedCorner roundedCorner) {
+ mRoundedCorners.setRoundedCorner(position, roundedCorner);
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public Builder setPrivacyIndicatorBounds(@Nullable PrivacyIndicatorBounds bounds) {
+ mPrivacyIndicatorBounds = bounds;
+ return this;
+ }
+
+ /**
+ * Sets the bounds of the system privacy indicator.
+ *
+ * @param bounds The bounds of the system privacy indicator
+ */
+ @NonNull
+ public Builder setPrivacyIndicatorBounds(@Nullable Rect bounds) {
+ //TODO 188788786: refactor the indicator bounds
+ Rect[] boundsArr = { bounds, bounds, bounds, bounds };
+ mPrivacyIndicatorBounds = new PrivacyIndicatorBounds(boundsArr, ROTATION_0);
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public Builder setRound(boolean round) {
+ mIsRound = round;
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public Builder setAlwaysConsumeSystemBars(boolean alwaysConsumeSystemBars) {
+ mAlwaysConsumeSystemBars = alwaysConsumeSystemBars;
+ return this;
+ }
+
+ /**
+ * Builds a {@link WindowInsets} instance.
+ *
+ * @return the {@link WindowInsets} instance.
+ */
+ @NonNull
+ public WindowInsets build() {
+ return new WindowInsets(mSystemInsetsConsumed ? null : mTypeInsetsMap,
+ mStableInsetsConsumed ? null : mTypeMaxInsetsMap, mTypeVisibilityMap,
+ mIsRound, mAlwaysConsumeSystemBars, mDisplayCutout, mRoundedCorners,
+ mPrivacyIndicatorBounds, systemBars(), false /* compatIgnoreVisibility */);
+ }
+ }
+
+ /**
+ * Class that defines different types of sources causing window insets.
+ */
+ public static final class Type {
+
+ static final int FIRST = 1 << 0;
+ static final int STATUS_BARS = FIRST;
+ static final int NAVIGATION_BARS = 1 << 1;
+ static final int CAPTION_BAR = 1 << 2;
+
+ static final int IME = 1 << 3;
+
+ static final int SYSTEM_GESTURES = 1 << 4;
+ static final int MANDATORY_SYSTEM_GESTURES = 1 << 5;
+ static final int TAPPABLE_ELEMENT = 1 << 6;
+
+ static final int DISPLAY_CUTOUT = 1 << 7;
+
+ static final int LAST = 1 << 8;
+ static final int SIZE = 9;
+ static final int WINDOW_DECOR = LAST;
+
+ static int indexOf(@InsetsType int type) {
+ switch (type) {
+ case STATUS_BARS:
+ return 0;
+ case NAVIGATION_BARS:
+ return 1;
+ case CAPTION_BAR:
+ return 2;
+ case IME:
+ return 3;
+ case SYSTEM_GESTURES:
+ return 4;
+ case MANDATORY_SYSTEM_GESTURES:
+ return 5;
+ case TAPPABLE_ELEMENT:
+ return 6;
+ case DISPLAY_CUTOUT:
+ return 7;
+ case WINDOW_DECOR:
+ return 8;
+ default:
+ throw new IllegalArgumentException("type needs to be >= FIRST and <= LAST,"
+ + " type=" + type);
+ }
+ }
+
+ static String toString(@InsetsType int types) {
+ StringBuilder result = new StringBuilder();
+ if ((types & STATUS_BARS) != 0) {
+ result.append("statusBars |");
+ }
+ if ((types & NAVIGATION_BARS) != 0) {
+ result.append("navigationBars |");
+ }
+ if ((types & CAPTION_BAR) != 0) {
+ result.append("captionBar |");
+ }
+ if ((types & IME) != 0) {
+ result.append("ime |");
+ }
+ if ((types & SYSTEM_GESTURES) != 0) {
+ result.append("systemGestures |");
+ }
+ if ((types & MANDATORY_SYSTEM_GESTURES) != 0) {
+ result.append("mandatorySystemGestures |");
+ }
+ if ((types & TAPPABLE_ELEMENT) != 0) {
+ result.append("tappableElement |");
+ }
+ if ((types & DISPLAY_CUTOUT) != 0) {
+ result.append("displayCutout |");
+ }
+ if ((types & WINDOW_DECOR) != 0) {
+ result.append("windowDecor |");
+ }
+ if (result.length() > 0) {
+ result.delete(result.length() - 2, result.length());
+ }
+ return result.toString();
+ }
+
+ private Type() {
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {STATUS_BARS, NAVIGATION_BARS, CAPTION_BAR, IME, WINDOW_DECOR,
+ SYSTEM_GESTURES, MANDATORY_SYSTEM_GESTURES, TAPPABLE_ELEMENT, DISPLAY_CUTOUT})
+ public @interface InsetsType {
+ }
+
+ /**
+ * @return An insets type representing any system bars for displaying status.
+ */
+ public static @InsetsType int statusBars() {
+ return STATUS_BARS;
+ }
+
+ /**
+ * @return An insets type representing any system bars for navigation.
+ */
+ public static @InsetsType int navigationBars() {
+ return NAVIGATION_BARS;
+ }
+
+ /**
+ * @return An insets type representing the window of a caption bar.
+ */
+ public static @InsetsType int captionBar() {
+ return CAPTION_BAR;
+ }
+
+ /**
+ * @return An insets type representing the window of an {@link InputMethod}.
+ */
+ public static @InsetsType int ime() {
+ return IME;
+ }
+
+ /**
+ * Returns an insets type representing the system gesture insets.
+ *
+ * <p>The system gesture insets represent the area of a window where system gestures have
+ * priority and may consume some or all touch input, e.g. due to the a system bar
+ * occupying it, or it being reserved for touch-only gestures.
+ *
+ * <p>Simple taps are guaranteed to reach the window even within the system gesture insets,
+ * as long as they are outside the {@link #getSystemWindowInsets() system window insets}.
+ *
+ * <p>When {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} is requested, an inset will be returned
+ * even when the system gestures are inactive due to
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} or
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
+ *
+ * @see #getSystemGestureInsets()
+ */
+ public static @InsetsType int systemGestures() {
+ return SYSTEM_GESTURES;
+ }
+
+ /**
+ * @see #getMandatorySystemGestureInsets
+ */
+ public static @InsetsType int mandatorySystemGestures() {
+ return MANDATORY_SYSTEM_GESTURES;
+ }
+
+ /**
+ * @see #getTappableElementInsets
+ */
+ public static @InsetsType int tappableElement() {
+ return TAPPABLE_ELEMENT;
+ }
+
+ /**
+ * Returns an insets type representing the area that used by {@link DisplayCutout}.
+ *
+ * <p>This is equivalent to the safe insets on {@link #getDisplayCutout()}.
+ *
+ * <p>Note: During dispatch to {@link View#onApplyWindowInsets}, if the window is using
+ * the {@link WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT default}
+ * {@link WindowManager.LayoutParams#layoutInDisplayCutoutMode}, {@link #getDisplayCutout()}
+ * will return {@code null} even if the window overlaps a display cutout area, in which case
+ * the {@link #displayCutout() displayCutout() inset} will still report the accurate value.
+ *
+ * @see DisplayCutout#getSafeInsetLeft()
+ * @see DisplayCutout#getSafeInsetTop()
+ * @see DisplayCutout#getSafeInsetRight()
+ * @see DisplayCutout#getSafeInsetBottom()
+ */
+ public static @InsetsType int displayCutout() {
+ return DISPLAY_CUTOUT;
+ }
+
+ /**
+ * @return All system bars. Includes {@link #statusBars()}, {@link #captionBar()} as well as
+ * {@link #navigationBars()}, but not {@link #ime()}.
+ */
+ public static @InsetsType int systemBars() {
+ return STATUS_BARS | NAVIGATION_BARS | CAPTION_BAR;
+ }
+
+ /**
+ * @return All inset types combined.
+ *
+ * @hide
+ */
+ public static @InsetsType int all() {
+ return 0xFFFFFFFF;
+ }
+
+ /**
+ * Checks whether the specified type is considered to be part of visible insets.
+ * @hide
+ */
+ public static boolean isVisibleInsetsType(int type,
+ @SoftInputModeFlags int softInputModeFlags) {
+ int softInputMode = softInputModeFlags & SOFT_INPUT_MASK_ADJUST;
+ return (type & Type.systemBars()) != 0
+ || (softInputMode != SOFT_INPUT_ADJUST_NOTHING && (type & Type.ime()) != 0);
+ }
+ }
+
+ /**
+ * Class that defines different sides for insets.
+ */
+ public static final class Side {
+
+ public static final int LEFT = 1 << 0;
+ public static final int TOP = 1 << 1;
+ public static final int RIGHT = 1 << 2;
+ public static final int BOTTOM = 1 << 3;
+
+ private Side() {
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {LEFT, TOP, RIGHT, BOTTOM})
+ public @interface InsetsSide {}
+
+ /**
+ * @return all four sides.
+ */
+ public static @InsetsSide int all() {
+ return LEFT | TOP | RIGHT | BOTTOM;
+ }
+ }
+}
diff --git a/android/view/WindowInsetsAnimation.java b/android/view/WindowInsetsAnimation.java
new file mode 100644
index 0000000..6576eea
--- /dev/null
+++ b/android/view/WindowInsetsAnimation.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.Insets;
+import android.view.animation.Interpolator;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Class representing an animation of a set of windows that cause insets.
+ */
+public final class WindowInsetsAnimation {
+
+ @WindowInsets.Type.InsetsType
+ private final int mTypeMask;
+ private float mFraction;
+ @Nullable
+ private final Interpolator mInterpolator;
+ private final long mDurationMillis;
+ private float mAlpha;
+
+ /**
+ * Creates a new {@link WindowInsetsAnimation} object.
+ * <p>
+ * This should only be used for testing, as usually the system creates this object for the
+ * application to listen to with {@link Callback}.
+ * </p>
+ * @param typeMask The bitmask of {@link WindowInsets.Type}s that are animating.
+ * @param interpolator The interpolator of the animation.
+ * @param durationMillis The duration of the animation in
+ * {@link java.util.concurrent.TimeUnit#MILLISECONDS}.
+ */
+ public WindowInsetsAnimation(
+ @WindowInsets.Type.InsetsType int typeMask, @Nullable Interpolator interpolator,
+ long durationMillis) {
+ mTypeMask = typeMask;
+ mInterpolator = interpolator;
+ mDurationMillis = durationMillis;
+ }
+
+ /**
+ * @return The bitmask of {@link WindowInsets.Type}s that are animating.
+ */
+ @WindowInsets.Type.InsetsType
+ public int getTypeMask() {
+ return mTypeMask;
+ }
+
+ /**
+ * Returns the raw fractional progress of this animation between
+ * start state of the animation and the end state of the animation. Note
+ * that this progress is the global progress of the animation, whereas
+ * {@link Callback#onProgress} will only dispatch the insets that may
+ * be inset with {@link WindowInsets#inset} by parents of views in the hierarchy.
+ * Progress per insets animation is global for the entire animation. One animation animates
+ * all things together (in, out, ...). If they don't animate together, we'd have
+ * multiple animations.
+ * <p>
+ * Note: In case the application is controlling the animation, the valued returned here will
+ * be the same as the application passed into
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}.
+ * </p>
+ * @return The current progress of this animation.
+ */
+ @FloatRange(from = 0f, to = 1f)
+ public float getFraction() {
+ return mFraction;
+ }
+
+ /**
+ * Returns the interpolated fractional progress of this animation between
+ * start state of the animation and the end state of the animation. Note
+ * that this progress is the global progress of the animation, whereas
+ * {@link Callback#onProgress} will only dispatch the insets that may
+ * be inset with {@link WindowInsets#inset} by parents of views in the hierarchy.
+ * Progress per insets animation is global for the entire animation. One animation animates
+ * all things together (in, out, ...). If they don't animate together, we'd have
+ * multiple animations.
+ * <p>
+ * Note: In case the application is controlling the animation, the valued returned here will
+ * be the same as the application passed into
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)},
+ * interpolated with the interpolator passed into
+ * {@link WindowInsetsController#controlWindowInsetsAnimation}.
+ * </p>
+ * <p>
+ * Note: For system-initiated animations, this will always return a valid value between 0
+ * and 1.
+ * </p>
+ * @see #getFraction() for raw fraction.
+ * @return The current interpolated progress of this animation.
+ */
+ public float getInterpolatedFraction() {
+ if (mInterpolator != null) {
+ return mInterpolator.getInterpolation(mFraction);
+ }
+ return mFraction;
+ }
+
+ /**
+ * Retrieves the interpolator used for this animation, or {@code null} if this animation
+ * doesn't follow an interpolation curved. For system-initiated animations, this will never
+ * return {@code null}.
+ *
+ * @return The interpolator used for this animation.
+ */
+ @Nullable
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * @return duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or
+ * -1 if the animation doesn't have a fixed duration.
+ */
+ public long getDurationMillis() {
+ return mDurationMillis;
+ }
+
+ /**
+ * Set fraction of the progress if {@link WindowInsets.Type} animation is
+ * controlled by the app.
+ * <p>
+ * Note: This should only be used for testing, as the system fills in the fraction for the
+ * application or the fraction that was passed into
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)} is being
+ * used.
+ * </p>
+ * @param fraction fractional progress between 0 and 1 where 0 represents hidden and
+ * zero progress and 1 represent fully shown final state.
+ * @see #getFraction()
+ */
+ public void setFraction(@FloatRange(from = 0f, to = 1f) float fraction) {
+ mFraction = fraction;
+ }
+
+ /**
+ * Retrieves the translucency of the windows that are animating.
+ *
+ * @return Alpha of windows that cause insets of type {@link WindowInsets.Type}.
+ */
+ @FloatRange(from = 0f, to = 1f)
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Sets the translucency of the windows that are animating.
+ * <p>
+ * Note: This should only be used for testing, as the system fills in the alpha for the
+ * application or the alpha that was passed into
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)} is being
+ * used.
+ * </p>
+ * @param alpha Alpha of windows that cause insets of type {@link WindowInsets.Type}.
+ * @see #getAlpha()
+ */
+ public void setAlpha(@FloatRange(from = 0f, to = 1f) float alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * Class representing the range of an {@link WindowInsetsAnimation}
+ */
+ public static final class Bounds {
+
+ private final Insets mLowerBound;
+ private final Insets mUpperBound;
+
+ public Bounds(@NonNull Insets lowerBound, @NonNull Insets upperBound) {
+ mLowerBound = lowerBound;
+ mUpperBound = upperBound;
+ }
+
+ /**
+ * Queries the lower inset bound of the animation. If the animation is about showing or
+ * hiding a window that cause insets, the lower bound is {@link Insets#NONE} and the upper
+ * bound is the same as {@link WindowInsets#getInsets(int)} for the fully shown state. This
+ * is the same as {@link WindowInsetsAnimationController#getHiddenStateInsets} and
+ * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets
+ * invoked because of an animation that originates from
+ * {@link WindowInsetsAnimationController}.
+ * <p>
+ * However, if the size of a window that causes insets is changing, these are the
+ * lower/upper bounds of that size animation.
+ * </p>
+ * There are no overlapping animations for a specific type, but there may be multiple
+ * animations running at the same time for different inset types.
+ *
+ * @see #getUpperBound()
+ * @see WindowInsetsAnimationController#getHiddenStateInsets
+ */
+ @NonNull
+ public Insets getLowerBound() {
+ return mLowerBound;
+ }
+
+ /**
+ * Queries the upper inset bound of the animation. If the animation is about showing or
+ * hiding a window that cause insets, the lower bound is {@link Insets#NONE}
+ * nd the upper bound is the same as {@link WindowInsets#getInsets(int)} for the fully
+ * shown state. This is the same as
+ * {@link WindowInsetsAnimationController#getHiddenStateInsets} and
+ * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets
+ * invoked because of an animation that originates from
+ * {@link WindowInsetsAnimationController}.
+ * <p>
+ * However, if the size of a window that causes insets is changing, these are the
+ * lower/upper bounds of that size animation.
+ * <p>
+ * There are no overlapping animations for a specific type, but there may be multiple
+ * animations running at the same time for different inset types.
+ *
+ * @see #getLowerBound()
+ * @see WindowInsetsAnimationController#getShownStateInsets
+ */
+ @NonNull
+ public Insets getUpperBound() {
+ return mUpperBound;
+ }
+
+ /**
+ * Insets both the lower and upper bound by the specified insets. This is to be used in
+ * {@link Callback#onStart} to indicate that a part of the insets has
+ * been used to offset or clip its children, and the children shouldn't worry about that
+ * part anymore.
+ *
+ * @param insets The amount to inset.
+ * @return A copy of this instance inset in the given directions.
+ * @see WindowInsets#inset
+ * @see Callback#onStart
+ */
+ @NonNull
+ public Bounds inset(@NonNull Insets insets) {
+ return new Bounds(
+ // TODO: refactor so that WindowInsets.insetInsets() is in a more appropriate
+ // place eventually.
+ WindowInsets.insetInsets(
+ mLowerBound, insets.left, insets.top, insets.right, insets.bottom),
+ WindowInsets.insetInsets(
+ mUpperBound, insets.left, insets.top, insets.right, insets.bottom));
+ }
+
+ @Override
+ public String toString() {
+ return "Bounds{lower=" + mLowerBound + " upper=" + mUpperBound + "}";
+ }
+ }
+
+ /**
+ * Interface that allows the application to listen to animation events for windows that cause
+ * insets.
+ */
+ @SuppressLint("CallbackMethodName") // TODO(b/149430296) Should be on method, not class.
+ public abstract static class Callback {
+
+ /**
+ * Return value for {@link #getDispatchMode()}: Dispatching of animation events should
+ * stop at this level in the view hierarchy, and no animation events should be dispatch to
+ * the subtree of the view hierarchy.
+ */
+ public static final int DISPATCH_MODE_STOP = 0;
+
+ /**
+ * Return value for {@link #getDispatchMode()}: Dispatching of animation events should
+ * continue in the view hierarchy.
+ */
+ public static final int DISPATCH_MODE_CONTINUE_ON_SUBTREE = 1;
+
+ /** @hide */
+ @IntDef(prefix = { "DISPATCH_MODE_" }, value = {
+ DISPATCH_MODE_STOP,
+ DISPATCH_MODE_CONTINUE_ON_SUBTREE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DispatchMode {}
+
+ @DispatchMode
+ private final int mDispatchMode;
+
+ /**
+ * Creates a new {@link WindowInsetsAnimation} callback with the given
+ * {@link #getDispatchMode() dispatch mode}.
+ *
+ * @param dispatchMode The dispatch mode for this callback. See {@link #getDispatchMode()}.
+ */
+ public Callback(@DispatchMode int dispatchMode) {
+ mDispatchMode = dispatchMode;
+ }
+
+ /**
+ * Retrieves the dispatch mode of this listener. Dispatch of the all animation events is
+ * hierarchical: It will starts at the root of the view hierarchy and then traverse it and
+ * invoke the callback of the specific {@link View} that is being traversed.
+ * The method may return either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that
+ * animation events should be propagated to the subtree of the view hierarchy, or
+ * {@link #DISPATCH_MODE_STOP} to stop dispatching. In that case, all animation callbacks
+ * related to the animation passed in will be stopped from propagating to the subtree of the
+ * hierarchy.
+ * <p>
+ * Also note that {@link #DISPATCH_MODE_STOP} behaves the same way as
+ * returning {@link WindowInsets#CONSUMED} during the regular insets dispatch in
+ * {@link View#onApplyWindowInsets}.
+ *
+ * @return Either {@link #DISPATCH_MODE_CONTINUE_ON_SUBTREE} to indicate that dispatching of
+ * animation events will continue to the subtree of the view hierarchy, or
+ * {@link #DISPATCH_MODE_STOP} to indicate that animation events will stop
+ * dispatching.
+ */
+ @DispatchMode
+ @SuppressLint("CallbackMethodName") // TODO(b/149430296) False positive: not a callback.
+ public final int getDispatchMode() {
+ return mDispatchMode;
+ }
+
+ /**
+ * Called when an insets animation is about to start and before the views have been laid out
+ * in the end state of the animation. The ordering of events during an insets animation is
+ * the following:
+ * <p>
+ * <ul>
+ * <li>Application calls {@link WindowInsetsController#hide(int)},
+ * {@link WindowInsetsController#show(int)},
+ * {@link WindowInsetsController#controlWindowInsetsAnimation}</li>
+ * <li>onPrepare is called on the view hierarchy listeners</li>
+ * <li>{@link View#onApplyWindowInsets} will be called with the end state of the
+ * animation</li>
+ * <li>View hierarchy gets laid out according to the changes the application has
+ * requested due to the new insets being dispatched</li>
+ * <li>{@link #onStart} is called <em>before</em> the view
+ * hierarchy gets drawn in the new laid out state</li>
+ * <li>{@link #onProgress} is called immediately after with the animation start
+ * state</li>
+ * <li>The frame gets drawn.</li>
+ * </ul>
+ * <p>
+ * This ordering allows the application to inspect the end state after the animation has
+ * finished, and then revert to the starting state of the animation in the first
+ * {@link #onProgress} callback by using post-layout view properties like {@link View#setX}
+ * and related methods.
+ *
+ * <p>Note that the animation might be cancelled before {@link #onStart} is dispatched. On
+ * {@link android.os.Build.VERSION_CODES#S S} and later, {@link #onEnd} is immediately
+ * dispatched without an {@link #onStart} in that case.
+ * On {@link android.os.Build.VERSION_CODES#R R}, no callbacks are dispatched after
+ * {@code #onPrepare} for such an animation.
+ *
+ * <p>
+ * Note: If the animation is application controlled by using
+ * {@link WindowInsetsController#controlWindowInsetsAnimation}, the end state of the
+ * animation is undefined as the application may decide on the end state only by passing in
+ * {@code shown} parameter when calling {@link WindowInsetsAnimationController#finish}. In
+ * this situation, the system will dispatch the insets in the opposite visibility state
+ * before the animation starts. Example: When controlling the input method with
+ * {@link WindowInsetsController#controlWindowInsetsAnimation} and the input method is
+ * currently showing, {@link View#onApplyWindowInsets} will receive a {@link WindowInsets}
+ * instance for which {@link WindowInsets#isVisible} will return {@code false} for
+ * {@link WindowInsets.Type#ime}.
+ *
+ * @param animation The animation that is about to start.
+ */
+ public void onPrepare(@NonNull WindowInsetsAnimation animation) {
+ }
+
+ /**
+ * Called when an insets animation gets started.
+ * <p>
+ * Note that, like {@link #onProgress}, dispatch of the animation start event is
+ * hierarchical: It will starts at the root of the view hierarchy and then traverse it
+ * and invoke the callback of the specific {@link View} that is being traversed.
+ * The method may return a modified
+ * instance of the bounds by calling {@link Bounds#inset} to indicate that a part of
+ * the insets have been used to offset or clip its children, and the children shouldn't
+ * worry about that part anymore. Furthermore, if {@link #getDispatchMode()} returns
+ * {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore.
+ *
+ * @param animation The animation that is about to start.
+ * @param bounds The bounds in which animation happens.
+ * @return The animation representing the part of the insets that should be dispatched to
+ * the subtree of the hierarchy.
+ */
+ @NonNull
+ public Bounds onStart(
+ @NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds) {
+ return bounds;
+ }
+
+ /**
+ * Called when the insets change as part of running an animation. Note that even if multiple
+ * animations for different types are running, there will only be one progress callback per
+ * frame. The {@code insets} passed as an argument represents the overall state and will
+ * include all types, regardless of whether they are animating or not.
+ * <p>
+ * Note that insets dispatch is hierarchical: It will start at the root of the view
+ * hierarchy, and then traverse it and invoke the callback of the specific {@link View}
+ * being traversed. The method may return a modified instance by calling
+ * {@link WindowInsets#inset(int, int, int, int)} to indicate that a part of the insets have
+ * been used to offset or clip its children, and the children shouldn't worry about that
+ * part anymore. Furthermore, if {@link #getDispatchMode()} returns
+ * {@link #DISPATCH_MODE_STOP}, children of this view will not receive the callback anymore.
+ *
+ * @param insets The current insets.
+ * @param runningAnimations The currently running animations.
+ * @return The insets to dispatch to the subtree of the hierarchy.
+ */
+ @NonNull
+ public abstract WindowInsets onProgress(@NonNull WindowInsets insets,
+ @NonNull List<WindowInsetsAnimation> runningAnimations);
+
+ /**
+ * Called when an insets animation has ended.
+ *
+ * @param animation The animation that has ended. This will be the same instance
+ * as passed into {@link #onStart}
+ */
+ public void onEnd(@NonNull WindowInsetsAnimation animation) {
+ }
+
+ }
+}
diff --git a/android/view/WindowInsetsAnimationControlListener.java b/android/view/WindowInsetsAnimationControlListener.java
new file mode 100644
index 0000000..140a9a8
--- /dev/null
+++ b/android/view/WindowInsetsAnimationControlListener.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.EditorInfo;
+
+/**
+ * Listener that encapsulates a request to
+ * {@link WindowInsetsController#controlWindowInsetsAnimation}.
+ *
+ * <p>
+ * Insets can be controlled with the supplied {@link WindowInsetsAnimationController} from
+ * {@link #onReady} until either {@link #onFinished} or {@link #onCancelled}.
+ *
+ * <p>
+ * Once the control over insets is finished or cancelled, it will not be regained until a new
+ * request to {@link WindowInsetsController#controlWindowInsetsAnimation} is made.
+ *
+ * <p>
+ * The request to control insets can fail immediately. In that case {@link #onCancelled} will be
+ * invoked without a preceding {@link #onReady}.
+ *
+ * @see WindowInsetsController#controlWindowInsetsAnimation
+ */
+public interface WindowInsetsAnimationControlListener {
+
+ /**
+ * Called when the animation is ready to be controlled. This may be delayed when the IME needs
+ * to redraw because of an {@link EditorInfo} change, or when the window is starting up.
+ *
+ * @param controller The controller to control the inset animation.
+ * @param types The {@link WindowInsets.Type}s it was able to gain control over. Note that this
+ * may be different than the types passed into
+ * {@link WindowInsetsController#controlWindowInsetsAnimation} in case the window
+ * wasn't able to gain the controls because it wasn't the IME target or not
+ * currently the window that's controlling the system bars.
+ * @see WindowInsetsAnimationController#isReady
+ */
+ void onReady(@NonNull WindowInsetsAnimationController controller, @InsetsType int types);
+
+ /**
+ * Called when the request for control over the insets has
+ * {@link WindowInsetsAnimationController#finish finished}.
+ *
+ * Once this callback is invoked, the supplied {@link WindowInsetsAnimationController}
+ * is no longer {@link WindowInsetsAnimationController#isReady() ready}.
+ *
+ * Control will not be regained until a new request
+ * to {@link WindowInsetsController#controlWindowInsetsAnimation} is made.
+ *
+ * @param controller the controller which has finished.
+ * @see WindowInsetsAnimationController#isFinished
+ */
+ void onFinished(@NonNull WindowInsetsAnimationController controller);
+
+ /**
+ * Called when the request for control over the insets has been cancelled, either
+ * because the {@link android.os.CancellationSignal} associated with the
+ * {@link WindowInsetsController#controlWindowInsetsAnimation request} has been invoked, or
+ * the window has lost control over the insets (e.g. because it lost focus).
+ *
+ * Once this callback is invoked, the supplied {@link WindowInsetsAnimationController}
+ * is no longer {@link WindowInsetsAnimationController#isReady() ready}.
+ *
+ * Control will not be regained until a new request
+ * to {@link WindowInsetsController#controlWindowInsetsAnimation} is made.
+ *
+ * @param controller the controller which has been cancelled, or null if the request
+ * was cancelled before {@link #onReady} was invoked.
+ * @see WindowInsetsAnimationController#isCancelled
+ */
+ void onCancelled(@Nullable WindowInsetsAnimationController controller);
+}
diff --git a/android/view/WindowInsetsAnimationController.java b/android/view/WindowInsetsAnimationController.java
new file mode 100644
index 0000000..6578e9b
--- /dev/null
+++ b/android/view/WindowInsetsAnimationController.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.Insets;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.WindowInsetsAnimation.Bounds;
+
+/**
+ * Controller for app-driven animation of system windows.
+ * <p>
+ * {@code WindowInsetsAnimationController} lets apps animate system windows such as
+ * the {@link android.inputmethodservice.InputMethodService IME}. The animation is
+ * synchronized, such that changes the system windows and the app's current frame
+ * are rendered at the same time.
+ * <p>
+ * Control is obtained through {@link WindowInsetsController#controlWindowInsetsAnimation}.
+ */
+@SuppressLint("NotCloseable")
+public interface WindowInsetsAnimationController {
+
+ /**
+ * Retrieves the {@link Insets} when the windows this animation is controlling are fully hidden.
+ * <p>
+ * Note that these insets are always relative to the window, which is the same as being relative
+ * to {@link View#getRootView}
+ * <p>
+ * If there are any animation listeners registered, this value is the same as
+ * {@link Bounds#getLowerBound()} that is being be passed into the root view of the
+ * hierarchy.
+ *
+ * @return Insets when the windows this animation is controlling are fully hidden.
+ *
+ * @see Bounds#getLowerBound()
+ */
+ @NonNull Insets getHiddenStateInsets();
+
+ /**
+ * Retrieves the {@link Insets} when the windows this animation is controlling are fully shown.
+ * <p>
+ * Note that these insets are always relative to the window, which is the same as being relative
+ * to {@link View#getRootView}
+ * <p>
+ * If there are any animation listeners registered, this value is the same as
+ * {@link Bounds#getUpperBound()} that is being passed into the root view of hierarchy.
+ *
+ * @return Insets when the windows this animation is controlling are fully shown.
+ *
+ * @see Bounds#getUpperBound()
+ */
+ @NonNull Insets getShownStateInsets();
+
+ /**
+ * Retrieves the current insets.
+ * <p>
+ * Note that these insets are always relative to the window, which is the same as
+ * being relative
+ * to {@link View#getRootView}
+ * @return The current insets on the currently showing frame. These insets will change as the
+ * animation progresses to reflect the current insets provided by the controlled window.
+ */
+ @NonNull Insets getCurrentInsets();
+
+ /**
+ * Returns the progress as previously set by {@code fraction} in {@link #setInsetsAndAlpha}
+ *
+ * @return the progress of the animation, where {@code 0} is fully hidden and {@code 1} is
+ * fully shown.
+ * <p>
+ * Note: this value represents raw overall progress of the animation
+ * i.e. the combined progress of insets and alpha.
+ * <p>
+ */
+ @FloatRange(from = 0f, to = 1f)
+ float getCurrentFraction();
+
+ /**
+ * Current alpha value of the window.
+ * @return float value between 0 and 1.
+ */
+ float getCurrentAlpha();
+
+ /**
+ * @return The {@link WindowInsets.Type}s this object is currently controlling.
+ */
+ @InsetsType int getTypes();
+
+ /**
+ * Modifies the insets for the frame being drawn by indirectly moving the windows around in the
+ * system that are causing window insets.
+ * <p>
+ * Note that these insets are always relative to the window, which is the same as being relative
+ * to {@link View#getRootView}
+ * <p>
+ * Also note that this will <b>not</b> inform the view system of a full inset change via
+ * {@link View#dispatchApplyWindowInsets} in order to avoid a full layout pass during the
+ * animation. If you'd like to animate views during a window inset animation, register a
+ * {@link WindowInsetsAnimation.Callback} by calling
+ * {@link View#setWindowInsetsAnimationCallback(WindowInsetsAnimation.Callback)} that will be
+ * notified about any insets change via {@link WindowInsetsAnimation.Callback#onProgress} during
+ * the animation.
+ * <p>
+ * {@link View#dispatchApplyWindowInsets} will instead be called once the animation has
+ * finished, i.e. once {@link #finish} has been called.
+ * Note: If there are no insets, alpha animation is still applied.
+ *
+ * @param insets The new insets to apply. Based on the requested insets, the system will
+ * calculate the positions of the windows in the system causing insets such that
+ * the resulting insets of that configuration will match the passed in parameter.
+ * Note that these insets are being clamped to the range from
+ * {@link #getHiddenStateInsets} to {@link #getShownStateInsets}.
+ * If you intend on changing alpha only, pass null or {@link #getCurrentInsets()}.
+ * @param alpha The new alpha to apply to the inset side.
+ * @param fraction instantaneous animation progress. This value is dispatched to
+ * {@link WindowInsetsAnimation.Callback}.
+ *
+ * @see WindowInsetsAnimation.Callback
+ * @see View#setWindowInsetsAnimationCallback(WindowInsetsAnimation.Callback)
+ */
+ void setInsetsAndAlpha(@Nullable Insets insets, @FloatRange(from = 0f, to = 1f) float alpha,
+ @FloatRange(from = 0f, to = 1f) float fraction);
+
+ /**
+ * Finishes the animation, and leaves the windows shown or hidden.
+ * <p>
+ * After invoking {@link #finish}, this instance is no longer {@link #isReady ready}.
+ * <p>
+ * Note: Finishing an animation implicitly {@link #setInsetsAndAlpha sets insets and alpha}
+ * according to the requested end state without any further animation.
+ *
+ * @param shown if {@code true}, the windows will be shown after finishing the
+ * animation. Otherwise they will be hidden.
+ */
+ void finish(boolean shown);
+
+ /**
+ * Returns whether this instance is ready to be used to control window insets.
+ * <p>
+ * Instances are ready when passed in {@link WindowInsetsAnimationControlListener#onReady}
+ * and stop being ready when it is either {@link #isFinished() finished} or
+ * {@link #isCancelled() cancelled}.
+ *
+ * @return {@code true} if the instance is ready, {@code false} otherwise.
+ */
+ default boolean isReady() {
+ return !isFinished() && !isCancelled();
+ }
+
+ /**
+ * Returns whether this instance has been finished by a call to {@link #finish}.
+ *
+ * @see WindowInsetsAnimationControlListener#onFinished
+ * @return {@code true} if the instance is finished, {@code false} otherwise.
+ */
+ boolean isFinished();
+
+ /**
+ * Returns whether this instance has been cancelled by the system, or by invoking the
+ * {@link android.os.CancellationSignal} passed into
+ * {@link WindowInsetsController#controlWindowInsetsAnimation}.
+ *
+ * @see WindowInsetsAnimationControlListener#onCancelled
+ * @return {@code true} if the instance is cancelled, {@code false} otherwise.
+ */
+ boolean isCancelled();
+
+ /**
+ * @hide
+ * @return {@code true} when controller controls IME and IME has no insets (floating,
+ * fullscreen or non-overlapping).
+ */
+ boolean hasZeroInsetsIme();
+}
diff --git a/android/view/WindowInsetsController.java b/android/view/WindowInsetsController.java
new file mode 100644
index 0000000..227b9f4
--- /dev/null
+++ b/android/view/WindowInsetsController.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.inputmethodservice.InputMethodService;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.animation.Interpolator;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Interface to control windows that generate insets.
+ */
+public interface WindowInsetsController {
+
+ /**
+ * Makes status bars become opaque with solid dark background and light foreground.
+ * @hide
+ */
+ int APPEARANCE_OPAQUE_STATUS_BARS = 1;
+
+ /**
+ * Makes navigation bars become opaque with solid dark background and light foreground.
+ * @hide
+ */
+ int APPEARANCE_OPAQUE_NAVIGATION_BARS = 1 << 1;
+
+ /**
+ * Makes items on system bars become less noticeable without changing the layout of the bars.
+ * @hide
+ */
+ int APPEARANCE_LOW_PROFILE_BARS = 1 << 2;
+
+ /**
+ * Changes the foreground color for light status bars so that the items on the bar can be read
+ * clearly.
+ */
+ int APPEARANCE_LIGHT_STATUS_BARS = 1 << 3;
+
+ /**
+ * Changes the foreground color for light navigation bars so that the items on the bar can be
+ * read clearly.
+ */
+ int APPEARANCE_LIGHT_NAVIGATION_BARS = 1 << 4;
+
+ /**
+ * Makes status bars semi-transparent with dark background and light foreground.
+ * @hide
+ */
+ int APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS = 1 << 5;
+
+ /**
+ * Makes navigation bars semi-transparent with dark background and light foreground.
+ * @hide
+ */
+ int APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS = 1 << 6;
+
+ /**
+ * Determines the appearance of system bars.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {APPEARANCE_OPAQUE_STATUS_BARS, APPEARANCE_OPAQUE_NAVIGATION_BARS,
+ APPEARANCE_LOW_PROFILE_BARS, APPEARANCE_LIGHT_STATUS_BARS,
+ APPEARANCE_LIGHT_NAVIGATION_BARS, APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
+ APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS})
+ @interface Appearance {
+ }
+
+ /**
+ * Option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly shown on any
+ * user interaction on the corresponding display if navigation bars are hidden by
+ * {@link #hide(int)} or
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}.
+ * @deprecated This is not supported on Android {@link Build.VERSION_CODES#S} and later. Use
+ * {@link #BEHAVIOR_DEFAULT} or {@link #BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}
+ * instead.
+ */
+ @Deprecated
+ int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0;
+
+ /**
+ * The default option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
+ * interactive when hiding navigation bars by calling {@link #hide(int)} or
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}.
+ *
+ * <p>When system bars are hidden in this mode, they can be revealed with system gestures, such
+ * as swiping from the edge of the screen where the bar is hidden from.</p>
+ *
+ * <p>When the gesture navigation is enabled, the system gestures can be triggered regardless
+ * the visibility of system bars.</p>
+ */
+ int BEHAVIOR_DEFAULT = 1;
+
+ /**
+ * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive when
+ * hiding navigation bars by calling {@link #hide(int)} or
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}.
+ *
+ * <p>When system bars are hidden in this mode, they can be revealed with system gestures, such
+ * as swiping from the edge of the screen where the bar is hidden from.</p>
+ * @deprecated Use {@link #BEHAVIOR_DEFAULT} instead.
+ */
+ @Deprecated
+ int BEHAVIOR_SHOW_BARS_BY_SWIPE = BEHAVIOR_DEFAULT;
+
+ /**
+ * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive when
+ * hiding navigation bars by calling {@link #hide(int)} or
+ * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}.
+ *
+ * <p>When system bars are hidden in this mode, they can be revealed temporarily with system
+ * gestures, such as swiping from the edge of the screen where the bar is hidden from. These
+ * transient system bars will overlay app’s content, may have some degree of transparency, and
+ * will automatically hide after a short timeout.</p>
+ */
+ int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2;
+
+ /**
+ * Determines the behavior of system bars when hiding them by calling {@link #hide}.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {BEHAVIOR_DEFAULT, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
+ @interface Behavior {
+ }
+
+ /**
+ * Makes a set of windows that cause insets appear on screen.
+ * <p>
+ * Note that if the window currently doesn't have control over a certain type, it will apply the
+ * change as soon as the window gains control. The app can listen to the event by observing
+ * {@link View#onApplyWindowInsets} and checking visibility with {@link WindowInsets#isVisible}.
+ *
+ * @param types A bitmask of {@link WindowInsets.Type} specifying what windows the app
+ * would like to make appear on screen.
+ */
+ void show(@InsetsType int types);
+
+ /**
+ * Makes a set of windows causing insets disappear.
+ * <p>
+ * Note that if the window currently doesn't have control over a certain type, it will apply the
+ * change as soon as the window gains control. The app can listen to the event by observing
+ * {@link View#onApplyWindowInsets} and checking visibility with {@link WindowInsets#isVisible}.
+ *
+ * @param types A bitmask of {@link WindowInsets.Type} specifying what windows the app
+ * would like to make disappear.
+ */
+ void hide(@InsetsType int types);
+
+ /**
+ * Lets the application control window inset animations in a frame-by-frame manner by modifying
+ * the position of the windows in the system causing insets directly.
+ *
+ * @param types The {@link WindowInsets.Type}s the application has requested to control.
+ * @param durationMillis Duration of animation in
+ * {@link java.util.concurrent.TimeUnit#MILLISECONDS}, or -1 if the
+ * animation doesn't have a predetermined duration. This value will be
+ * passed to {@link WindowInsetsAnimation#getDurationMillis()}
+ * @param interpolator The interpolator used for this animation, or {@code null} if this
+ * animation doesn't follow an interpolation curve. This value will be
+ * passed to {@link WindowInsetsAnimation#getInterpolator()} and used to
+ * calculate {@link WindowInsetsAnimation#getInterpolatedFraction()}.
+ * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when the
+ * windows are ready to be controlled, among other callbacks.
+ * @param cancellationSignal A cancellation signal that the caller can use to cancel the
+ * request to obtain control, or once they have control, to cancel the
+ * control.
+ * @see WindowInsetsAnimation#getFraction()
+ * @see WindowInsetsAnimation#getInterpolatedFraction()
+ * @see WindowInsetsAnimation#getInterpolator()
+ * @see WindowInsetsAnimation#getDurationMillis()
+ */
+ void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
+ @Nullable Interpolator interpolator,
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull WindowInsetsAnimationControlListener listener);
+
+ /**
+ * Controls the appearance of system bars.
+ * <p>
+ * For example, the following statement adds {@link #APPEARANCE_LIGHT_STATUS_BARS}:
+ * <pre>
+ * setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS)
+ * </pre>
+ * And the following statement clears it:
+ * <pre>
+ * setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS)
+ * </pre>
+ *
+ * @param appearance Bitmask of appearance flags.
+ * @param mask Specifies which flags of appearance should be changed.
+ * @see #getSystemBarsAppearance
+ */
+ void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask);
+
+ /**
+ * Retrieves the requested appearance of system bars.
+ *
+ * @return The requested bitmask of system bar appearance controlled by this window.
+ * @see #setSystemBarsAppearance(int, int)
+ */
+ @Appearance int getSystemBarsAppearance();
+
+ /**
+ * Notify the caption insets height change. The information will be used on the client side to,
+ * make sure the InsetsState has the correct caption insets.
+ *
+ * @param height the height of caption bar insets.
+ * @hide
+ */
+ void setCaptionInsetsHeight(int height);
+
+ /**
+ * Controls the behavior of system bars.
+ *
+ * @param behavior Determines how the bars behave when being hidden by the application.
+ * @see #getSystemBarsBehavior
+ */
+ void setSystemBarsBehavior(@Behavior int behavior);
+
+ /**
+ * Retrieves the requested behavior of system bars.
+ *
+ * @return the system bar behavior controlled by this window.
+ * @see #setSystemBarsBehavior(int)
+ */
+ @Behavior int getSystemBarsBehavior();
+
+ /**
+ * Disables or enables the animations.
+ *
+ * @hide
+ */
+ void setAnimationsDisabled(boolean disable);
+
+ /**
+ * @hide
+ */
+ InsetsState getState();
+
+ /**
+ * @return Whether the specified insets source is currently requested to be visible by the
+ * application.
+ * @hide
+ */
+ boolean isRequestedVisible(@InternalInsetsType int type);
+
+ /**
+ * Adds a {@link OnControllableInsetsChangedListener} to the window insets controller.
+ *
+ * @param listener The listener to add.
+ *
+ * @see OnControllableInsetsChangedListener
+ * @see #removeOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
+ */
+ void addOnControllableInsetsChangedListener(
+ @NonNull OnControllableInsetsChangedListener listener);
+
+ /**
+ * Removes a {@link OnControllableInsetsChangedListener} from the window insets controller.
+ *
+ * @param listener The listener to remove.
+ *
+ * @see OnControllableInsetsChangedListener
+ * @see #addOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
+ */
+ void removeOnControllableInsetsChangedListener(
+ @NonNull OnControllableInsetsChangedListener listener);
+
+ /**
+ * Listener to be notified when the set of controllable {@link WindowInsets.Type} controlled by
+ * a {@link WindowInsetsController} changes.
+ * <p>
+ * Once a {@link WindowInsets.Type} becomes controllable, the app will be able to control the
+ * window that is causing this type of insets by calling {@link #controlWindowInsetsAnimation}.
+ * <p>
+ * Note: When listening to controllability of the {@link Type#ime},
+ * {@link #controlWindowInsetsAnimation} may still fail in case the {@link InputMethodService}
+ * decides to cancel the show request. This could happen when there is a hardware keyboard
+ * attached.
+ *
+ * @see #addOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
+ * @see #removeOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
+ */
+ interface OnControllableInsetsChangedListener {
+
+ /**
+ * Called when the set of controllable {@link WindowInsets.Type} changes.
+ *
+ * @param controller The controller for which the set of controllable
+ * {@link WindowInsets.Type}s are changing.
+ * @param typeMask Bitwise type-mask of the {@link WindowInsets.Type}s the controller is
+ * currently able to control.
+ */
+ void onControllableInsetsChanged(@NonNull WindowInsetsController controller,
+ @InsetsType int typeMask);
+ }
+}
diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java
new file mode 100644
index 0000000..55beae0
--- /dev/null
+++ b/android/view/WindowManager.java
@@ -0,0 +1,4621 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT;
+import static android.view.View.STATUS_BAR_DISABLE_BACK;
+import static android.view.View.STATUS_BAR_DISABLE_CLOCK;
+import static android.view.View.STATUS_BAR_DISABLE_EXPAND;
+import static android.view.View.STATUS_BAR_DISABLE_HOME;
+import static android.view.View.STATUS_BAR_DISABLE_NOTIFICATION_ALERTS;
+import static android.view.View.STATUS_BAR_DISABLE_NOTIFICATION_ICONS;
+import static android.view.View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER;
+import static android.view.View.STATUS_BAR_DISABLE_RECENT;
+import static android.view.View.STATUS_BAR_DISABLE_SEARCH;
+import static android.view.View.STATUS_BAR_DISABLE_SYSTEM_INFO;
+import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
+import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE;
+import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE;
+import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
+import static android.view.WindowInsets.Side.BOTTOM;
+import static android.view.WindowInsets.Side.LEFT;
+import static android.view.WindowInsets.Side.RIGHT;
+import static android.view.WindowInsets.Side.TOP;
+import static android.view.WindowInsets.Type.CAPTION_BAR;
+import static android.view.WindowInsets.Type.IME;
+import static android.view.WindowInsets.Type.MANDATORY_SYSTEM_GESTURES;
+import static android.view.WindowInsets.Type.NAVIGATION_BARS;
+import static android.view.WindowInsets.Type.STATUS_BARS;
+import static android.view.WindowInsets.Type.SYSTEM_GESTURES;
+import static android.view.WindowInsets.Type.TAPPABLE_ELEMENT;
+import static android.view.WindowInsets.Type.WINDOW_DECOR;
+import static android.view.WindowLayoutParamsProto.ALPHA;
+import static android.view.WindowLayoutParamsProto.APPEARANCE;
+import static android.view.WindowLayoutParamsProto.BEHAVIOR;
+import static android.view.WindowLayoutParamsProto.BUTTON_BRIGHTNESS;
+import static android.view.WindowLayoutParamsProto.COLOR_MODE;
+import static android.view.WindowLayoutParamsProto.FIT_IGNORE_VISIBILITY;
+import static android.view.WindowLayoutParamsProto.FIT_INSETS_SIDES;
+import static android.view.WindowLayoutParamsProto.FIT_INSETS_TYPES;
+import static android.view.WindowLayoutParamsProto.FLAGS;
+import static android.view.WindowLayoutParamsProto.FORMAT;
+import static android.view.WindowLayoutParamsProto.GRAVITY;
+import static android.view.WindowLayoutParamsProto.HAS_SYSTEM_UI_LISTENERS;
+import static android.view.WindowLayoutParamsProto.HEIGHT;
+import static android.view.WindowLayoutParamsProto.HORIZONTAL_MARGIN;
+import static android.view.WindowLayoutParamsProto.INPUT_FEATURE_FLAGS;
+import static android.view.WindowLayoutParamsProto.PREFERRED_REFRESH_RATE;
+import static android.view.WindowLayoutParamsProto.PRIVATE_FLAGS;
+import static android.view.WindowLayoutParamsProto.ROTATION_ANIMATION;
+import static android.view.WindowLayoutParamsProto.SCREEN_BRIGHTNESS;
+import static android.view.WindowLayoutParamsProto.SOFT_INPUT_MODE;
+import static android.view.WindowLayoutParamsProto.SUBTREE_SYSTEM_UI_VISIBILITY_FLAGS;
+import static android.view.WindowLayoutParamsProto.SYSTEM_UI_VISIBILITY_FLAGS;
+import static android.view.WindowLayoutParamsProto.TYPE;
+import static android.view.WindowLayoutParamsProto.USER_ACTIVITY_TIMEOUT;
+import static android.view.WindowLayoutParamsProto.VERTICAL_MARGIN;
+import static android.view.WindowLayoutParamsProto.WIDTH;
+import static android.view.WindowLayoutParamsProto.WINDOW_ANIMATIONS;
+import static android.view.WindowLayoutParamsProto.X;
+import static android.view.WindowLayoutParamsProto.Y;
+
+import android.Manifest.permission;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.app.KeyguardManager;
+import android.app.Presentation;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.Gravity.GravityFlags;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.WindowInsets.Side;
+import android.view.WindowInsets.Side.InsetsSide;
+import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * The interface that apps use to talk to the window manager.
+ * </p><p>
+ * Each window manager instance is bound to a particular {@link Display}.
+ * To obtain a {@link WindowManager} for a different display, use
+ * {@link Context#createDisplayContext} to obtain a {@link Context} for that
+ * display, then use <code>Context.getSystemService(Context.WINDOW_SERVICE)</code>
+ * to get the WindowManager.
+ * </p><p>
+ * The simplest way to show a window on another display is to create a
+ * {@link Presentation}. The presentation will automatically obtain a
+ * {@link WindowManager} and {@link Context} for that display.
+ * </p>
+ */
+@SystemService(Context.WINDOW_SERVICE)
+public interface WindowManager extends ViewManager {
+
+ /** @hide */
+ int DOCKED_INVALID = -1;
+ /** @hide */
+ int DOCKED_LEFT = 1;
+ /** @hide */
+ int DOCKED_TOP = 2;
+ /** @hide */
+ int DOCKED_RIGHT = 3;
+ /** @hide */
+ int DOCKED_BOTTOM = 4;
+
+ /** @hide */
+ String INPUT_CONSUMER_PIP = "pip_input_consumer";
+ /** @hide */
+ String INPUT_CONSUMER_NAVIGATION = "nav_input_consumer";
+ /** @hide */
+ String INPUT_CONSUMER_WALLPAPER = "wallpaper_input_consumer";
+ /** @hide */
+ String INPUT_CONSUMER_RECENTS_ANIMATION = "recents_animation_input_consumer";
+
+ /** @hide */
+ int SHELL_ROOT_LAYER_DIVIDER = 0;
+ /** @hide */
+ int SHELL_ROOT_LAYER_PIP = 1;
+
+ /**
+ * Declares the layer the shell root will belong to. This is for z-ordering.
+ * @hide
+ */
+ @IntDef(prefix = { "SHELL_ROOT_LAYER_" }, value = {
+ SHELL_ROOT_LAYER_DIVIDER,
+ SHELL_ROOT_LAYER_PIP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ShellRootLayer {}
+
+ /**
+ * Not set up for a transition.
+ * @hide
+ */
+ int TRANSIT_OLD_UNSET = -1;
+
+ /**
+ * No animation for transition.
+ * @hide
+ */
+ int TRANSIT_OLD_NONE = 0;
+
+ /**
+ * A window in a new activity is being opened on top of an existing one in the same task.
+ * @hide
+ */
+ int TRANSIT_OLD_ACTIVITY_OPEN = 6;
+
+ /**
+ * The window in the top-most activity is being closed to reveal the previous activity in the
+ * same task.
+ * @hide
+ */
+ int TRANSIT_OLD_ACTIVITY_CLOSE = 7;
+
+ /**
+ * A window in a new task is being opened on top of an existing one in another activity's task.
+ * @hide
+ */
+ int TRANSIT_OLD_TASK_OPEN = 8;
+
+ /**
+ * A window in the top-most activity is being closed to reveal the previous activity in a
+ * different task.
+ * @hide
+ */
+ int TRANSIT_OLD_TASK_CLOSE = 9;
+
+ /**
+ * A window in an existing task is being displayed on top of an existing one in another
+ * activity's task.
+ * @hide
+ */
+ int TRANSIT_OLD_TASK_TO_FRONT = 10;
+
+ /**
+ * A window in an existing task is being put below all other tasks.
+ * @hide
+ */
+ int TRANSIT_OLD_TASK_TO_BACK = 11;
+
+ /**
+ * A window in a new activity that doesn't have a wallpaper is being opened on top of one that
+ * does, effectively closing the wallpaper.
+ * @hide
+ */
+ int TRANSIT_OLD_WALLPAPER_CLOSE = 12;
+
+ /**
+ * A window in a new activity that does have a wallpaper is being opened on one that didn't,
+ * effectively opening the wallpaper.
+ * @hide
+ */
+ int TRANSIT_OLD_WALLPAPER_OPEN = 13;
+
+ /**
+ * A window in a new activity is being opened on top of an existing one, and both are on top
+ * of the wallpaper.
+ * @hide
+ */
+ int TRANSIT_OLD_WALLPAPER_INTRA_OPEN = 14;
+
+ /**
+ * The window in the top-most activity is being closed to reveal the previous activity, and
+ * both are on top of the wallpaper.
+ * @hide
+ */
+ int TRANSIT_OLD_WALLPAPER_INTRA_CLOSE = 15;
+
+ /**
+ * A window in a new task is being opened behind an existing one in another activity's task.
+ * The new window will show briefly and then be gone.
+ * @hide
+ */
+ int TRANSIT_OLD_TASK_OPEN_BEHIND = 16;
+
+ /**
+ * An activity is being relaunched (e.g. due to configuration change).
+ * @hide
+ */
+ int TRANSIT_OLD_ACTIVITY_RELAUNCH = 18;
+
+ /**
+ * Keyguard is going away.
+ * @hide
+ */
+ int TRANSIT_OLD_KEYGUARD_GOING_AWAY = 20;
+
+ /**
+ * Keyguard is going away with showing an activity behind that requests wallpaper.
+ * @hide
+ */
+ int TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER = 21;
+
+ /**
+ * Keyguard is being occluded.
+ * @hide
+ */
+ int TRANSIT_OLD_KEYGUARD_OCCLUDE = 22;
+
+ /**
+ * Keyguard is being unoccluded.
+ * @hide
+ */
+ int TRANSIT_OLD_KEYGUARD_UNOCCLUDE = 23;
+
+ /**
+ * A translucent activity is being opened.
+ * @hide
+ */
+ int TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN = 24;
+
+ /**
+ * A translucent activity is being closed.
+ * @hide
+ */
+ int TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE = 25;
+
+ /**
+ * A crashing activity is being closed.
+ * @hide
+ */
+ int TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE = 26;
+
+ /**
+ * A task is changing windowing modes
+ * @hide
+ */
+ int TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE = 27;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "TRANSIT_OLD_" }, value = {
+ TRANSIT_OLD_UNSET,
+ TRANSIT_OLD_NONE,
+ TRANSIT_OLD_ACTIVITY_OPEN,
+ TRANSIT_OLD_ACTIVITY_CLOSE,
+ TRANSIT_OLD_TASK_OPEN,
+ TRANSIT_OLD_TASK_CLOSE,
+ TRANSIT_OLD_TASK_TO_FRONT,
+ TRANSIT_OLD_TASK_TO_BACK,
+ TRANSIT_OLD_WALLPAPER_CLOSE,
+ TRANSIT_OLD_WALLPAPER_OPEN,
+ TRANSIT_OLD_WALLPAPER_INTRA_OPEN,
+ TRANSIT_OLD_WALLPAPER_INTRA_CLOSE,
+ TRANSIT_OLD_TASK_OPEN_BEHIND,
+ TRANSIT_OLD_ACTIVITY_RELAUNCH,
+ TRANSIT_OLD_KEYGUARD_GOING_AWAY,
+ TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+ TRANSIT_OLD_KEYGUARD_OCCLUDE,
+ TRANSIT_OLD_KEYGUARD_UNOCCLUDE,
+ TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN,
+ TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE,
+ TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE,
+ TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionOldType {}
+
+ /** @hide */
+ int TRANSIT_NONE = 0;
+ /**
+ * A window that didn't exist before has been created and made visible.
+ * @hide
+ */
+ int TRANSIT_OPEN = 1;
+ /**
+ * A window that was visible no-longer exists (was finished or destroyed).
+ * @hide
+ */
+ int TRANSIT_CLOSE = 2;
+ /**
+ * A window that already existed but was not visible is made visible.
+ * @hide
+ */
+ int TRANSIT_TO_FRONT = 3;
+ /**
+ * A window that was visible is made invisible but still exists.
+ * @hide
+ */
+ int TRANSIT_TO_BACK = 4;
+ /** @hide */
+ int TRANSIT_RELAUNCH = 5;
+ /**
+ * A window is visible before and after but changes in some way (eg. it resizes or changes
+ * windowing-mode).
+ * @hide
+ */
+ int TRANSIT_CHANGE = 6;
+ /**
+ * The keyguard was visible and has been dismissed.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_GOING_AWAY = 7;
+ /**
+ * A window is appearing above a locked keyguard.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_OCCLUDE = 8;
+ /**
+ * A window is made invisible revealing a locked keyguard.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_UNOCCLUDE = 9;
+ /**
+ * The first slot for custom transition types. Callers (like Shell) can make use of custom
+ * transition types for dealing with special cases. These types are effectively ignored by
+ * Core and will just be passed along as part of TransitionInfo objects. An example is
+ * split-screen using a custom type for it's snap-to-dismiss action. By using a custom type,
+ * Shell can properly dispatch the results of that transition to the split-screen
+ * implementation.
+ * @hide
+ */
+ int TRANSIT_FIRST_CUSTOM = 10;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "TRANSIT_" }, value = {
+ TRANSIT_NONE,
+ TRANSIT_OPEN,
+ TRANSIT_CLOSE,
+ TRANSIT_TO_FRONT,
+ TRANSIT_TO_BACK,
+ TRANSIT_RELAUNCH,
+ TRANSIT_CHANGE,
+ TRANSIT_KEYGUARD_GOING_AWAY,
+ TRANSIT_KEYGUARD_OCCLUDE,
+ TRANSIT_KEYGUARD_UNOCCLUDE,
+ TRANSIT_FIRST_CUSTOM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionType {}
+
+ /**
+ * Transition flag: Keyguard is going away, but keeping the notification shade open
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE = 0x1;
+
+ /**
+ * Transition flag: Keyguard is going away, but doesn't want an animation for it
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION = 0x2;
+
+ /**
+ * Transition flag: Keyguard is going away while it was showing the system wallpaper.
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER = 0x4;
+
+ /**
+ * Transition flag: Keyguard is going away with subtle animation.
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION = 0x8;
+
+ /**
+ * Transition flag: App is crashed.
+ * @hide
+ */
+ int TRANSIT_FLAG_APP_CRASHED = 0x10;
+
+ /**
+ * Transition flag: A window in a new task is being opened behind an existing one in another
+ * activity's task.
+ * @hide
+ */
+ int TRANSIT_FLAG_OPEN_BEHIND = 0x20;
+
+ /**
+ * Transition flag: The keyguard is locked throughout the whole transition.
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_LOCKED = 0x40;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION,
+ TRANSIT_FLAG_APP_CRASHED,
+ TRANSIT_FLAG_OPEN_BEHIND,
+ TRANSIT_FLAG_KEYGUARD_LOCKED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionFlags {}
+
+ /**
+ * Remove content mode: Indicates remove content mode is currently not defined.
+ * @hide
+ */
+ int REMOVE_CONTENT_MODE_UNDEFINED = 0;
+
+ /**
+ * Remove content mode: Indicates that when display is removed, all its activities will be moved
+ * to the primary display and the topmost activity should become focused.
+ * @hide
+ */
+ int REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY = 1;
+
+ /**
+ * Remove content mode: Indicates that when display is removed, all its stacks and tasks will be
+ * removed, all activities will be destroyed according to the usual lifecycle.
+ * @hide
+ */
+ int REMOVE_CONTENT_MODE_DESTROY = 2;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "REMOVE_CONTENT_MODE_" }, value = {
+ REMOVE_CONTENT_MODE_UNDEFINED,
+ REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY,
+ REMOVE_CONTENT_MODE_DESTROY,
+ })
+ @interface RemoveContentMode {}
+
+ /**
+ * Display IME Policy: The IME should appear on the local display.
+ * @hide
+ */
+ @TestApi
+ int DISPLAY_IME_POLICY_LOCAL = 0;
+
+ /**
+ * Display IME Policy: The IME should appear on the fallback display.
+ * @hide
+ */
+ @TestApi
+ int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1;
+
+ /**
+ * Display IME Policy: The IME should be hidden.
+ *
+ * Setting this policy will prevent the IME from making a connection. This
+ * will prevent any IME from receiving metadata about input.
+ * @hide
+ */
+ @TestApi
+ int DISPLAY_IME_POLICY_HIDE = 2;
+
+ /**
+ * @hide
+ */
+ @IntDef({
+ DISPLAY_IME_POLICY_LOCAL,
+ DISPLAY_IME_POLICY_FALLBACK_DISPLAY,
+ DISPLAY_IME_POLICY_HIDE,
+ })
+ @interface DisplayImePolicy {}
+
+ /**
+ * Exception that is thrown when trying to add view whose
+ * {@link LayoutParams} {@link LayoutParams#token}
+ * is invalid.
+ */
+ public static class BadTokenException extends RuntimeException {
+ public BadTokenException() {
+ }
+
+ public BadTokenException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * Exception that is thrown when calling {@link #addView} to a secondary display that cannot
+ * be found. See {@link android.app.Presentation} for more information on secondary displays.
+ */
+ public static class InvalidDisplayException extends RuntimeException {
+ public InvalidDisplayException() {
+ }
+
+ public InvalidDisplayException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * Returns the {@link Display} upon which this {@link WindowManager} instance
+ * will create new windows.
+ * <p>
+ * Despite the name of this method, the display that is returned is not
+ * necessarily the primary display of the system (see {@link Display#DEFAULT_DISPLAY}).
+ * The returned display could instead be a secondary display that this
+ * window manager instance is managing. Think of it as the display that
+ * this {@link WindowManager} instance uses by default.
+ * </p><p>
+ * To create windows on a different display, you need to obtain a
+ * {@link WindowManager} for that {@link Display}. (See the {@link WindowManager}
+ * class documentation for more information.)
+ * </p>
+ *
+ * @return The display that this window manager is managing.
+ * @deprecated Use {@link Context#getDisplay()} instead.
+ */
+ @Deprecated
+ public Display getDefaultDisplay();
+
+ /**
+ * Special variation of {@link #removeView} that immediately invokes
+ * the given view hierarchy's {@link View#onDetachedFromWindow()
+ * View.onDetachedFromWindow()} methods before returning. This is not
+ * for normal applications; using it correctly requires great care.
+ *
+ * @param view The view to be removed.
+ */
+ public void removeViewImmediate(View view);
+
+ /**
+ * Returns the {@link WindowMetrics} according to the current system state.
+ * <p>
+ * The metrics describe the size of the area the window would occupy with
+ * {@link LayoutParams#MATCH_PARENT MATCH_PARENT} width and height, and the {@link WindowInsets}
+ * such a window would have.
+ * <p>
+ * The value of this is based on the <b>current</b> windowing state of the system.
+ *
+ * For example, for activities in multi-window mode, the metrics returned are based on the
+ * current bounds that the user has selected for the {@link android.app.Activity Activity}'s
+ * task.
+ * <p>
+ * In most scenarios, {@link #getCurrentWindowMetrics()} rather than
+ * {@link #getMaximumWindowMetrics()} is the correct API to use, since it ensures values reflect
+ * window size when the app is not fullscreen.
+ *
+ * @see #getMaximumWindowMetrics()
+ * @see WindowMetrics
+ */
+ default @NonNull WindowMetrics getCurrentWindowMetrics() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the largest {@link WindowMetrics} an app may expect in the current system state.
+ * <p>
+ * The value of this is based on the largest <b>potential</b> windowing state of the system.
+ *
+ * For example, for activities in multi-window mode, the metrics returned are based on the
+ * what the bounds would be if the user expanded the {@link android.app.Activity Activity}'s
+ * task to cover the entire screen.
+ * <p>
+ * The metrics describe the size of the largest potential area the window might occupy with
+ * {@link LayoutParams#MATCH_PARENT MATCH_PARENT} width and height, and the {@link WindowInsets}
+ * such a window would have.
+ * <p>
+ * Note that this might still be smaller than the size of the physical display if certain areas
+ * of the display are not available to windows created in this {@link Context}.
+ *
+ * For example, given that there's a device which have a multi-task mode to limit activities
+ * to a half screen. In this case, {@link #getMaximumWindowMetrics()} reports the bounds of
+ * the half screen which the activity is located.
+ * <p>
+ * <b>Generally {@link #getCurrentWindowMetrics()} is the correct API to use</b> for choosing
+ * UI layouts. {@link #getMaximumWindowMetrics()} are only appropriate when the application
+ * needs to know the largest possible size it can occupy if the user expands/maximizes it on the
+ * screen.
+ *
+ * @see #getCurrentWindowMetrics()
+ * @see WindowMetrics
+ * @see Display#getRealSize(Point)
+ */
+ default @NonNull WindowMetrics getMaximumWindowMetrics() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Used to asynchronously request Keyboard Shortcuts from the focused window.
+ *
+ * @hide
+ */
+ public interface KeyboardShortcutsReceiver {
+ /**
+ * Callback used when the focused window keyboard shortcuts are ready to be displayed.
+ *
+ * @param result The keyboard shortcuts to be displayed.
+ */
+ void onKeyboardShortcutsReceived(List<KeyboardShortcutGroup> result);
+ }
+
+ /**
+ * Invoke screenshot flow to capture a full-screen image.
+ * @hide
+ */
+ int TAKE_SCREENSHOT_FULLSCREEN = 1;
+
+ /**
+ * Invoke screenshot flow allowing the user to select a region.
+ * @hide
+ */
+ int TAKE_SCREENSHOT_SELECTED_REGION = 2;
+
+ /**
+ * Invoke screenshot flow with an image provided by the caller.
+ * @hide
+ */
+ int TAKE_SCREENSHOT_PROVIDED_IMAGE = 3;
+
+ /**
+ * Enum listing the types of screenshot requests available.
+ *
+ * @hide
+ */
+ @IntDef({TAKE_SCREENSHOT_FULLSCREEN,
+ TAKE_SCREENSHOT_SELECTED_REGION,
+ TAKE_SCREENSHOT_PROVIDED_IMAGE})
+ @interface ScreenshotType {}
+
+ /**
+ * Enum listing the possible sources from which a screenshot was originated. Used for logging.
+ *
+ * @hide
+ */
+ @IntDef({ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS,
+ ScreenshotSource.SCREENSHOT_KEY_CHORD,
+ ScreenshotSource.SCREENSHOT_KEY_OTHER,
+ ScreenshotSource.SCREENSHOT_OVERVIEW,
+ ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
+ ScreenshotSource.SCREENSHOT_OTHER,
+ ScreenshotSource.SCREENSHOT_VENDOR_GESTURE})
+ @interface ScreenshotSource {
+ int SCREENSHOT_GLOBAL_ACTIONS = 0;
+ int SCREENSHOT_KEY_CHORD = 1;
+ int SCREENSHOT_KEY_OTHER = 2;
+ int SCREENSHOT_OVERVIEW = 3;
+ int SCREENSHOT_ACCESSIBILITY_ACTIONS = 4;
+ int SCREENSHOT_OTHER = 5;
+ int SCREENSHOT_VENDOR_GESTURE = 6;
+ }
+
+ /**
+ * @hide
+ */
+ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array";
+
+ /**
+ * Request for keyboard shortcuts to be retrieved asynchronously.
+ *
+ * @param receiver The callback to be triggered when the result is ready.
+ *
+ * @hide
+ */
+ public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId);
+
+ /**
+ * Return the touch region for the current IME window, or an empty region if there is none.
+ *
+ * @return The region of the IME that is accepting touch inputs, or null if there is no IME, no
+ * region or there was an error.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public Region getCurrentImeTouchRegion();
+
+ /**
+ * Sets that the display should show its content when non-secure keyguard is shown.
+ *
+ * @param displayId Display ID.
+ * @param shouldShow Indicates that the display should show its content when non-secure keyguard
+ * is shown.
+ * @see KeyguardManager#isDeviceSecure()
+ * @see KeyguardManager#isDeviceLocked()
+ * @hide
+ */
+ @TestApi
+ default void setShouldShowWithInsecureKeyguard(int displayId, boolean shouldShow) {
+ }
+
+ /**
+ * Sets that the display should show system decors.
+ * <p>
+ * System decors include status bar, navigation bar, launcher.
+ * </p>
+ *
+ * @param displayId The id of the display.
+ * @param shouldShow Indicates that the display should show system decors.
+ * @see #shouldShowSystemDecors(int)
+ * @hide
+ */
+ @TestApi
+ default void setShouldShowSystemDecors(int displayId, boolean shouldShow) {
+ }
+
+ /**
+ * Checks if the display supports showing system decors.
+ * <p>
+ * System decors include status bar, navigation bar, launcher.
+ * </p>
+ *
+ * @param displayId The id of the display.
+ * @see #setShouldShowSystemDecors(int, boolean)
+ * @hide
+ */
+ @TestApi
+ default boolean shouldShowSystemDecors(int displayId) {
+ return false;
+ }
+
+ /**
+ * Sets the policy for how the display should show IME.
+ *
+ * @param displayId Display ID.
+ * @param imePolicy Indicates the policy for how the display should show IME.
+ * @hide
+ */
+ @TestApi
+ default void setDisplayImePolicy(int displayId, @DisplayImePolicy int imePolicy) {
+ }
+
+ /**
+ * Indicates the policy for how the display should show IME.
+ *
+ * @param displayId The id of the display.
+ * @return The policy for how the display should show IME.
+ * @hide
+ */
+ @TestApi
+ default @DisplayImePolicy int getDisplayImePolicy(int displayId) {
+ return DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
+ }
+
+ /**
+ * <p>
+ * Returns whether cross-window blur is currently enabled. This affects both window blur behind
+ * (see {@link LayoutParams#setBlurBehindRadius}) and window background blur (see
+ * {@link Window#setBackgroundBlurRadius}).
+ * </p><p>
+ * Cross-window blur might not be supported by some devices due to GPU limitations. It can also
+ * be disabled at runtime, e.g. during battery saving mode, when multimedia tunneling is used or
+ * when minimal post processing is requested. In such situations, no blur will be computed or
+ * drawn, so the blur target area will not be blurred. To handle this, the app might want to
+ * change its theme to one that does not use blurs. To listen for cross-window blur
+ * enabled/disabled events, use {@link #addCrossWindowBlurEnabledListener}.
+ * </p>
+ *
+ * @see #addCrossWindowBlurEnabledListener
+ * @see LayoutParams#setBlurBehindRadius
+ * @see Window#setBackgroundBlurRadius
+ */
+ default boolean isCrossWindowBlurEnabled() {
+ return false;
+ }
+
+ /**
+ * <p>
+ * Adds a listener, which will be called when cross-window blurs are enabled/disabled at
+ * runtime. This affects both window blur behind (see {@link LayoutParams#setBlurBehindRadius})
+ * and window background blur (see {@link Window#setBackgroundBlurRadius}).
+ * </p><p>
+ * Cross-window blur might not be supported by some devices due to GPU limitations. It can also
+ * be disabled at runtime, e.g. during battery saving mode, when multimedia tunneling is used or
+ * when minimal post processing is requested. In such situations, no blur will be computed or
+ * drawn, so the blur target area will not be blurred. To handle this, the app might want to
+ * change its theme to one that does not use blurs.
+ * </p><p>
+ * The listener will be called on the main thread.
+ * </p><p>
+ * If the listener is added successfully, it will be called immediately with the current
+ * cross-window blur enabled state.
+ * </p>
+ *
+ * @param listener the listener to be added. It will be called back with a boolean parameter,
+ * which is true if cross-window blur is enabled and false if it is disabled
+ *
+ * @see #removeCrossWindowBlurEnabledListener
+ * @see #isCrossWindowBlurEnabled
+ * @see LayoutParams#setBlurBehindRadius
+ * @see Window#setBackgroundBlurRadius
+ */
+ default void addCrossWindowBlurEnabledListener(@NonNull Consumer<Boolean> listener) {
+ }
+
+ /**
+ * <p>
+ * Adds a listener, which will be called when cross-window blurs are enabled/disabled at
+ * runtime. This affects both window blur behind (see {@link LayoutParams#setBlurBehindRadius})
+ * and window background blur (see {@link Window#setBackgroundBlurRadius}).
+ * </p><p>
+ * Cross-window blur might not be supported by some devices due to GPU limitations. It can also
+ * be disabled at runtime, e.g. during battery saving mode, when multimedia tunneling is used or
+ * when minimal post processing is requested. In such situations, no blur will be computed or
+ * drawn, so the blur target area will not be blurred. To handle this, the app might want to
+ * change its theme to one that does not use blurs.
+ * </p><p>
+ * If the listener is added successfully, it will be called immediately with the current
+ * cross-window blur enabled state.
+ * </p>
+ *
+ * @param executor {@link Executor} to handle the listener callback
+ * @param listener the listener to be added. It will be called back with a boolean parameter,
+ * which is true if cross-window blur is enabled and false if it is disabled
+ *
+ * @see #removeCrossWindowBlurEnabledListener
+ * @see #isCrossWindowBlurEnabled
+ * @see LayoutParams#setBlurBehindRadius
+ * @see Window#setBackgroundBlurRadius
+ */
+ default void addCrossWindowBlurEnabledListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<Boolean> listener) {
+ }
+
+ /**
+ * Removes a listener, previously added with {@link #addCrossWindowBlurEnabledListener}
+ *
+ * @param listener the listener to be removed
+ *
+ * @see #addCrossWindowBlurEnabledListener
+ */
+ default void removeCrossWindowBlurEnabledListener(@NonNull Consumer<Boolean> listener) {
+ }
+
+ /**
+ * @hide
+ */
+ static String transitTypeToString(@TransitionType int type) {
+ switch (type) {
+ case TRANSIT_NONE: return "NONE";
+ case TRANSIT_OPEN: return "OPEN";
+ case TRANSIT_CLOSE: return "CLOSE";
+ case TRANSIT_TO_FRONT: return "TO_FRONT";
+ case TRANSIT_TO_BACK: return "TO_BACK";
+ case TRANSIT_RELAUNCH: return "RELAUNCH";
+ case TRANSIT_CHANGE: return "CHANGE";
+ case TRANSIT_KEYGUARD_GOING_AWAY: return "KEYGUARD_GOING_AWAY";
+ case TRANSIT_KEYGUARD_OCCLUDE: return "KEYGUARD_OCCLUDE";
+ case TRANSIT_KEYGUARD_UNOCCLUDE: return "KEYGUARD_UNOCCLUDE";
+ case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM";
+ default:
+ if (type > TRANSIT_FIRST_CUSTOM) {
+ return "FIRST_CUSTOM+" + (type - TRANSIT_FIRST_CUSTOM);
+ }
+ return "UNKNOWN(" + type + ")";
+ }
+ }
+
+ public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
+ /**
+ * X position for this window. With the default gravity it is ignored.
+ * When using {@link Gravity#LEFT} or {@link Gravity#START} or {@link Gravity#RIGHT} or
+ * {@link Gravity#END} it provides an offset from the given edge.
+ */
+ @ViewDebug.ExportedProperty
+ public int x;
+
+ /**
+ * Y position for this window. With the default gravity it is ignored.
+ * When using {@link Gravity#TOP} or {@link Gravity#BOTTOM} it provides
+ * an offset from the given edge.
+ */
+ @ViewDebug.ExportedProperty
+ public int y;
+
+ /**
+ * Indicates how much of the extra space will be allocated horizontally
+ * to the view associated with these LayoutParams. Specify 0 if the view
+ * should not be stretched. Otherwise the extra pixels will be pro-rated
+ * among all views whose weight is greater than 0.
+ */
+ @ViewDebug.ExportedProperty
+ public float horizontalWeight;
+
+ /**
+ * Indicates how much of the extra space will be allocated vertically
+ * to the view associated with these LayoutParams. Specify 0 if the view
+ * should not be stretched. Otherwise the extra pixels will be pro-rated
+ * among all views whose weight is greater than 0.
+ */
+ @ViewDebug.ExportedProperty
+ public float verticalWeight;
+
+ /**
+ * The general type of window. There are three main classes of
+ * window types:
+ * <ul>
+ * <li> <strong>Application windows</strong> (ranging from
+ * {@link #FIRST_APPLICATION_WINDOW} to
+ * {@link #LAST_APPLICATION_WINDOW}) are normal top-level application
+ * windows. For these types of windows, the {@link #token} must be
+ * set to the token of the activity they are a part of (this will
+ * normally be done for you if {@link #token} is null).
+ * <li> <strong>Sub-windows</strong> (ranging from
+ * {@link #FIRST_SUB_WINDOW} to
+ * {@link #LAST_SUB_WINDOW}) are associated with another top-level
+ * window. For these types of windows, the {@link #token} must be
+ * the token of the window it is attached to.
+ * <li> <strong>System windows</strong> (ranging from
+ * {@link #FIRST_SYSTEM_WINDOW} to
+ * {@link #LAST_SYSTEM_WINDOW}) are special types of windows for
+ * use by the system for specific purposes. They should not normally
+ * be used by applications, and a special permission is required
+ * to use them.
+ * </ul>
+ *
+ * @see #TYPE_BASE_APPLICATION
+ * @see #TYPE_APPLICATION
+ * @see #TYPE_APPLICATION_STARTING
+ * @see #TYPE_DRAWN_APPLICATION
+ * @see #TYPE_APPLICATION_PANEL
+ * @see #TYPE_APPLICATION_MEDIA
+ * @see #TYPE_APPLICATION_SUB_PANEL
+ * @see #TYPE_APPLICATION_ATTACHED_DIALOG
+ * @see #TYPE_STATUS_BAR
+ * @see #TYPE_SEARCH_BAR
+ * @see #TYPE_PHONE
+ * @see #TYPE_SYSTEM_ALERT
+ * @see #TYPE_TOAST
+ * @see #TYPE_SYSTEM_OVERLAY
+ * @see #TYPE_PRIORITY_PHONE
+ * @see #TYPE_SYSTEM_DIALOG
+ * @see #TYPE_KEYGUARD_DIALOG
+ * @see #TYPE_SYSTEM_ERROR
+ * @see #TYPE_INPUT_METHOD
+ * @see #TYPE_INPUT_METHOD_DIALOG
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = TYPE_BASE_APPLICATION,
+ to = "BASE_APPLICATION"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION,
+ to = "APPLICATION"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_STARTING,
+ to = "APPLICATION_STARTING"),
+ @ViewDebug.IntToString(from = TYPE_DRAWN_APPLICATION,
+ to = "DRAWN_APPLICATION"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_PANEL,
+ to = "APPLICATION_PANEL"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA,
+ to = "APPLICATION_MEDIA"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_SUB_PANEL,
+ to = "APPLICATION_SUB_PANEL"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_ABOVE_SUB_PANEL,
+ to = "APPLICATION_ABOVE_SUB_PANEL"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_ATTACHED_DIALOG,
+ to = "APPLICATION_ATTACHED_DIALOG"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA_OVERLAY,
+ to = "APPLICATION_MEDIA_OVERLAY"),
+ @ViewDebug.IntToString(from = TYPE_STATUS_BAR,
+ to = "STATUS_BAR"),
+ @ViewDebug.IntToString(from = TYPE_SEARCH_BAR,
+ to = "SEARCH_BAR"),
+ @ViewDebug.IntToString(from = TYPE_PHONE,
+ to = "PHONE"),
+ @ViewDebug.IntToString(from = TYPE_SYSTEM_ALERT,
+ to = "SYSTEM_ALERT"),
+ @ViewDebug.IntToString(from = TYPE_TOAST,
+ to = "TOAST"),
+ @ViewDebug.IntToString(from = TYPE_SYSTEM_OVERLAY,
+ to = "SYSTEM_OVERLAY"),
+ @ViewDebug.IntToString(from = TYPE_PRIORITY_PHONE,
+ to = "PRIORITY_PHONE"),
+ @ViewDebug.IntToString(from = TYPE_SYSTEM_DIALOG,
+ to = "SYSTEM_DIALOG"),
+ @ViewDebug.IntToString(from = TYPE_KEYGUARD_DIALOG,
+ to = "KEYGUARD_DIALOG"),
+ @ViewDebug.IntToString(from = TYPE_SYSTEM_ERROR,
+ to = "SYSTEM_ERROR"),
+ @ViewDebug.IntToString(from = TYPE_INPUT_METHOD,
+ to = "INPUT_METHOD"),
+ @ViewDebug.IntToString(from = TYPE_INPUT_METHOD_DIALOG,
+ to = "INPUT_METHOD_DIALOG"),
+ @ViewDebug.IntToString(from = TYPE_WALLPAPER,
+ to = "WALLPAPER"),
+ @ViewDebug.IntToString(from = TYPE_STATUS_BAR_PANEL,
+ to = "STATUS_BAR_PANEL"),
+ @ViewDebug.IntToString(from = TYPE_SECURE_SYSTEM_OVERLAY,
+ to = "SECURE_SYSTEM_OVERLAY"),
+ @ViewDebug.IntToString(from = TYPE_DRAG,
+ to = "DRAG"),
+ @ViewDebug.IntToString(from = TYPE_STATUS_BAR_SUB_PANEL,
+ to = "STATUS_BAR_SUB_PANEL"),
+ @ViewDebug.IntToString(from = TYPE_POINTER,
+ to = "POINTER"),
+ @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR,
+ to = "NAVIGATION_BAR"),
+ @ViewDebug.IntToString(from = TYPE_VOLUME_OVERLAY,
+ to = "VOLUME_OVERLAY"),
+ @ViewDebug.IntToString(from = TYPE_BOOT_PROGRESS,
+ to = "BOOT_PROGRESS"),
+ @ViewDebug.IntToString(from = TYPE_INPUT_CONSUMER,
+ to = "INPUT_CONSUMER"),
+ @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR_PANEL,
+ to = "NAVIGATION_BAR_PANEL"),
+ @ViewDebug.IntToString(from = TYPE_DISPLAY_OVERLAY,
+ to = "DISPLAY_OVERLAY"),
+ @ViewDebug.IntToString(from = TYPE_MAGNIFICATION_OVERLAY,
+ to = "MAGNIFICATION_OVERLAY"),
+ @ViewDebug.IntToString(from = TYPE_PRESENTATION,
+ to = "PRESENTATION"),
+ @ViewDebug.IntToString(from = TYPE_PRIVATE_PRESENTATION,
+ to = "PRIVATE_PRESENTATION"),
+ @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION,
+ to = "VOICE_INTERACTION"),
+ @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION_STARTING,
+ to = "VOICE_INTERACTION_STARTING"),
+ @ViewDebug.IntToString(from = TYPE_DOCK_DIVIDER,
+ to = "DOCK_DIVIDER"),
+ @ViewDebug.IntToString(from = TYPE_QS_DIALOG,
+ to = "QS_DIALOG"),
+ @ViewDebug.IntToString(from = TYPE_SCREENSHOT,
+ to = "SCREENSHOT"),
+ @ViewDebug.IntToString(from = TYPE_APPLICATION_OVERLAY,
+ to = "APPLICATION_OVERLAY")
+ })
+ @WindowType
+ public int type;
+
+ /**
+ * Start of window types that represent normal application windows.
+ */
+ public static final int FIRST_APPLICATION_WINDOW = 1;
+
+ /**
+ * Window type: an application window that serves as the "base" window
+ * of the overall application; all other application windows will
+ * appear on top of it.
+ * In multiuser systems shows only on the owning user's window.
+ */
+ public static final int TYPE_BASE_APPLICATION = 1;
+
+ /**
+ * Window type: a normal application window. The {@link #token} must be
+ * an Activity token identifying who the window belongs to.
+ * In multiuser systems shows only on the owning user's window.
+ */
+ public static final int TYPE_APPLICATION = 2;
+
+ /**
+ * Window type: special application window that is displayed while the
+ * application is starting. Not for use by applications themselves;
+ * this is used by the system to display something until the
+ * application can show its own windows.
+ * In multiuser systems shows on all users' windows.
+ */
+ public static final int TYPE_APPLICATION_STARTING = 3;
+
+ /**
+ * Window type: a variation on TYPE_APPLICATION that ensures the window
+ * manager will wait for this window to be drawn before the app is shown.
+ * In multiuser systems shows only on the owning user's window.
+ */
+ public static final int TYPE_DRAWN_APPLICATION = 4;
+
+ /**
+ * End of types of application windows.
+ */
+ public static final int LAST_APPLICATION_WINDOW = 99;
+
+ /**
+ * Start of types of sub-windows. The {@link #token} of these windows
+ * must be set to the window they are attached to. These types of
+ * windows are kept next to their attached window in Z-order, and their
+ * coordinate space is relative to their attached window.
+ */
+ public static final int FIRST_SUB_WINDOW = 1000;
+
+ /**
+ * Window type: a panel on top of an application window. These windows
+ * appear on top of their attached window.
+ */
+ public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
+
+ /**
+ * Window type: window for showing media (such as video). These windows
+ * are displayed behind their attached window.
+ */
+ public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
+
+ /**
+ * Window type: a sub-panel on top of an application window. These
+ * windows are displayed on top their attached window and any
+ * {@link #TYPE_APPLICATION_PANEL} panels.
+ */
+ public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
+
+ /** Window type: like {@link #TYPE_APPLICATION_PANEL}, but layout
+ * of the window happens as that of a top-level window, <em>not</em>
+ * as a child of its container.
+ */
+ public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
+
+ /**
+ * Window type: window for showing overlays on top of media windows.
+ * These windows are displayed between TYPE_APPLICATION_MEDIA and the
+ * application window. They should be translucent to be useful. This
+ * is a big ugly hack so:
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;
+
+ /**
+ * Window type: a above sub-panel on top of an application window and it's
+ * sub-panel windows. These windows are displayed on top of their attached window
+ * and any {@link #TYPE_APPLICATION_SUB_PANEL} panels.
+ * @hide
+ */
+ public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
+
+ /**
+ * End of types of sub-windows.
+ */
+ public static final int LAST_SUB_WINDOW = 1999;
+
+ /**
+ * Start of system-specific window types. These are not normally
+ * created by applications.
+ */
+ public static final int FIRST_SYSTEM_WINDOW = 2000;
+
+ /**
+ * Window type: the status bar. There can be only one status bar
+ * window; it is placed at the top of the screen, and all other
+ * windows are shifted down so they are below it.
+ * In multiuser systems shows on all users' windows.
+ */
+ public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
+
+ /**
+ * Window type: the search bar. There can be only one search bar
+ * window; it is placed at the top of the screen.
+ * In multiuser systems shows on all users' windows.
+ */
+ public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
+
+ /**
+ * Window type: phone. These are non-application windows providing
+ * user interaction with the phone (in particular incoming calls).
+ * These windows are normally placed above all applications, but behind
+ * the status bar.
+ * In multiuser systems shows on all users' windows.
+ * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
+ */
+ @Deprecated
+ public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;
+
+ /**
+ * Window type: system window, such as low power alert. These windows
+ * are always on top of application windows.
+ * In multiuser systems shows only on the owning user's window.
+ * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
+ */
+ @Deprecated
+ public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;
+
+ /**
+ * Window type: keyguard window.
+ * In multiuser systems shows on all users' windows.
+ * @removed
+ */
+ public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
+
+ /**
+ * Window type: transient notifications.
+ * In multiuser systems shows only on the owning user's window.
+ * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
+ */
+ @Deprecated
+ public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
+
+ /**
+ * Window type: system overlay windows, which need to be displayed
+ * on top of everything else. These windows must not take input
+ * focus, or they will interfere with the keyguard.
+ * In multiuser systems shows only on the owning user's window.
+ * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
+ */
+ @Deprecated
+ public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;
+
+ /**
+ * Window type: priority phone UI, which needs to be displayed even if
+ * the keyguard is active. These windows must not take input
+ * focus, or they will interfere with the keyguard.
+ * In multiuser systems shows on all users' windows.
+ * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
+ */
+ @Deprecated
+ public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;
+
+ /**
+ * Window type: panel that slides out from the status bar
+ * In multiuser systems shows on all users' windows.
+ */
+ public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
+
+ /**
+ * Window type: dialogs that the keyguard shows
+ * In multiuser systems shows on all users' windows.
+ */
+ public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
+
+ /**
+ * Window type: internal system error windows, appear on top of
+ * everything they can.
+ * In multiuser systems shows only on the owning user's window.
+ * @deprecated for non-system apps. Use {@link #TYPE_APPLICATION_OVERLAY} instead.
+ */
+ @Deprecated
+ public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;
+
+ /**
+ * Window type: internal input methods windows, which appear above
+ * the normal UI. Application windows may be resized or panned to keep
+ * the input focus visible while this window is displayed.
+ * In multiuser systems shows only on the owning user's window.
+ */
+ public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;
+
+ /**
+ * Window type: internal input methods dialog windows, which appear above
+ * the current input method window.
+ * In multiuser systems shows only on the owning user's window.
+ */
+ public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
+
+ /**
+ * Window type: wallpaper window, placed behind any window that wants
+ * to sit on top of the wallpaper.
+ * In multiuser systems shows only on the owning user's window.
+ */
+ public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;
+
+ /**
+ * Window type: panel that slides out from over the status bar
+ * In multiuser systems shows on all users' windows.
+ *
+ * @removed
+ */
+ public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
+
+ /**
+ * Window type: secure system overlay windows, which need to be displayed
+ * on top of everything else. These windows must not take input
+ * focus, or they will interfere with the keyguard.
+ *
+ * This is exactly like {@link #TYPE_SYSTEM_OVERLAY} except that only the
+ * system itself is allowed to create these overlays. Applications cannot
+ * obtain permission to create secure system overlays.
+ *
+ * In multiuser systems shows only on the owning user's window.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
+
+ /**
+ * Window type: the drag-and-drop pseudowindow. There is only one
+ * drag layer (at most), and it is placed on top of all other windows.
+ * In multiuser systems shows only on the owning user's window.
+ * @hide
+ */
+ public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+16;
+
+ /**
+ * Window type: panel that slides out from over the status bar
+ * In multiuser systems shows on all users' windows. These windows
+ * are displayed on top of the stauts bar and any {@link #TYPE_STATUS_BAR_PANEL}
+ * windows.
+ * @hide
+ */
+ public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
+
+ /**
+ * Window type: (mouse) pointer
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
+
+ /**
+ * Window type: Navigation bar (when distinct from status bar)
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
+
+ /**
+ * Window type: The volume level overlay/dialog shown when the user
+ * changes the system volume.
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
+
+ /**
+ * Window type: The boot progress dialog, goes on top of everything
+ * in the world.
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
+
+ /**
+ * Window type to consume input events when the systemUI bars are hidden.
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_INPUT_CONSUMER = FIRST_SYSTEM_WINDOW+22;
+
+ /**
+ * Window type: Navigation bar panel (when navigation bar is distinct from status bar)
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
+
+ /**
+ * Window type: Display overlay window. Used to simulate secondary display devices.
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
+
+ /**
+ * Window type: Magnification overlay window. Used to highlight the magnified
+ * portion of a display when accessibility magnification is enabled.
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
+
+ /**
+ * Window type: Window for Presentation on top of private
+ * virtual display.
+ */
+ public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
+
+ /**
+ * Window type: Windows in the voice interaction layer.
+ * @hide
+ */
+ public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
+
+ /**
+ * Window type: Windows that are overlaid <em>only</em> by a connected {@link
+ * android.accessibilityservice.AccessibilityService} for interception of
+ * user interactions without changing the windows an accessibility service
+ * can introspect. In particular, an accessibility service can introspect
+ * only windows that a sighted user can interact with which is they can touch
+ * these windows or can type into these windows. For example, if there
+ * is a full screen accessibility overlay that is touchable, the windows
+ * below it will be introspectable by an accessibility service even though
+ * they are covered by a touchable window.
+ */
+ public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
+
+ /**
+ * Window type: Starting window for voice interaction layer.
+ * @hide
+ */
+ public static final int TYPE_VOICE_INTERACTION_STARTING = FIRST_SYSTEM_WINDOW+33;
+
+ /**
+ * Window for displaying a handle used for resizing docked stacks. This window is owned
+ * by the system process.
+ * @hide
+ */
+ public static final int TYPE_DOCK_DIVIDER = FIRST_SYSTEM_WINDOW+34;
+
+ /**
+ * Window type: like {@link #TYPE_APPLICATION_ATTACHED_DIALOG}, but used
+ * by Quick Settings Tiles.
+ * @hide
+ */
+ public static final int TYPE_QS_DIALOG = FIRST_SYSTEM_WINDOW+35;
+
+ /**
+ * Window type: shows directly above the keyguard. The layer is
+ * reserved for screenshot animation, region selection and UI.
+ * In multiuser systems shows only on the owning user's window.
+ * @hide
+ */
+ public static final int TYPE_SCREENSHOT = FIRST_SYSTEM_WINDOW + 36;
+
+ /**
+ * Window type: Window for Presentation on an external display.
+ * @see android.app.Presentation
+ * @hide
+ */
+ public static final int TYPE_PRESENTATION = FIRST_SYSTEM_WINDOW + 37;
+
+ /**
+ * Window type: Application overlay windows are displayed above all activity windows
+ * (types between {@link #FIRST_APPLICATION_WINDOW} and {@link #LAST_APPLICATION_WINDOW})
+ * but below critical system windows like the status bar or IME.
+ * <p>
+ * The system may change the position, size, or visibility of these windows at anytime
+ * to reduce visual clutter to the user and also manage resources.
+ * <p>
+ * Requires {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} permission.
+ * <p>
+ * The system will adjust the importance of processes with this window type to reduce the
+ * chance of the low-memory-killer killing them.
+ * <p>
+ * In multi-user systems shows only on the owning user's screen.
+ */
+ public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;
+
+ /**
+ * Window type: Window for adding accessibility window magnification above other windows.
+ * This will place the window in the overlay windows.
+ * @hide
+ */
+ public static final int TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 39;
+
+ /**
+ * Window type: the notification shade and keyguard. There can be only one status bar
+ * window; it is placed at the top of the screen, and all other
+ * windows are shifted down so they are below it.
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_NOTIFICATION_SHADE = FIRST_SYSTEM_WINDOW + 40;
+
+ /**
+ * Window type: used to show the status bar in non conventional parts of the screen (i.e.
+ * the left or the bottom of the screen).
+ * In multiuser systems shows on all users' windows.
+ * @hide
+ */
+ public static final int TYPE_STATUS_BAR_ADDITIONAL = FIRST_SYSTEM_WINDOW + 41;
+
+ /**
+ * End of types of system windows.
+ */
+ public static final int LAST_SYSTEM_WINDOW = 2999;
+
+ /**
+ * @hide
+ * Used internally when there is no suitable type available.
+ */
+ public static final int INVALID_WINDOW_TYPE = -1;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = "TYPE_", value = {
+ TYPE_BASE_APPLICATION,
+ TYPE_APPLICATION,
+ TYPE_APPLICATION_STARTING,
+ TYPE_DRAWN_APPLICATION,
+ TYPE_APPLICATION_PANEL,
+ TYPE_APPLICATION_MEDIA,
+ TYPE_APPLICATION_SUB_PANEL,
+ TYPE_APPLICATION_ATTACHED_DIALOG,
+ TYPE_APPLICATION_MEDIA_OVERLAY,
+ TYPE_APPLICATION_ABOVE_SUB_PANEL,
+ TYPE_STATUS_BAR,
+ TYPE_SEARCH_BAR,
+ TYPE_PHONE,
+ TYPE_SYSTEM_ALERT,
+ TYPE_KEYGUARD,
+ TYPE_TOAST,
+ TYPE_SYSTEM_OVERLAY,
+ TYPE_PRIORITY_PHONE,
+ TYPE_SYSTEM_DIALOG,
+ TYPE_KEYGUARD_DIALOG,
+ TYPE_SYSTEM_ERROR,
+ TYPE_INPUT_METHOD,
+ TYPE_INPUT_METHOD_DIALOG,
+ TYPE_WALLPAPER,
+ TYPE_STATUS_BAR_PANEL,
+ TYPE_SECURE_SYSTEM_OVERLAY,
+ TYPE_DRAG,
+ TYPE_STATUS_BAR_SUB_PANEL,
+ TYPE_POINTER,
+ TYPE_NAVIGATION_BAR,
+ TYPE_VOLUME_OVERLAY,
+ TYPE_BOOT_PROGRESS,
+ TYPE_INPUT_CONSUMER,
+ TYPE_NAVIGATION_BAR_PANEL,
+ TYPE_DISPLAY_OVERLAY,
+ TYPE_MAGNIFICATION_OVERLAY,
+ TYPE_PRIVATE_PRESENTATION,
+ TYPE_VOICE_INTERACTION,
+ TYPE_ACCESSIBILITY_OVERLAY,
+ TYPE_VOICE_INTERACTION_STARTING,
+ TYPE_DOCK_DIVIDER,
+ TYPE_QS_DIALOG,
+ TYPE_SCREENSHOT,
+ TYPE_PRESENTATION,
+ TYPE_APPLICATION_OVERLAY,
+ TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
+ TYPE_NOTIFICATION_SHADE,
+ TYPE_STATUS_BAR_ADDITIONAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface WindowType {}
+
+ /**
+ * Return true if the window type is an alert window.
+ *
+ * @param type The window type.
+ * @return If the window type is an alert window.
+ * @hide
+ */
+ public static boolean isSystemAlertWindowType(@WindowType int type) {
+ switch (type) {
+ case TYPE_PHONE:
+ case TYPE_PRIORITY_PHONE:
+ case TYPE_SYSTEM_ALERT:
+ case TYPE_SYSTEM_ERROR:
+ case TYPE_SYSTEM_OVERLAY:
+ case TYPE_APPLICATION_OVERLAY:
+ return true;
+ }
+ return false;
+ }
+
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int MEMORY_TYPE_NORMAL = 0;
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int MEMORY_TYPE_HARDWARE = 1;
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int MEMORY_TYPE_GPU = 2;
+ /** @deprecated this is ignored, this value is set automatically when needed. */
+ @Deprecated
+ public static final int MEMORY_TYPE_PUSH_BUFFERS = 3;
+
+ /**
+ * @deprecated this is ignored
+ */
+ @Deprecated
+ public int memoryType;
+
+ /** Window flag: as long as this window is visible to the user, allow
+ * the lock screen to activate while the screen is on.
+ * This can be used independently, or in combination with
+ * {@link #FLAG_KEEP_SCREEN_ON} and/or {@link #FLAG_SHOW_WHEN_LOCKED} */
+ public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;
+
+ /** Window flag: everything behind this window will be dimmed.
+ * Use {@link #dimAmount} to control the amount of dim. */
+ public static final int FLAG_DIM_BEHIND = 0x00000002;
+
+ /** Window flag: enable blur behind for this window. */
+ public static final int FLAG_BLUR_BEHIND = 0x00000004;
+
+ /** Window flag: this window won't ever get key input focus, so the
+ * user can not send key or other button events to it. Those will
+ * instead go to whatever focusable window is behind it. This flag
+ * will also enable {@link #FLAG_NOT_TOUCH_MODAL} whether or not that
+ * is explicitly set.
+ *
+ * <p>Setting this flag also implies that the window will not need to
+ * interact with
+ * a soft input method, so it will be Z-ordered and positioned
+ * independently of any active input method (typically this means it
+ * gets Z-ordered on top of the input method, so it can use the full
+ * screen for its content and cover the input method if needed. You
+ * can use {@link #FLAG_ALT_FOCUSABLE_IM} to modify this behavior. */
+ public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
+
+ /**
+ * Window flag: this window can never receive touch events.
+ *
+ * <p>The intention of this flag is to leave the touch to be handled by some window below
+ * this window (in Z order).
+ *
+ * <p>Starting from Android {@link Build.VERSION_CODES#S}, for security reasons, touch
+ * events that pass through windows containing this flag (ie. are within the bounds of the
+ * window) will only be delivered to the touch-consuming window if one (or more) of the
+ * items below are true:
+ * <ol>
+ * <li><b>Same UID</b>: This window belongs to the same UID that owns the touch-consuming
+ * window.
+ * <li><b>Trusted windows</b>: This window is trusted. Trusted windows include (but are
+ * not limited to) accessibility windows ({@link #TYPE_ACCESSIBILITY_OVERLAY}), the IME
+ * ({@link #TYPE_INPUT_METHOD}) and assistant windows (TYPE_VOICE_INTERACTION). Windows of
+ * type {@link #TYPE_APPLICATION_OVERLAY} are <b>not</b> trusted, see below.
+ * <li><b>Invisible windows</b>: This window is {@link View#GONE} or
+ * {@link View#INVISIBLE}.
+ * <li><b>Fully transparent windows</b>: This window has {@link LayoutParams#alpha} equal
+ * to 0.
+ * <li><b>One SAW window with enough transparency</b>: This window is of type {@link
+ * #TYPE_APPLICATION_OVERLAY}, has {@link LayoutParams#alpha} below or equal to the
+ * <a href="#MaximumOpacity">maximum obscuring opacity</a> (see below) and it's the
+ * <b>only</b> window of type {@link #TYPE_APPLICATION_OVERLAY} from this UID in the touch
+ * path.
+ * <li><b>Multiple SAW windows with enough transparency</b>: The multiple overlapping
+ * {@link #TYPE_APPLICATION_OVERLAY} windows in the
+ * touch path from this UID have a <b>combined obscuring opacity</b> below or equal to
+ * the <a href="#MaximumOpacity">maximum obscuring opacity</a>. See section
+ * <a href="#ObscuringOpacity">Combined obscuring opacity</a> below on how to compute this
+ * value.
+ * </ol>
+ * <p>If none of these cases hold, the touch will not be delivered and a message will be
+ * logged to logcat.</p>
+ *
+ * <a name="MaximumOpacity"></a>
+ * <h3>Maximum obscuring opacity</h3>
+ * <p>This value is <b>0.8</b>. Apps that want to gather this value from the system rather
+ * than hard-coding it might want to use {@link
+ * android.hardware.input.InputManager#getMaximumObscuringOpacityForTouch()}.</p>
+ *
+ * <a name="ObscuringOpacity"></a>
+ * <h3>Combined obscuring opacity</h3>
+ *
+ * <p>The <b>combined obscuring opacity</b> of a set of windows is obtained by combining the
+ * opacity values of all windows in the set using the associative and commutative operation
+ * defined as:
+ * <pre>
+ * opacity({A,B}) = 1 - (1 - opacity(A))*(1 - opacity(B))
+ * </pre>
+ * <p>where {@code opacity(X)} is the {@link LayoutParams#alpha} of window X. So, for a set
+ * of windows {@code {W1, .., Wn}}, the combined obscuring opacity will be:
+ * <pre>
+ * opacity({W1, .., Wn}) = 1 - (1 - opacity(W1)) * ... * (1 - opacity(Wn))
+ * </pre>
+ */
+ public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
+
+ /** Window flag: even when this window is focusable (its
+ * {@link #FLAG_NOT_FOCUSABLE} is not set), allow any pointer events
+ * outside of the window to be sent to the windows behind it. Otherwise
+ * it will consume all pointer events itself, regardless of whether they
+ * are inside of the window. */
+ public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
+
+ /** Window flag: when set, if the device is asleep when the touch
+ * screen is pressed, you will receive this first touch event. Usually
+ * the first touch event is consumed by the system since the user can
+ * not see what they are pressing on.
+ *
+ * @deprecated This flag has no effect.
+ */
+ @Deprecated
+ public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
+
+ /** Window flag: as long as this window is visible to the user, keep
+ * the device's screen turned on and bright. */
+ public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;
+
+ /**
+ * Window flag for attached windows: Place the window within the entire screen, ignoring
+ * any constraints from the parent window.
+ *
+ * <p>Note: on displays that have a {@link DisplayCutout}, the window may be placed
+ * such that it avoids the {@link DisplayCutout} area if necessary according to the
+ * {@link #layoutInDisplayCutoutMode}.
+ */
+ public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
+
+ /** Window flag: allow window to extend outside of the screen. */
+ public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
+
+ /**
+ * Window flag: hide all screen decorations (such as the status bar) while
+ * this window is displayed. This allows the window to use the entire
+ * display space for itself -- the status bar will be hidden when
+ * an app window with this flag set is on the top layer. A fullscreen window
+ * will ignore a value of {@link #SOFT_INPUT_ADJUST_RESIZE} for the window's
+ * {@link #softInputMode} field; the window will stay fullscreen
+ * and will not resize.
+ *
+ * <p>This flag can be controlled in your theme through the
+ * {@link android.R.attr#windowFullscreen} attribute; this attribute
+ * is automatically set for you in the standard fullscreen themes
+ * such as {@link android.R.style#Theme_NoTitleBar_Fullscreen},
+ * {@link android.R.style#Theme_Black_NoTitleBar_Fullscreen},
+ * {@link android.R.style#Theme_Light_NoTitleBar_Fullscreen},
+ * {@link android.R.style#Theme_Holo_NoActionBar_Fullscreen},
+ * {@link android.R.style#Theme_Holo_Light_NoActionBar_Fullscreen},
+ * {@link android.R.style#Theme_DeviceDefault_NoActionBar_Fullscreen}, and
+ * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_Fullscreen}.</p>
+ *
+ * @deprecated Use {@link WindowInsetsController#hide(int)} with {@link Type#statusBars()}
+ * instead.
+ */
+ @Deprecated
+ public static final int FLAG_FULLSCREEN = 0x00000400;
+
+ /**
+ * Window flag: override {@link #FLAG_FULLSCREEN} and force the
+ * screen decorations (such as the status bar) to be shown.
+ *
+ * @deprecated This value became API "by accident", and shouldn't be used by 3rd party
+ * applications.
+ */
+ @Deprecated
+ public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
+
+ /** Window flag: turn on dithering when compositing this window to
+ * the screen.
+ * @deprecated This flag is no longer used. */
+ @Deprecated
+ public static final int FLAG_DITHER = 0x00001000;
+
+ /** Window flag: treat the content of the window as secure, preventing
+ * it from appearing in screenshots or from being viewed on non-secure
+ * displays.
+ *
+ * <p>See {@link android.view.Display#FLAG_SECURE} for more details about
+ * secure surfaces and secure displays.
+ */
+ public static final int FLAG_SECURE = 0x00002000;
+
+ /** Window flag: a special mode where the layout parameters are used
+ * to perform scaling of the surface when it is composited to the
+ * screen. */
+ public static final int FLAG_SCALED = 0x00004000;
+
+ /** Window flag: intended for windows that will often be used when the user is
+ * holding the screen against their face, it will aggressively filter the event
+ * stream to prevent unintended presses in this situation that may not be
+ * desired for a particular window, when such an event stream is detected, the
+ * application will receive a CANCEL motion event to indicate this so applications
+ * can handle this accordingly by taking no action on the event
+ * until the finger is released. */
+ public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;
+
+ /**
+ * Window flag: a special option only for use in combination with
+ * {@link #FLAG_LAYOUT_IN_SCREEN}. When requesting layout in the
+ * screen your window may appear on top of or behind screen decorations
+ * such as the status bar. By also including this flag, the window
+ * manager will report the inset rectangle needed to ensure your
+ * content is not covered by screen decorations. This flag is normally
+ * set for you by Window as described in {@link Window#setFlags}
+ *
+ * @deprecated Insets will always be delivered to your application.
+ */
+ @Deprecated
+ public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;
+
+ /** Window flag: when set, inverts the input method focusability of the window.
+ *
+ * The effect of setting this flag depends on whether {@link #FLAG_NOT_FOCUSABLE} is set:
+ * <p>
+ * If {@link #FLAG_NOT_FOCUSABLE} is <em>not</em> set, i.e. when the window is focusable,
+ * setting this flag prevents this window from becoming the target of the input method.
+ * Consequently, it will <em>not</em> be able to interact with the input method,
+ * and will be layered above the input method (unless there is another input method
+ * target above it).
+ *
+ * <p>
+ * If {@link #FLAG_NOT_FOCUSABLE} <em>is</em> set, setting this flag requests for the window
+ * to be the input method target even though the window is <em>not</em> focusable.
+ * Consequently, it will be layered below the input method.
+ * Note: Windows that set {@link #FLAG_NOT_FOCUSABLE} cannot interact with the input method,
+ * regardless of this flag.
+ */
+ public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
+
+ /** Window flag: if you have set {@link #FLAG_NOT_TOUCH_MODAL}, you
+ * can set this flag to receive a single special MotionEvent with
+ * the action
+ * {@link MotionEvent#ACTION_OUTSIDE MotionEvent.ACTION_OUTSIDE} for
+ * touches that occur outside of your window. Note that you will not
+ * receive the full down/move/up gesture, only the location of the
+ * first down as an ACTION_OUTSIDE.
+ */
+ public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
+
+ /** Window flag: special flag to let windows be shown when the screen
+ * is locked. This will let application windows take precedence over
+ * key guard or any other lock screens. Can be used with
+ * {@link #FLAG_KEEP_SCREEN_ON} to turn screen on and display windows
+ * directly before showing the key guard window. Can be used with
+ * {@link #FLAG_DISMISS_KEYGUARD} to automatically fully dismisss
+ * non-secure keyguards. This flag only applies to the top-most
+ * full-screen window.
+ * @deprecated Use {@link android.R.attr#showWhenLocked} or
+ * {@link android.app.Activity#setShowWhenLocked(boolean)} instead to prevent an
+ * unintentional double life-cycle event.
+ */
+ @Deprecated
+ public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
+
+ /** Window flag: ask that the system wallpaper be shown behind
+ * your window. The window surface must be translucent to be able
+ * to actually see the wallpaper behind it; this flag just ensures
+ * that the wallpaper surface will be there if this window actually
+ * has translucent regions.
+ *
+ * <p>This flag can be controlled in your theme through the
+ * {@link android.R.attr#windowShowWallpaper} attribute; this attribute
+ * is automatically set for you in the standard wallpaper themes
+ * such as {@link android.R.style#Theme_Wallpaper},
+ * {@link android.R.style#Theme_Wallpaper_NoTitleBar},
+ * {@link android.R.style#Theme_Wallpaper_NoTitleBar_Fullscreen},
+ * {@link android.R.style#Theme_Holo_Wallpaper},
+ * {@link android.R.style#Theme_Holo_Wallpaper_NoTitleBar},
+ * {@link android.R.style#Theme_DeviceDefault_Wallpaper}, and
+ * {@link android.R.style#Theme_DeviceDefault_Wallpaper_NoTitleBar}.</p>
+ */
+ public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
+
+ /** Window flag: when set as a window is being added or made
+ * visible, once the window has been shown then the system will
+ * poke the power manager's user activity (as if the user had woken
+ * up the device) to turn the screen on.
+ * @deprecated Use {@link android.R.attr#turnScreenOn} or
+ * {@link android.app.Activity#setTurnScreenOn(boolean)} instead to prevent an
+ * unintentional double life-cycle event.
+ */
+ @Deprecated
+ public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
+
+ /**
+ * Window flag: when set the window will cause the keyguard to be
+ * dismissed, only if it is not a secure lock keyguard. Because such a
+ * keyguard is not needed for security, it will never re-appear if the
+ * user navigates to another window (in contrast to
+ * {@link #FLAG_SHOW_WHEN_LOCKED}, which will only temporarily hide both
+ * secure and non-secure keyguards but ensure they reappear when the
+ * user moves to another UI that doesn't hide them). If the keyguard is
+ * currently active and is secure (requires an unlock credential) than
+ * the user will still need to confirm it before seeing this window,
+ * unless {@link #FLAG_SHOW_WHEN_LOCKED} has also been set.
+ *
+ * @deprecated Use {@link #FLAG_SHOW_WHEN_LOCKED} or
+ * {@link KeyguardManager#requestDismissKeyguard} instead.
+ * Since keyguard was dismissed all the time as long as an
+ * activity with this flag on its window was focused,
+ * keyguard couldn't guard against unintentional touches on
+ * the screen, which isn't desired.
+ */
+ @Deprecated
+ public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
+
+ /** Window flag: when set the window will accept for touch events
+ * outside of its bounds to be sent to other windows that also
+ * support split touch. When this flag is not set, the first pointer
+ * that goes down determines the window to which all subsequent touches
+ * go until all pointers go up. When this flag is set, each pointer
+ * (not necessarily the first) that goes down determines the window
+ * to which all subsequent touches of that pointer will go until that
+ * pointer goes up thereby enabling touches with multiple pointers
+ * to be split across multiple windows.
+ */
+ public static final int FLAG_SPLIT_TOUCH = 0x00800000;
+
+ /**
+ * <p>Indicates whether this window should be hardware accelerated.
+ * Requesting hardware acceleration does not guarantee it will happen.</p>
+ *
+ * <p>This flag can be controlled programmatically <em>only</em> to enable
+ * hardware acceleration. To enable hardware acceleration for a given
+ * window programmatically, do the following:</p>
+ *
+ * <pre>
+ * Window w = activity.getWindow(); // in Activity's onCreate() for instance
+ * w.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ * WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
+ * </pre>
+ *
+ * <p>It is important to remember that this flag <strong>must</strong>
+ * be set before setting the content view of your activity or dialog.</p>
+ *
+ * <p>This flag cannot be used to disable hardware acceleration after it
+ * was enabled in your manifest using
+ * {@link android.R.attr#hardwareAccelerated}. If you need to selectively
+ * and programmatically disable hardware acceleration (for automated testing
+ * for instance), make sure it is turned off in your manifest and enable it
+ * on your activity or dialog when you need it instead, using the method
+ * described above.</p>
+ *
+ * <p>This flag is automatically set by the system if the
+ * {@link android.R.attr#hardwareAccelerated android:hardwareAccelerated}
+ * XML attribute is set to true on an activity or on the application.</p>
+ */
+ public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
+
+ /**
+ * Window flag: allow window contents to extend in to the screen's
+ * overscan area, if there is one. The window should still correctly
+ * position its contents to take the overscan area into account.
+ *
+ * <p>This flag can be controlled in your theme through the
+ * {@link android.R.attr#windowOverscan} attribute; this attribute
+ * is automatically set for you in the standard overscan themes
+ * such as
+ * {@link android.R.style#Theme_Holo_NoActionBar_Overscan},
+ * {@link android.R.style#Theme_Holo_Light_NoActionBar_Overscan},
+ * {@link android.R.style#Theme_DeviceDefault_NoActionBar_Overscan}, and
+ * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_Overscan}.</p>
+ *
+ * <p>When this flag is enabled for a window, its normal content may be obscured
+ * to some degree by the overscan region of the display. To ensure key parts of
+ * that content are visible to the user, you can use
+ * {@link View#setFitsSystemWindows(boolean) View.setFitsSystemWindows(boolean)}
+ * to set the point in the view hierarchy where the appropriate offsets should
+ * be applied. (This can be done either by directly calling this function, using
+ * the {@link android.R.attr#fitsSystemWindows} attribute in your view hierarchy,
+ * or implementing you own {@link View#fitSystemWindows(android.graphics.Rect)
+ * View.fitSystemWindows(Rect)} method).</p>
+ *
+ * <p>This mechanism for positioning content elements is identical to its equivalent
+ * use with layout and {@link View#setSystemUiVisibility(int)
+ * View.setSystemUiVisibility(int)}; here is an example layout that will correctly
+ * position its UI elements with this overscan flag is set:</p>
+ *
+ * {@sample development/samples/ApiDemos/res/layout/overscan_activity.xml complete}
+ *
+ * @deprecated Overscan areas aren't set by any Android product anymore as of Android 11.
+ */
+ @Deprecated
+ public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
+
+ /**
+ * Window flag: request a translucent status bar with minimal system-provided
+ * background protection.
+ *
+ * <p>This flag can be controlled in your theme through the
+ * {@link android.R.attr#windowTranslucentStatus} attribute; this attribute
+ * is automatically set for you in the standard translucent decor themes
+ * such as
+ * {@link android.R.style#Theme_Holo_NoActionBar_TranslucentDecor},
+ * {@link android.R.style#Theme_Holo_Light_NoActionBar_TranslucentDecor},
+ * {@link android.R.style#Theme_DeviceDefault_NoActionBar_TranslucentDecor}, and
+ * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_TranslucentDecor}.</p>
+ *
+ * <p>When this flag is enabled for a window, it automatically sets
+ * the system UI visibility flags {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.</p>
+ *
+ * <p>Note: For devices that support
+ * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE} this flag may be ignored.
+ *
+ * @deprecated Use {@link Window#setStatusBarColor(int)} with a half-translucent color
+ * instead.
+ */
+ @Deprecated
+ public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
+
+ /**
+ * Window flag: request a translucent navigation bar with minimal system-provided
+ * background protection.
+ *
+ * <p>This flag can be controlled in your theme through the
+ * {@link android.R.attr#windowTranslucentNavigation} attribute; this attribute
+ * is automatically set for you in the standard translucent decor themes
+ * such as
+ * {@link android.R.style#Theme_Holo_NoActionBar_TranslucentDecor},
+ * {@link android.R.style#Theme_Holo_Light_NoActionBar_TranslucentDecor},
+ * {@link android.R.style#Theme_DeviceDefault_NoActionBar_TranslucentDecor}, and
+ * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_TranslucentDecor}.</p>
+ *
+ * <p>When this flag is enabled for a window, it automatically sets
+ * the system UI visibility flags {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
+ * {@link View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.</p>
+ *
+ * <p>Note: For devices that support
+ * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE} this flag can be disabled
+ * by the car manufacturers.
+ *
+ * @deprecated Use {@link Window#setNavigationBarColor(int)} with a half-translucent color
+ * instead.
+ */
+ @Deprecated
+ public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;
+
+ /**
+ * Flag for a window in local focus mode.
+ * Window in local focus mode can control focus independent of window manager using
+ * {@link Window#setLocalFocus(boolean, boolean)}.
+ * Usually window in this mode will not get touch/key events from window manager, but will
+ * get events only via local injection using {@link Window#injectInputEvent(InputEvent)}.
+ */
+ public static final int FLAG_LOCAL_FOCUS_MODE = 0x10000000;
+
+ /** Window flag: Enable touches to slide out of a window into neighboring
+ * windows in mid-gesture instead of being captured for the duration of
+ * the gesture.
+ *
+ * This flag changes the behavior of touch focus for this window only.
+ * Touches can slide out of the window but they cannot necessarily slide
+ * back in (unless the other window with touch focus permits it).
+ *
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ public static final int FLAG_SLIPPERY = 0x20000000;
+
+ /**
+ * Window flag: When requesting layout with an attached window, the attached window may
+ * overlap with the screen decorations of the parent window such as the navigation bar. By
+ * including this flag, the window manager will layout the attached window within the decor
+ * frame of the parent window such that it doesn't overlap with screen decorations.
+ *
+ * @deprecated Use {@link #setFitInsetsTypes(int)} to determine whether the attached
+ * window will overlap with system bars.
+ */
+ @Deprecated
+ public static final int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000;
+
+ /**
+ * Flag indicating that this Window is responsible for drawing the background for the
+ * system bars. If set, the system bars are drawn with a transparent background and the
+ * corresponding areas in this window are filled with the colors specified in
+ * {@link Window#getStatusBarColor()} and {@link Window#getNavigationBarColor()}.
+ */
+ public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix = "FLAG_", value = {
+ FLAG_ALLOW_LOCK_WHILE_SCREEN_ON,
+ FLAG_DIM_BEHIND,
+ FLAG_BLUR_BEHIND,
+ FLAG_NOT_FOCUSABLE,
+ FLAG_NOT_TOUCHABLE,
+ FLAG_NOT_TOUCH_MODAL,
+ FLAG_TOUCHABLE_WHEN_WAKING,
+ FLAG_KEEP_SCREEN_ON,
+ FLAG_LAYOUT_IN_SCREEN,
+ FLAG_LAYOUT_NO_LIMITS,
+ FLAG_FULLSCREEN,
+ FLAG_FORCE_NOT_FULLSCREEN,
+ FLAG_DITHER,
+ FLAG_SECURE,
+ FLAG_SCALED,
+ FLAG_IGNORE_CHEEK_PRESSES,
+ FLAG_LAYOUT_INSET_DECOR,
+ FLAG_ALT_FOCUSABLE_IM,
+ FLAG_WATCH_OUTSIDE_TOUCH,
+ FLAG_SHOW_WHEN_LOCKED,
+ FLAG_SHOW_WALLPAPER,
+ FLAG_TURN_SCREEN_ON,
+ FLAG_DISMISS_KEYGUARD,
+ FLAG_SPLIT_TOUCH,
+ FLAG_HARDWARE_ACCELERATED,
+ FLAG_LAYOUT_IN_OVERSCAN,
+ FLAG_TRANSLUCENT_STATUS,
+ FLAG_TRANSLUCENT_NAVIGATION,
+ FLAG_LOCAL_FOCUS_MODE,
+ FLAG_SLIPPERY,
+ FLAG_LAYOUT_ATTACHED_IN_DECOR,
+ FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Flags {}
+
+ /**
+ * Various behavioral options/flags. Default is none.
+ *
+ * @see #FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ * @see #FLAG_DIM_BEHIND
+ * @see #FLAG_NOT_FOCUSABLE
+ * @see #FLAG_NOT_TOUCHABLE
+ * @see #FLAG_NOT_TOUCH_MODAL
+ * @see #FLAG_TOUCHABLE_WHEN_WAKING
+ * @see #FLAG_KEEP_SCREEN_ON
+ * @see #FLAG_LAYOUT_IN_SCREEN
+ * @see #FLAG_LAYOUT_NO_LIMITS
+ * @see #FLAG_FULLSCREEN
+ * @see #FLAG_FORCE_NOT_FULLSCREEN
+ * @see #FLAG_SECURE
+ * @see #FLAG_SCALED
+ * @see #FLAG_IGNORE_CHEEK_PRESSES
+ * @see #FLAG_LAYOUT_INSET_DECOR
+ * @see #FLAG_ALT_FOCUSABLE_IM
+ * @see #FLAG_WATCH_OUTSIDE_TOUCH
+ * @see #FLAG_SHOW_WHEN_LOCKED
+ * @see #FLAG_SHOW_WALLPAPER
+ * @see #FLAG_TURN_SCREEN_ON
+ * @see #FLAG_DISMISS_KEYGUARD
+ * @see #FLAG_SPLIT_TOUCH
+ * @see #FLAG_HARDWARE_ACCELERATED
+ * @see #FLAG_LOCAL_FOCUS_MODE
+ * @see #FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
+ */
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(mask = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, equals = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON,
+ name = "ALLOW_LOCK_WHILE_SCREEN_ON"),
+ @ViewDebug.FlagToString(mask = FLAG_DIM_BEHIND, equals = FLAG_DIM_BEHIND,
+ name = "DIM_BEHIND"),
+ @ViewDebug.FlagToString(mask = FLAG_BLUR_BEHIND, equals = FLAG_BLUR_BEHIND,
+ name = "BLUR_BEHIND"),
+ @ViewDebug.FlagToString(mask = FLAG_NOT_FOCUSABLE, equals = FLAG_NOT_FOCUSABLE,
+ name = "NOT_FOCUSABLE"),
+ @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCHABLE, equals = FLAG_NOT_TOUCHABLE,
+ name = "NOT_TOUCHABLE"),
+ @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCH_MODAL, equals = FLAG_NOT_TOUCH_MODAL,
+ name = "NOT_TOUCH_MODAL"),
+ @ViewDebug.FlagToString(mask = FLAG_TOUCHABLE_WHEN_WAKING, equals = FLAG_TOUCHABLE_WHEN_WAKING,
+ name = "TOUCHABLE_WHEN_WAKING"),
+ @ViewDebug.FlagToString(mask = FLAG_KEEP_SCREEN_ON, equals = FLAG_KEEP_SCREEN_ON,
+ name = "KEEP_SCREEN_ON"),
+ @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_SCREEN, equals = FLAG_LAYOUT_IN_SCREEN,
+ name = "LAYOUT_IN_SCREEN"),
+ @ViewDebug.FlagToString(mask = FLAG_LAYOUT_NO_LIMITS, equals = FLAG_LAYOUT_NO_LIMITS,
+ name = "LAYOUT_NO_LIMITS"),
+ @ViewDebug.FlagToString(mask = FLAG_FULLSCREEN, equals = FLAG_FULLSCREEN,
+ name = "FULLSCREEN"),
+ @ViewDebug.FlagToString(mask = FLAG_FORCE_NOT_FULLSCREEN, equals = FLAG_FORCE_NOT_FULLSCREEN,
+ name = "FORCE_NOT_FULLSCREEN"),
+ @ViewDebug.FlagToString(mask = FLAG_DITHER, equals = FLAG_DITHER,
+ name = "DITHER"),
+ @ViewDebug.FlagToString(mask = FLAG_SECURE, equals = FLAG_SECURE,
+ name = "SECURE"),
+ @ViewDebug.FlagToString(mask = FLAG_SCALED, equals = FLAG_SCALED,
+ name = "SCALED"),
+ @ViewDebug.FlagToString(mask = FLAG_IGNORE_CHEEK_PRESSES, equals = FLAG_IGNORE_CHEEK_PRESSES,
+ name = "IGNORE_CHEEK_PRESSES"),
+ @ViewDebug.FlagToString(mask = FLAG_LAYOUT_INSET_DECOR, equals = FLAG_LAYOUT_INSET_DECOR,
+ name = "LAYOUT_INSET_DECOR"),
+ @ViewDebug.FlagToString(mask = FLAG_ALT_FOCUSABLE_IM, equals = FLAG_ALT_FOCUSABLE_IM,
+ name = "ALT_FOCUSABLE_IM"),
+ @ViewDebug.FlagToString(mask = FLAG_WATCH_OUTSIDE_TOUCH, equals = FLAG_WATCH_OUTSIDE_TOUCH,
+ name = "WATCH_OUTSIDE_TOUCH"),
+ @ViewDebug.FlagToString(mask = FLAG_SHOW_WHEN_LOCKED, equals = FLAG_SHOW_WHEN_LOCKED,
+ name = "SHOW_WHEN_LOCKED"),
+ @ViewDebug.FlagToString(mask = FLAG_SHOW_WALLPAPER, equals = FLAG_SHOW_WALLPAPER,
+ name = "SHOW_WALLPAPER"),
+ @ViewDebug.FlagToString(mask = FLAG_TURN_SCREEN_ON, equals = FLAG_TURN_SCREEN_ON,
+ name = "TURN_SCREEN_ON"),
+ @ViewDebug.FlagToString(mask = FLAG_DISMISS_KEYGUARD, equals = FLAG_DISMISS_KEYGUARD,
+ name = "DISMISS_KEYGUARD"),
+ @ViewDebug.FlagToString(mask = FLAG_SPLIT_TOUCH, equals = FLAG_SPLIT_TOUCH,
+ name = "SPLIT_TOUCH"),
+ @ViewDebug.FlagToString(mask = FLAG_HARDWARE_ACCELERATED, equals = FLAG_HARDWARE_ACCELERATED,
+ 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 = "TRANSLUCENT_STATUS"),
+ @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_NAVIGATION, equals = 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 = "DRAWS_SYSTEM_BAR_BACKGROUNDS")
+ }, formatToHexString = true)
+ @Flags
+ public int flags;
+
+ /**
+ * In the system process, we globally do not use hardware acceleration
+ * because there are many threads doing UI there and they conflict.
+ * If certain parts of the UI that really do want to use hardware
+ * acceleration, this flag can be set to force it. This is basically
+ * for the lock screen. Anyone else using it, you are probably wrong.
+ *
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED = 0x00000002;
+
+ /**
+ * By default, wallpapers are sent new offsets when the wallpaper is scrolled. Wallpapers
+ * may elect to skip these notifications if they are not doing anything productive with
+ * them (they do not affect the wallpaper scrolling operation) by calling
+ * {@link
+ * android.service.wallpaper.WallpaperService.Engine#setOffsetNotificationsEnabled(boolean)}.
+ *
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS = 0x00000004;
+
+ /**
+ * When set {@link LayoutParams#TYPE_APPLICATION_OVERLAY} windows will stay visible, even if
+ * {@link LayoutParams#SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS} is set for another
+ * visible window.
+ * @hide
+ */
+ @RequiresPermission(permission.SYSTEM_APPLICATION_OVERLAY)
+ public static final int PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY = 0x00000008;
+
+ /** In a multiuser system if this flag is set and the owner is a system process then this
+ * window will appear on all user screens. This overrides the default behavior of window
+ * types that normally only appear on the owning user's screen. Refer to each window type
+ * to determine its default behavior.
+ *
+ * {@hide} */
+ @SystemApi
+ @RequiresPermission(permission.INTERNAL_SYSTEM_WINDOW)
+ public static final int SYSTEM_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;
+
+ /**
+ * Never animate position changes of the window.
+ *
+ * {@hide}
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 0x00000040;
+
+ /** Window flag: special flag to limit the size of the window to be
+ * original size ([320x480] x density). Used to create window for applications
+ * running under compatibility mode.
+ *
+ * {@hide} */
+ public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 0x00000080;
+
+ /** Window flag: a special option intended for system dialogs. When
+ * this flag is set, the window will demand focus unconditionally when
+ * it is created.
+ * {@hide} */
+ public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
+
+ /**
+ * Flag that prevents the wallpaper behind the current window from receiving touch events.
+ *
+ * {@hide}
+ */
+ public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 0x00000800;
+
+ /**
+ * Flag to force the status bar window to be visible all the time. If the bar is hidden when
+ * this flag is set it will be shown again.
+ * This can only be set by {@link LayoutParams#TYPE_STATUS_BAR}.
+ *
+ * {@hide}
+ */
+ public static final int PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR = 0x00001000;
+
+ /**
+ * Flag that will make window ignore app visibility and instead depend purely on the decor
+ * view visibility for determining window visibility. This is used by recents to keep
+ * drawing after it launches an app.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY = 0x00004000;
+
+ /**
+ * Flag to indicate that this window is not expected to be replaced across
+ * configuration change triggered activity relaunches. In general the WindowManager
+ * expects Windows to be replaced after relaunch, and thus it will preserve their surfaces
+ * until the replacement is ready to show in order to prevent visual glitch. However
+ * some windows, such as PopupWindows expect to be cleared across configuration change,
+ * and thus should hint to the WindowManager that it should not wait for a replacement.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH = 0x00008000;
+
+ /**
+ * Flag to indicate that this child window should always be laid-out in the parent
+ * frame regardless of the current windowing mode configuration.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME = 0x00010000;
+
+ /**
+ * Flag to indicate that this window is always drawing the status bar background, no matter
+ * what the other flags are.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS = 0x00020000;
+
+ /**
+ * Flag to indicate that this window needs Sustained Performance Mode if
+ * the device supports it.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE = 0x00040000;
+
+ /**
+ * Flag to indicate that any window added by an application process that is of type
+ * {@link #TYPE_TOAST} or that requires
+ * {@link android.app.AppOpsManager#OP_SYSTEM_ALERT_WINDOW} permission should be hidden when
+ * this window is visible.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS)
+ public static final int SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 0x00080000;
+
+ /**
+ * Indicates that this window is the rounded corners overlay present on some
+ * devices this means that it will be excluded from: screenshots,
+ * screen magnification, and mirroring.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY = 0x00100000;
+
+ /**
+ * Flag to prevent the window from being magnified by the accessibility magnifier.
+ *
+ * TODO(b/190623172): This is a temporary solution and need to find out another way instead.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_NOT_MAGNIFIABLE = 0x00400000;
+
+ /**
+ * Flag to indicate that the status bar window is in a state such that it forces showing
+ * the navigation bar unless the navigation bar window is explicitly set to
+ * {@link View#GONE}.
+ * It only takes effects if this is set by {@link LayoutParams#TYPE_STATUS_BAR}.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION = 0x00800000;
+
+ /**
+ * Flag to indicate that the window is color space agnostic, and the color can be
+ * interpreted to any color space.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC = 0x01000000;
+
+ /**
+ * Flag to request creation of a BLAST (Buffer as LayerState) Layer.
+ * If not specified the client will receive a BufferQueue layer.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_USE_BLAST = 0x02000000;
+
+ /**
+ * Flag to indicate that the window is controlling the appearance of system bars. So we
+ * don't need to adjust it by reading its system UI flags for compatibility.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_APPEARANCE_CONTROLLED = 0x04000000;
+
+ /**
+ * Flag to indicate that the window is controlling the behavior of system bars. So we don't
+ * need to adjust it by reading its window flags or system UI flags for compatibility.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_BEHAVIOR_CONTROLLED = 0x08000000;
+
+ /**
+ * Flag to indicate that the window is controlling how it fits window insets on its own.
+ * So we don't need to adjust its attributes for fitting window insets.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_FIT_INSETS_CONTROLLED = 0x10000000;
+
+ /**
+ * Flag to indicate that the window is a trusted overlay.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_TRUSTED_OVERLAY = 0x20000000;
+
+ /**
+ * Flag to indicate that the parent frame of a window should be inset by IME.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME = 0x40000000;
+
+ /**
+ * Flag to indicate that we want to intercept and handle global drag and drop for all users.
+ * This flag allows a window to considered for drag events even if not visible, and will
+ * receive drags for all active users in the system.
+ *
+ * Additional data is provided to windows with this flag, including the {@link ClipData}
+ * including all items with the {@link DragEvent#ACTION_DRAG_STARTED} event, and the
+ * actual drag surface with the {@link DragEvent#ACTION_DROP} event. If the window consumes,
+ * the drop, then the cleanup of the drag surface (provided as a part of
+ * {@link DragEvent#ACTION_DROP}) will be relinquished to the window.
+ * @hide
+ */
+ @RequiresPermission(permission.MANAGE_ACTIVITY_TASKS)
+ public static final int PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP = 0x80000000;
+
+ /**
+ * An internal annotation for flags that can be specified to {@link #softInputMode}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "SYSTEM_FLAG_" }, value = {
+ SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+ SYSTEM_FLAG_SHOW_FOR_ALL_USERS,
+ })
+ public @interface SystemFlags {}
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix="PRIVATE_FLAG_", value = {
+ PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED,
+ PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS,
+ SYSTEM_FLAG_SHOW_FOR_ALL_USERS,
+ PRIVATE_FLAG_NO_MOVE_ANIMATION,
+ PRIVATE_FLAG_COMPATIBLE_WINDOW,
+ PRIVATE_FLAG_SYSTEM_ERROR,
+ PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
+ PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
+ PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY,
+ PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH,
+ PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME,
+ PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS,
+ PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
+ SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+ PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
+ PRIVATE_FLAG_NOT_MAGNIFIABLE,
+ PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
+ PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
+ PRIVATE_FLAG_USE_BLAST,
+ PRIVATE_FLAG_APPEARANCE_CONTROLLED,
+ PRIVATE_FLAG_BEHAVIOR_CONTROLLED,
+ PRIVATE_FLAG_FIT_INSETS_CONTROLLED,
+ PRIVATE_FLAG_TRUSTED_OVERLAY,
+ PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME,
+ PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP,
+ PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY,
+ })
+ public @interface PrivateFlags {}
+
+ /**
+ * Control flags that are private to the platform.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @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 = SYSTEM_FLAG_SHOW_FOR_ALL_USERS,
+ equals = SYSTEM_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_DISABLE_WALLPAPER_TOUCH_EVENTS,
+ equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
+ name = "DISABLE_WALLPAPER_TOUCH_EVENTS"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
+ equals = PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
+ name = "FORCE_STATUS_BAR_VISIBLE"),
+ @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_BAR_BACKGROUNDS,
+ equals = PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS,
+ 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 = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+ equals = SYSTEM_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_NOT_MAGNIFIABLE,
+ equals = PRIVATE_FLAG_NOT_MAGNIFIABLE,
+ name = "NOT_MAGNIFIABLE"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
+ equals = PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
+ name = "STATUS_FORCE_SHOW_NAVIGATION"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
+ equals = PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
+ name = "COLOR_SPACE_AGNOSTIC"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_USE_BLAST,
+ equals = PRIVATE_FLAG_USE_BLAST,
+ name = "USE_BLAST"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_APPEARANCE_CONTROLLED,
+ equals = PRIVATE_FLAG_APPEARANCE_CONTROLLED,
+ name = "APPEARANCE_CONTROLLED"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_BEHAVIOR_CONTROLLED,
+ equals = PRIVATE_FLAG_BEHAVIOR_CONTROLLED,
+ name = "BEHAVIOR_CONTROLLED"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_FIT_INSETS_CONTROLLED,
+ equals = PRIVATE_FLAG_FIT_INSETS_CONTROLLED,
+ name = "FIT_INSETS_CONTROLLED"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_TRUSTED_OVERLAY,
+ equals = PRIVATE_FLAG_TRUSTED_OVERLAY,
+ name = "TRUSTED_OVERLAY"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME,
+ equals = PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME,
+ name = "INSET_PARENT_FRAME_BY_IME"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP,
+ equals = PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP,
+ name = "INTERCEPT_GLOBAL_DRAG_AND_DROP"),
+ @ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY,
+ equals = PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY,
+ name = "PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY")
+ })
+ @PrivateFlags
+ @TestApi
+ public int privateFlags;
+
+ /**
+ * Given a particular set of window manager flags, determine whether
+ * such a window may be a target for an input method when it has
+ * focus. In particular, this checks the
+ * {@link #FLAG_NOT_FOCUSABLE} and {@link #FLAG_ALT_FOCUSABLE_IM}
+ * flags and returns true if the combination of the two corresponds
+ * to a window that can use the input method.
+ *
+ * @param flags The current window manager flags.
+ *
+ * @return Returns {@code true} if a window with the given flags would be able to
+ * use the input method, {@code false} if not.
+ */
+ public static boolean mayUseInputMethod(int flags) {
+ return (flags & FLAG_NOT_FOCUSABLE) != FLAG_NOT_FOCUSABLE
+ && (flags & FLAG_ALT_FOCUSABLE_IM) != FLAG_ALT_FOCUSABLE_IM;
+ }
+
+ /**
+ * Mask for {@link #softInputMode} of the bits that determine the
+ * desired visibility state of the soft input area for this window.
+ */
+ public static final int SOFT_INPUT_MASK_STATE = 0x0f;
+
+ /**
+ * Visibility state for {@link #softInputMode}: no state has been specified. The system may
+ * show or hide the software keyboard for better user experience when the window gains
+ * focus.
+ */
+ public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
+
+ /**
+ * Visibility state for {@link #softInputMode}: please don't change the state of
+ * the soft input area.
+ */
+ public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
+
+ /**
+ * Visibility state for {@link #softInputMode}: please hide any soft input
+ * area when normally appropriate (when the user is navigating
+ * forward to your window).
+ */
+ public static final int SOFT_INPUT_STATE_HIDDEN = 2;
+
+ /**
+ * Visibility state for {@link #softInputMode}: please always hide any
+ * soft input area when this window receives focus.
+ */
+ public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
+
+ /**
+ * Visibility state for {@link #softInputMode}: please show the soft
+ * input area when normally appropriate (when the user is navigating
+ * forward to your window).
+ *
+ * <p>Applications that target {@link android.os.Build.VERSION_CODES#P} and later, this flag
+ * is ignored unless there is a focused view that returns {@code true} from
+ * {@link View#isInEditMode()} when the window is focused.</p>
+ */
+ public static final int SOFT_INPUT_STATE_VISIBLE = 4;
+
+ /**
+ * Visibility state for {@link #softInputMode}: please always make the
+ * soft input area visible when this window receives input focus.
+ *
+ * <p>Applications that target {@link android.os.Build.VERSION_CODES#P} and later, this flag
+ * is ignored unless there is a focused view that returns {@code true} from
+ * {@link View#isInEditMode()} when the window is focused.</p>
+ */
+ public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
+
+ /**
+ * Mask for {@link #softInputMode} of the bits that determine the
+ * way that the window should be adjusted to accommodate the soft
+ * input window.
+ */
+ public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
+
+ /** Adjustment option for {@link #softInputMode}: nothing specified.
+ * The system will try to pick one or
+ * the other depending on the contents of the window.
+ */
+ public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
+
+ /** Adjustment option for {@link #softInputMode}: set to allow the
+ * window to be resized when an input
+ * method is shown, so that its contents are not covered by the input
+ * method. This can <em>not</em> be combined with
+ * {@link #SOFT_INPUT_ADJUST_PAN}; if
+ * neither of these are set, then the system will try to pick one or
+ * the other depending on the contents of the window. If the window's
+ * layout parameter flags include {@link #FLAG_FULLSCREEN}, this
+ * value for {@link #softInputMode} will be ignored; the window will
+ * not resize, but will stay fullscreen.
+ *
+ * @deprecated Call {@link Window#setDecorFitsSystemWindows(boolean)} with {@code false} and
+ * install an {@link OnApplyWindowInsetsListener} on your root content view that fits insets
+ * of type {@link Type#ime()}.
+ */
+ @Deprecated
+ public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
+
+ /** Adjustment option for {@link #softInputMode}: set to have a window
+ * pan when an input method is
+ * shown, so it doesn't need to deal with resizing but just panned
+ * by the framework to ensure the current input focus is visible. This
+ * can <em>not</em> be combined with {@link #SOFT_INPUT_ADJUST_RESIZE}; if
+ * neither of these are set, then the system will try to pick one or
+ * the other depending on the contents of the window.
+ */
+ public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
+
+ /** Adjustment option for {@link #softInputMode}: set to have a window
+ * not adjust for a shown input method. The window will not be resized,
+ * and it will not be panned to make its focus visible.
+ */
+ public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
+
+ /**
+ * Bit for {@link #softInputMode}: set when the user has navigated
+ * forward to the window. This is normally set automatically for
+ * you by the system, though you may want to set it in certain cases
+ * when you are displaying a window yourself. This flag will always
+ * be cleared automatically after the window is displayed.
+ */
+ public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;
+
+ /**
+ * An internal annotation for flags that can be specified to {@link #softInputMode}.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "SOFT_INPUT_" }, value = {
+ SOFT_INPUT_STATE_UNSPECIFIED,
+ SOFT_INPUT_STATE_UNCHANGED,
+ SOFT_INPUT_STATE_HIDDEN,
+ SOFT_INPUT_STATE_ALWAYS_HIDDEN,
+ SOFT_INPUT_STATE_VISIBLE,
+ SOFT_INPUT_STATE_ALWAYS_VISIBLE,
+ SOFT_INPUT_ADJUST_UNSPECIFIED,
+ SOFT_INPUT_ADJUST_RESIZE,
+ SOFT_INPUT_ADJUST_PAN,
+ SOFT_INPUT_ADJUST_NOTHING,
+ SOFT_INPUT_IS_FORWARD_NAVIGATION,
+ })
+ public @interface SoftInputModeFlags {}
+
+ /**
+ * Desired operating mode for any soft input area. May be any combination
+ * of:
+ *
+ * <ul>
+ * <li> One of the visibility states
+ * {@link #SOFT_INPUT_STATE_UNSPECIFIED}, {@link #SOFT_INPUT_STATE_UNCHANGED},
+ * {@link #SOFT_INPUT_STATE_HIDDEN}, {@link #SOFT_INPUT_STATE_ALWAYS_HIDDEN},
+ * {@link #SOFT_INPUT_STATE_VISIBLE}, or {@link #SOFT_INPUT_STATE_ALWAYS_VISIBLE}.
+ * <li> One of the adjustment options
+ * {@link #SOFT_INPUT_ADJUST_UNSPECIFIED}, {@link #SOFT_INPUT_ADJUST_RESIZE},
+ * {@link #SOFT_INPUT_ADJUST_PAN}, or {@link #SOFT_INPUT_ADJUST_NOTHING}.
+ * </ul>
+ *
+ *
+ * <p>This flag can be controlled in your theme through the
+ * {@link android.R.attr#windowSoftInputMode} attribute.</p>
+ */
+ @SoftInputModeFlags
+ public int softInputMode;
+
+ /**
+ * Placement of window within the screen as per {@link Gravity}. Both
+ * {@link Gravity#apply(int, int, int, android.graphics.Rect, int, int,
+ * android.graphics.Rect) Gravity.apply} and
+ * {@link Gravity#applyDisplay(int, android.graphics.Rect, android.graphics.Rect)
+ * Gravity.applyDisplay} are used during window layout, with this value
+ * given as the desired gravity. For example you can specify
+ * {@link Gravity#DISPLAY_CLIP_HORIZONTAL Gravity.DISPLAY_CLIP_HORIZONTAL} and
+ * {@link Gravity#DISPLAY_CLIP_VERTICAL Gravity.DISPLAY_CLIP_VERTICAL} here
+ * to control the behavior of
+ * {@link Gravity#applyDisplay(int, android.graphics.Rect, android.graphics.Rect)
+ * Gravity.applyDisplay}.
+ *
+ * @see Gravity
+ */
+ @GravityFlags
+ public int gravity;
+
+ /**
+ * The horizontal margin, as a percentage of the container's width,
+ * between the container and the widget. See
+ * {@link Gravity#apply(int, int, int, android.graphics.Rect, int, int,
+ * android.graphics.Rect) Gravity.apply} for how this is used. This
+ * field is added with {@link #x} to supply the <var>xAdj</var> parameter.
+ */
+ public float horizontalMargin;
+
+ /**
+ * The vertical margin, as a percentage of the container's height,
+ * between the container and the widget. See
+ * {@link Gravity#apply(int, int, int, android.graphics.Rect, int, int,
+ * android.graphics.Rect) Gravity.apply} for how this is used. This
+ * field is added with {@link #y} to supply the <var>yAdj</var> parameter.
+ */
+ public float verticalMargin;
+
+ /**
+ * Positive insets between the drawing surface and window content.
+ *
+ * @hide
+ */
+ public final Rect surfaceInsets = new Rect();
+
+ /**
+ * Whether the surface insets have been manually set. When set to
+ * {@code false}, the view root will automatically determine the
+ * appropriate surface insets.
+ *
+ * @see #surfaceInsets
+ * @hide
+ */
+ public boolean hasManualSurfaceInsets;
+
+ /**
+ * Whether we should use global insets state when report insets to the window. When set to
+ * {@code true}, all the insets will be reported to the window regardless of the z-order.
+ * Otherwise, only the insets above the given window will be reported.
+ *
+ * @hide
+ */
+ public boolean receiveInsetsIgnoringZOrder;
+
+ /**
+ * Whether the previous surface insets should be used vs. what is currently set. When set
+ * to {@code true}, the view root will ignore surfaces insets in this object and use what
+ * it currently has.
+ *
+ * @see #surfaceInsets
+ * @hide
+ */
+ public boolean preservePreviousSurfaceInsets = true;
+
+ /**
+ * The desired bitmap format. May be one of the constants in
+ * {@link android.graphics.PixelFormat}. The choice of format
+ * might be overridden by {@link #setColorMode(int)}. Default is OPAQUE.
+ */
+ public int format;
+
+ /**
+ * A style resource defining the animations to use for this window.
+ * This must be a system resource; it can not be an application resource
+ * because the window manager does not have access to applications.
+ */
+ public int windowAnimations;
+
+ /**
+ * An alpha value to apply to this entire window.
+ * An alpha of 1.0 means fully opaque and 0.0 means fully transparent
+ */
+ public float alpha = 1.0f;
+
+ /**
+ * When {@link #FLAG_DIM_BEHIND} is set, this is the amount of dimming
+ * to apply. Range is from 1.0 for completely opaque to 0.0 for no
+ * dim.
+ */
+ public float dimAmount = 1.0f;
+
+ /**
+ * Default value for {@link #screenBrightness} and {@link #buttonBrightness}
+ * indicating that the brightness value is not overridden for this window
+ * and normal brightness policy should be used.
+ */
+ public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f;
+
+ /**
+ * Value for {@link #screenBrightness} and {@link #buttonBrightness}
+ * indicating that the screen or button backlight brightness should be set
+ * to the lowest value when this window is in front.
+ */
+ public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f;
+
+ /**
+ * Value for {@link #screenBrightness} and {@link #buttonBrightness}
+ * indicating that the screen or button backlight brightness should be set
+ * to the hightest value when this window is in front.
+ */
+ public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f;
+
+ /**
+ * This can be used to override the user's preferred brightness of
+ * the screen. A value of less than 0, the default, means to use the
+ * preferred screen brightness. 0 to 1 adjusts the brightness from
+ * dark to full bright.
+ */
+ public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE;
+
+ /**
+ * This can be used to override the standard behavior of the button and
+ * keyboard backlights. A value of less than 0, the default, means to
+ * use the standard backlight behavior. 0 to 1 adjusts the brightness
+ * from dark to full bright.
+ */
+ public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE;
+
+ /**
+ * Unspecified value for {@link #rotationAnimation} indicating
+ * a lack of preference.
+ * @hide
+ */
+ public static final int ROTATION_ANIMATION_UNSPECIFIED = -1;
+
+ /**
+ * Value for {@link #rotationAnimation} which specifies that this
+ * window will visually rotate in or out following a rotation.
+ */
+ public static final int ROTATION_ANIMATION_ROTATE = 0;
+
+ /**
+ * Value for {@link #rotationAnimation} which specifies that this
+ * window will fade in or out following a rotation.
+ */
+ public static final int ROTATION_ANIMATION_CROSSFADE = 1;
+
+ /**
+ * Value for {@link #rotationAnimation} which specifies that this window
+ * will immediately disappear or appear following a rotation.
+ */
+ public static final int ROTATION_ANIMATION_JUMPCUT = 2;
+
+ /**
+ * Value for {@link #rotationAnimation} to specify seamless rotation mode.
+ * This works like JUMPCUT but will fall back to CROSSFADE if rotation
+ * can't be applied without pausing the screen. For example, this is ideal
+ * for Camera apps which don't want the viewfinder contents to ever rotate
+ * or fade (and rather to be seamless) but also don't want ROTATION_ANIMATION_JUMPCUT
+ * during app transition scenarios where seamless rotation can't be applied.
+ */
+ public static final int ROTATION_ANIMATION_SEAMLESS = 3;
+
+ /**
+ * Define the exit and entry animations used on this window when the device is rotated.
+ * This only has an affect if the incoming and outgoing topmost
+ * opaque windows have the #FLAG_FULLSCREEN bit set and are not covered
+ * by other windows. All other situations default to the
+ * {@link #ROTATION_ANIMATION_ROTATE} behavior.
+ *
+ * @see #ROTATION_ANIMATION_ROTATE
+ * @see #ROTATION_ANIMATION_CROSSFADE
+ * @see #ROTATION_ANIMATION_JUMPCUT
+ */
+ public int rotationAnimation = ROTATION_ANIMATION_ROTATE;
+
+ /**
+ * Identifier for this window. This will usually be filled in for
+ * you.
+ */
+ public IBinder token = null;
+
+ /**
+ * The token of {@link android.window.WindowContext}. It is usually a
+ * {@link android.app.WindowTokenClient} and is used for associating the params with an
+ * existing node in the WindowManager hierarchy and getting the corresponding
+ * {@link Configuration} and {@link android.content.res.Resources} values with updates
+ * propagated from the server side.
+ *
+ * @hide
+ */
+ @Nullable
+ public IBinder mWindowContextToken = null;
+
+ /**
+ * Name of the package owning this window.
+ */
+ public String packageName = null;
+
+ /**
+ * Specific orientation value for a window.
+ * May be any of the same values allowed
+ * for {@link android.content.pm.ActivityInfo#screenOrientation}.
+ * If not set, a default value of
+ * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED}
+ * will be used.
+ */
+ @ActivityInfo.ScreenOrientation
+ public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+ /**
+ * The preferred refresh rate for the window.
+ *
+ * This must be one of the supported refresh rates obtained for the display(s) the window
+ * is on. The selected refresh rate will be applied to the display's default mode.
+ *
+ * This value is ignored if {@link #preferredDisplayModeId} is set.
+ *
+ * @see Display#getSupportedRefreshRates()
+ * @deprecated use {@link #preferredDisplayModeId} instead
+ */
+ @Deprecated
+ public float preferredRefreshRate;
+
+ /**
+ * Id of the preferred display mode for the window.
+ * <p>
+ * This must be one of the supported modes obtained for the display(s) the window is on.
+ * A value of {@code 0} means no preference.
+ *
+ * @see Display#getSupportedModes()
+ * @see Display.Mode#getModeId()
+ */
+ public int preferredDisplayModeId;
+
+ /**
+ * The min display refresh rate while the window is in focus.
+ *
+ * This value is ignored if {@link #preferredDisplayModeId} is set.
+ * @hide
+ */
+ public float preferredMinDisplayRefreshRate;
+
+ /**
+ * The max display refresh rate while the window is in focus.
+ *
+ * This value is ignored if {@link #preferredDisplayModeId} is set.
+ * @hide
+ */
+ public float preferredMaxDisplayRefreshRate;
+
+ /**
+ * An internal annotation for flags that can be specified to {@link #systemUiVisibility}
+ * and {@link #subtreeSystemUiVisibility}.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "" }, value = {
+ SYSTEM_UI_FLAG_VISIBLE,
+ SYSTEM_UI_FLAG_LOW_PROFILE,
+ SYSTEM_UI_FLAG_HIDE_NAVIGATION,
+ SYSTEM_UI_FLAG_FULLSCREEN,
+ SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
+ SYSTEM_UI_FLAG_LAYOUT_STABLE,
+ SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,
+ SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,
+ SYSTEM_UI_FLAG_IMMERSIVE,
+ SYSTEM_UI_FLAG_IMMERSIVE_STICKY,
+ SYSTEM_UI_FLAG_LIGHT_STATUS_BAR,
+ STATUS_BAR_DISABLE_EXPAND,
+ STATUS_BAR_DISABLE_NOTIFICATION_ICONS,
+ STATUS_BAR_DISABLE_NOTIFICATION_ALERTS,
+ STATUS_BAR_DISABLE_NOTIFICATION_TICKER,
+ STATUS_BAR_DISABLE_SYSTEM_INFO,
+ STATUS_BAR_DISABLE_HOME,
+ STATUS_BAR_DISABLE_BACK,
+ STATUS_BAR_DISABLE_CLOCK,
+ STATUS_BAR_DISABLE_RECENT,
+ STATUS_BAR_DISABLE_SEARCH,
+ })
+ public @interface SystemUiVisibilityFlags {}
+
+ /**
+ * Control the visibility of the status bar.
+ *
+ * @see View#STATUS_BAR_VISIBLE
+ * @see View#STATUS_BAR_HIDDEN
+ *
+ * @deprecated SystemUiVisibility flags are deprecated. Use {@link WindowInsetsController}
+ * instead.
+ */
+ @SystemUiVisibilityFlags
+ @Deprecated
+ public int systemUiVisibility;
+
+ /**
+ * @hide
+ * The ui visibility as requested by the views in this hierarchy.
+ * the combined value should be systemUiVisibility | subtreeSystemUiVisibility.
+ */
+ @SystemUiVisibilityFlags
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int subtreeSystemUiVisibility;
+
+ /**
+ * Get callbacks about the system ui visibility changing.
+ *
+ * TODO: Maybe there should be a bitfield of optional callbacks that we need.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean hasSystemUiListeners;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS})
+ @interface LayoutInDisplayCutoutMode {}
+
+ /**
+ * Controls how the window is laid out if there is a {@link DisplayCutout}.
+ *
+ * <p>
+ * Defaults to {@link #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT}.
+ *
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ * @see DisplayCutout
+ * @see android.R.attr#windowLayoutInDisplayCutoutMode
+ * android:windowLayoutInDisplayCutoutMode
+ */
+ @LayoutInDisplayCutoutMode
+ public int layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+
+ /**
+ * The window is allowed to extend into the {@link DisplayCutout} area, only if the
+ * {@link DisplayCutout} is fully contained within a system bar. Otherwise, the window is
+ * laid out such that it does not overlap with the {@link DisplayCutout} area.
+ *
+ * <p>
+ * In practice, this means that if the window did not set {@link #FLAG_FULLSCREEN} or
+ * {@link View#SYSTEM_UI_FLAG_FULLSCREEN}, it can extend into the cutout area in portrait
+ * if the cutout is at the top edge. Similarly for
+ * {@link View#SYSTEM_UI_FLAG_HIDE_NAVIGATION} and a cutout at the bottom of the screen.
+ * Otherwise (i.e. fullscreen or landscape) it is laid out such that it does not overlap the
+ * cutout area.
+ *
+ * <p>
+ * The usual precautions for not overlapping with the status and navigation bar are
+ * sufficient for ensuring that no important content overlaps with the DisplayCutout.
+ *
+ * @see DisplayCutout
+ * @see WindowInsets
+ * @see #layoutInDisplayCutoutMode
+ * @see android.R.attr#windowLayoutInDisplayCutoutMode
+ * android:windowLayoutInDisplayCutoutMode
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0;
+
+ /**
+ * The window is always allowed to extend into the {@link DisplayCutout} areas on the short
+ * edges of the screen.
+ *
+ * The window will never extend into a {@link DisplayCutout} area on the long edges of the
+ * screen.
+ *
+ * <p>
+ * The window must make sure that no important content overlaps with the
+ * {@link DisplayCutout}.
+ *
+ * <p>
+ * In this mode, the window extends under cutouts on the short edge of the display in both
+ * portrait and landscape, regardless of whether the window is hiding the system bars:<br/>
+ * <img src="{@docRoot}reference/android/images/display_cutout/short_edge/fullscreen_top_no_letterbox.png"
+ * height="720"
+ * alt="Screenshot of a fullscreen activity on a display with a cutout at the top edge in
+ * portrait, no letterbox is applied."/>
+ *
+ * <img src="{@docRoot}reference/android/images/display_cutout/short_edge/landscape_top_no_letterbox.png"
+ * width="720"
+ * alt="Screenshot of an activity on a display with a cutout at the top edge in landscape,
+ * no letterbox is applied."/>
+ *
+ * <p>
+ * A cutout in the corner is considered to be on the short edge: <br/>
+ * <img src="{@docRoot}reference/android/images/display_cutout/short_edge/fullscreen_corner_no_letterbox.png"
+ * height="720"
+ * alt="Screenshot of a fullscreen activity on a display with a cutout in the corner in
+ * portrait, no letterbox is applied."/>
+ *
+ * <p>
+ * On the other hand, should the cutout be on the long edge of the display, a letterbox will
+ * be applied such that the window does not extend into the cutout on either long edge:
+ * <br/>
+ * <img src="{@docRoot}reference/android/images/display_cutout/short_edge/portrait_side_letterbox.png"
+ * height="720"
+ * alt="Screenshot of an activity on a display with a cutout on the long edge in portrait,
+ * letterbox is applied."/>
+ *
+ * @see DisplayCutout
+ * @see WindowInsets#getDisplayCutout()
+ * @see #layoutInDisplayCutoutMode
+ * @see android.R.attr#windowLayoutInDisplayCutoutMode
+ * android:windowLayoutInDisplayCutoutMode
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES = 1;
+
+ /**
+ * The window is never allowed to overlap with the DisplayCutout area.
+ *
+ * <p>
+ * This should be used with windows that transiently set
+ * {@link View#SYSTEM_UI_FLAG_FULLSCREEN} or {@link View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}
+ * to avoid a relayout of the window when the respective flag is set or cleared.
+ *
+ * @see DisplayCutout
+ * @see #layoutInDisplayCutoutMode
+ * @see android.R.attr#windowLayoutInDisplayCutoutMode
+ * android:windowLayoutInDisplayCutoutMode
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;
+
+ /**
+ * The window is always allowed to extend into the {@link DisplayCutout} areas on the all
+ * edges of the screen.
+ *
+ * <p>
+ * The window must make sure that no important content overlaps with the
+ * {@link DisplayCutout}.
+ *
+ * <p>
+ * In this mode, the window extends under cutouts on the all edges of the display in both
+ * portrait and landscape, regardless of whether the window is hiding the system bars.
+ *
+ * @see DisplayCutout
+ * @see WindowInsets#getDisplayCutout()
+ * @see #layoutInDisplayCutoutMode
+ * @see android.R.attr#windowLayoutInDisplayCutoutMode
+ * android:windowLayoutInDisplayCutoutMode
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 3;
+
+ /**
+ * When this window has focus, disable touch pad pointer gesture processing.
+ * The window will receive raw position updates from the touch pad instead
+ * of pointer movements and synthetic touch events.
+ *
+ * @hide
+ */
+ public static final int INPUT_FEATURE_DISABLE_POINTER_GESTURES = 0x00000001;
+
+ /**
+ * Does not construct an input channel for this window. The channel will therefore
+ * be incapable of receiving input.
+ *
+ * @hide
+ */
+ public static final int INPUT_FEATURE_NO_INPUT_CHANNEL = 0x00000002;
+
+ /**
+ * When this window has focus, does not call user activity for all input events so
+ * the application will have to do it itself. Should only be used by
+ * the keyguard and phone app.
+ * <p>
+ * Should only be used by the keyguard and phone app.
+ * </p>
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int INPUT_FEATURE_DISABLE_USER_ACTIVITY = 0x00000004;
+
+ /**
+ * An internal annotation for flags that can be specified to {@link #inputFeatures}.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "INPUT_FEATURE_" }, value = {
+ INPUT_FEATURE_DISABLE_POINTER_GESTURES,
+ INPUT_FEATURE_NO_INPUT_CHANNEL,
+ INPUT_FEATURE_DISABLE_USER_ACTIVITY,
+ })
+ public @interface InputFeatureFlags {}
+
+ /**
+ * Control special features of the input subsystem.
+ *
+ * @see #INPUT_FEATURE_DISABLE_POINTER_GESTURES
+ * @see #INPUT_FEATURE_NO_INPUT_CHANNEL
+ * @see #INPUT_FEATURE_DISABLE_USER_ACTIVITY
+ * @hide
+ */
+ @InputFeatureFlags
+ @UnsupportedAppUsage
+ public int inputFeatures;
+
+ /**
+ * Sets the number of milliseconds before the user activity timeout occurs
+ * when this window has focus. A value of -1 uses the standard timeout.
+ * A value of 0 uses the minimum support display timeout.
+ * <p>
+ * This property can only be used to reduce the user specified display timeout;
+ * it can never make the timeout longer than it normally would be.
+ * </p><p>
+ * Should only be used by the keyguard and phone app.
+ * </p>
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public long userActivityTimeout = -1;
+
+ /**
+ * For windows with an anchor (e.g. PopupWindow), keeps track of the View that anchors the
+ * window.
+ *
+ * @hide
+ */
+ public long accessibilityIdOfAnchor = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+
+ /**
+ * The window title isn't kept in sync with what is displayed in the title bar, so we
+ * separately track the currently shown title to provide to accessibility.
+ *
+ * @hide
+ */
+ @TestApi
+ public CharSequence accessibilityTitle;
+
+ /**
+ * Sets a timeout in milliseconds before which the window will be hidden
+ * by the window manager. Useful for transient notifications like toasts
+ * so we don't have to rely on client cooperation to ensure the window
+ * is hidden. Must be specified at window creation time. Note that apps
+ * are not prepared to handle their windows being removed without their
+ * explicit request and may try to interact with the removed window
+ * resulting in undefined behavior and crashes. Therefore, we do hide
+ * such windows to prevent them from overlaying other apps.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public long hideTimeoutMilliseconds = -1;
+
+ /**
+ * Indicates whether this window wants the connected display to do minimal post processing
+ * on the produced image or video frames. This will only be requested if the window is
+ * visible on the screen.
+ *
+ * <p>This setting should be used when low latency has a higher priority than image
+ * enhancement processing (e.g. for games or video conferencing).
+ *
+ * <p>If the Display sink is connected via HDMI, the device will begin to send infoframes
+ * with Auto Low Latency Mode enabled and Game Content Type. This will switch the connected
+ * display to a minimal image processing mode (if available), which reduces latency,
+ * improving the user experience for gaming or video conferencing applications. For more
+ * information, see HDMI 2.1 specification.
+ *
+ * <p>If the Display sink has an internal connection or uses some other protocol than HDMI,
+ * effects may be similar but implementation-defined.
+ *
+ * <p>The ability to switch to a mode with minimal post proessing may be disabled by a user
+ * setting in the system settings menu. In that case, this field is ignored and the display
+ * will remain in its current mode.
+ *
+ * @see android.content.pm.ActivityInfo#FLAG_PREFER_MINIMAL_POST_PROCESSING
+ * @see android.view.Display#isMinimalPostProcessingSupported
+ * @see android.view.Window#setPreferMinimalPostProcessing
+ */
+ public boolean preferMinimalPostProcessing = false;
+
+ /**
+ * Specifies the amount of blur to be used to blur everything behind the window.
+ * The effect is similar to the dimAmount, but instead of dimming, the content behind
+ * will be blurred.
+ *
+ * The blur behind radius range starts at 0, which means no blur, and increases until 150
+ * for the densest blur.
+ *
+ * @see #setBlurBehindRadius
+ */
+ private int mBlurBehindRadius = 0;
+
+ /**
+ * The color mode requested by this window. The target display may
+ * not be able to honor the request. When the color mode is not set
+ * to {@link ActivityInfo#COLOR_MODE_DEFAULT}, it might override the
+ * pixel format specified in {@link #format}.
+ *
+ * @hide
+ */
+ @ActivityInfo.ColorMode
+ private int mColorMode = COLOR_MODE_DEFAULT;
+
+ /**
+ * Carries the requests about {@link WindowInsetsController.Appearance} and
+ * {@link WindowInsetsController.Behavior} to the system windows which can produce insets.
+ *
+ * @hide
+ */
+ public final InsetsFlags insetsFlags = new InsetsFlags();
+
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(
+ mask = STATUS_BARS,
+ equals = STATUS_BARS,
+ name = "STATUS_BARS"),
+ @ViewDebug.FlagToString(
+ mask = NAVIGATION_BARS,
+ equals = NAVIGATION_BARS,
+ name = "NAVIGATION_BARS"),
+ @ViewDebug.FlagToString(
+ mask = CAPTION_BAR,
+ equals = CAPTION_BAR,
+ name = "CAPTION_BAR"),
+ @ViewDebug.FlagToString(
+ mask = IME,
+ equals = IME,
+ name = "IME"),
+ @ViewDebug.FlagToString(
+ mask = SYSTEM_GESTURES,
+ equals = SYSTEM_GESTURES,
+ name = "SYSTEM_GESTURES"),
+ @ViewDebug.FlagToString(
+ mask = MANDATORY_SYSTEM_GESTURES,
+ equals = MANDATORY_SYSTEM_GESTURES,
+ name = "MANDATORY_SYSTEM_GESTURES"),
+ @ViewDebug.FlagToString(
+ mask = TAPPABLE_ELEMENT,
+ equals = TAPPABLE_ELEMENT,
+ name = "TAPPABLE_ELEMENT"),
+ @ViewDebug.FlagToString(
+ mask = WINDOW_DECOR,
+ equals = WINDOW_DECOR,
+ name = "WINDOW_DECOR")
+ })
+ private @InsetsType int mFitInsetsTypes = Type.systemBars();
+
+ @ViewDebug.ExportedProperty(flagMapping = {
+ @ViewDebug.FlagToString(
+ mask = LEFT,
+ equals = LEFT,
+ name = "LEFT"),
+ @ViewDebug.FlagToString(
+ mask = TOP,
+ equals = TOP,
+ name = "TOP"),
+ @ViewDebug.FlagToString(
+ mask = RIGHT,
+ equals = RIGHT,
+ name = "RIGHT"),
+ @ViewDebug.FlagToString(
+ mask = BOTTOM,
+ equals = BOTTOM,
+ name = "BOTTOM")
+ })
+ private @InsetsSide int mFitInsetsSides = Side.all();
+
+ private boolean mFitInsetsIgnoringVisibility = false;
+
+ /**
+ * {@link InsetsState.InternalInsetsType}s to be applied to the window
+ * If {@link #type} has the predefined insets (like {@link #TYPE_STATUS_BAR} or
+ * {@link #TYPE_NAVIGATION_BAR}), this field will be ignored.
+ *
+ * <p>Note: provide only one inset corresponding to the window type (like
+ * {@link InsetsState.InternalInsetsType#ITYPE_STATUS_BAR} or
+ * {@link InsetsState.InternalInsetsType#ITYPE_NAVIGATION_BAR})</p>
+ * @hide
+ */
+ public @InsetsState.InternalInsetsType int[] providesInsetsTypes;
+
+ /**
+ * Specifies types of insets that this window should avoid overlapping during layout.
+ *
+ * @param types which {@link WindowInsets.Type}s of insets that this window should avoid.
+ * The initial value of this object includes all system bars.
+ */
+ public void setFitInsetsTypes(@InsetsType int types) {
+ mFitInsetsTypes = types;
+ privateFlags |= PRIVATE_FLAG_FIT_INSETS_CONTROLLED;
+ }
+
+ /**
+ * Specifies sides of insets that this window should avoid overlapping during layout.
+ *
+ * @param sides which sides that this window should avoid overlapping with the types
+ * specified. The initial value of this object includes all sides.
+ */
+ public void setFitInsetsSides(@InsetsSide int sides) {
+ mFitInsetsSides = sides;
+ privateFlags |= PRIVATE_FLAG_FIT_INSETS_CONTROLLED;
+ }
+
+ /**
+ * Specifies if this window should fit the window insets no matter they are visible or not.
+ *
+ * @param ignore if true, this window will fit the given types even if they are not visible.
+ */
+ public void setFitInsetsIgnoringVisibility(boolean ignore) {
+ mFitInsetsIgnoringVisibility = ignore;
+ privateFlags |= PRIVATE_FLAG_FIT_INSETS_CONTROLLED;
+ }
+
+ /**
+ * Specifies that the window should be considered a trusted system overlay. Trusted system
+ * overlays are ignored when considering whether windows are obscured during input
+ * dispatch. Requires the {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW}
+ * permission.
+ *
+ * {@see android.view.MotionEvent#FLAG_WINDOW_IS_OBSCURED}
+ * {@see android.view.MotionEvent#FLAG_WINDOW_IS_PARTIALLY_OBSCURED}
+ * @hide
+ */
+ public void setTrustedOverlay() {
+ privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY;
+ }
+
+ /**
+ * When set on {@link LayoutParams#TYPE_APPLICATION_OVERLAY} windows they stay visible,
+ * even if {@link LayoutParams#SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS} is set for
+ * another visible window.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(permission.SYSTEM_APPLICATION_OVERLAY)
+ public void setSystemApplicationOverlay(boolean isSystemApplicationOverlay) {
+ if (isSystemApplicationOverlay) {
+ privateFlags |= PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY;
+ } else {
+ privateFlags &= ~PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY;
+ }
+ }
+
+ /**
+ * Returns if this window is marked as being a system application overlay.
+ * @see LayoutParams#setSystemApplicationOverlay(boolean)
+ *
+ * <p>Note: the owner of the window must hold
+ * {@link android.Manifest.permission#SYSTEM_APPLICATION_OVERLAY} for this to have any
+ * effect.
+ * @hide
+ */
+ @SystemApi
+ public boolean isSystemApplicationOverlay() {
+ return (privateFlags & PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY)
+ == PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY;
+ }
+
+ /**
+ * @return the {@link WindowInsets.Type}s that this window is avoiding overlapping.
+ */
+ public @InsetsType int getFitInsetsTypes() {
+ return mFitInsetsTypes;
+ }
+
+ /**
+ * @return the sides that this window is avoiding overlapping.
+ */
+ public @InsetsSide int getFitInsetsSides() {
+ return mFitInsetsSides;
+ }
+
+ /**
+ * @return {@code true} if this window fits the window insets no matter they are visible or
+ * not.
+ */
+ public boolean isFitInsetsIgnoringVisibility() {
+ return mFitInsetsIgnoringVisibility;
+ }
+
+ public LayoutParams() {
+ super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ type = TYPE_APPLICATION;
+ format = PixelFormat.OPAQUE;
+ }
+
+ public LayoutParams(int _type) {
+ super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ type = _type;
+ format = PixelFormat.OPAQUE;
+ }
+
+ public LayoutParams(int _type, int _flags) {
+ super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ type = _type;
+ flags = _flags;
+ format = PixelFormat.OPAQUE;
+ }
+
+ public LayoutParams(int _type, int _flags, int _format) {
+ super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ type = _type;
+ flags = _flags;
+ format = _format;
+ }
+
+ public LayoutParams(int w, int h, int _type, int _flags, int _format) {
+ super(w, h);
+ type = _type;
+ flags = _flags;
+ format = _format;
+ }
+
+ public LayoutParams(int w, int h, int xpos, int ypos, int _type,
+ int _flags, int _format) {
+ super(w, h);
+ x = xpos;
+ y = ypos;
+ type = _type;
+ flags = _flags;
+ format = _format;
+ }
+
+ public final void setTitle(CharSequence title) {
+ if (null == title)
+ title = "";
+
+ mTitle = TextUtils.stringOrSpannedString(title);
+ }
+
+ public final CharSequence getTitle() {
+ return mTitle != null ? mTitle : "";
+ }
+
+ /**
+ * Sets the surface insets based on the elevation (visual z position) of the input view.
+ * @hide
+ */
+ public final void setSurfaceInsets(View view, boolean manual, boolean preservePrevious) {
+ final int surfaceInset = (int) Math.ceil(view.getZ() * 2);
+ // Partial workaround for b/28318973. Every inset change causes a freeform window
+ // to jump a little for a few frames. If we never allow surface insets to decrease,
+ // they will stabilize quickly (often from the very beginning, as most windows start
+ // as focused).
+ // TODO(b/22668382) to fix this properly.
+ if (surfaceInset == 0) {
+ // OK to have 0 (this is the case for non-freeform windows).
+ surfaceInsets.set(0, 0, 0, 0);
+ } else {
+ surfaceInsets.set(
+ Math.max(surfaceInset, surfaceInsets.left),
+ Math.max(surfaceInset, surfaceInsets.top),
+ Math.max(surfaceInset, surfaceInsets.right),
+ Math.max(surfaceInset, surfaceInsets.bottom));
+ }
+ hasManualSurfaceInsets = manual;
+ preservePreviousSurfaceInsets = preservePrevious;
+ }
+
+ /**
+ * <p>Set the color mode of the window. Setting the color mode might
+ * override the window's pixel {@link WindowManager.LayoutParams#format format}.</p>
+ *
+ * <p>The color mode must be one of {@link ActivityInfo#COLOR_MODE_DEFAULT},
+ * {@link ActivityInfo#COLOR_MODE_WIDE_COLOR_GAMUT} or
+ * {@link ActivityInfo#COLOR_MODE_HDR}.</p>
+ *
+ * @see #getColorMode()
+ */
+ public void setColorMode(@ActivityInfo.ColorMode int colorMode) {
+ mColorMode = colorMode;
+ }
+
+ /**
+ * Returns the color mode of the window, one of {@link ActivityInfo#COLOR_MODE_DEFAULT},
+ * {@link ActivityInfo#COLOR_MODE_WIDE_COLOR_GAMUT} or {@link ActivityInfo#COLOR_MODE_HDR}.
+ *
+ * @see #setColorMode(int)
+ */
+ @ActivityInfo.ColorMode
+ public int getColorMode() {
+ return mColorMode;
+ }
+
+ /**
+ * <p>
+ * Blurs the screen behind the window. The effect is similar to that of {@link #dimAmount},
+ * but instead of dimmed, the content behind the window will be blurred (or combined with
+ * the dim amount, if such is specified).
+ * </p><p>
+ * The density of the blur is set by the blur radius. The radius defines the size
+ * of the neighbouring area, from which pixels will be averaged to form the final
+ * color for each pixel. The operation approximates a Gaussian blur.
+ * A radius of 0 means no blur. The higher the radius, the denser the blur.
+ * </p><p>
+ * Note the difference with {@link android.view.Window#setBackgroundBlurRadius},
+ * which blurs only within the bounds of the window. Blur behind blurs the whole screen
+ * behind the window.
+ * </p><p>
+ * Requires {@link #FLAG_BLUR_BEHIND} to be set.
+ * </p><p>
+ * Cross-window blur might not be supported by some devices due to GPU limitations. It can
+ * also be disabled at runtime, e.g. during battery saving mode, when multimedia tunneling
+ * is used or when minimal post processing is requested. In such situations, no blur will
+ * be computed or drawn, resulting in there being no depth separation between the window
+ * and the content behind it. To avoid this, the app might want to use more
+ * {@link #dimAmount} on its window. To listen for cross-window blur enabled/disabled
+ * events, use {@link #addCrossWindowBlurEnabledListener}.
+ * </p>
+ * @param blurBehindRadius The blur radius to use for blur behind in pixels
+ *
+ * @see #FLAG_BLUR_BEHIND
+ * @see #getBlurBehindRadius
+ * @see WindowManager#addCrossWindowBlurEnabledListener
+ * @see Window#setBackgroundBlurRadius
+ */
+ public void setBlurBehindRadius(@IntRange(from = 0) int blurBehindRadius) {
+ mBlurBehindRadius = blurBehindRadius;
+ }
+
+ /**
+ * Returns the blur behind radius of the window.
+ *
+ * @see #setBlurBehindRadius
+ */
+ public int getBlurBehindRadius() {
+ return mBlurBehindRadius;
+ }
+
+ /** @hide */
+ @SystemApi
+ public final void setUserActivityTimeout(long timeout) {
+ userActivityTimeout = timeout;
+ }
+
+ /** @hide */
+ @SystemApi
+ public final long getUserActivityTimeout() {
+ return userActivityTimeout;
+ }
+
+ /**
+ * Sets the {@link android.app.WindowContext} token.
+ *
+ * @see #getWindowContextToken()
+ *
+ * @hide
+ */
+ @TestApi
+ public final void setWindowContextToken(@NonNull IBinder token) {
+ mWindowContextToken = token;
+ }
+
+ /**
+ * Gets the {@link android.app.WindowContext} token.
+ *
+ * The token is usually a {@link android.app.WindowTokenClient} and is used for associating
+ * the params with an existing node in the WindowManager hierarchy and getting the
+ * corresponding {@link Configuration} and {@link android.content.res.Resources} values with
+ * updates propagated from the server side.
+ *
+ * @see android.app.WindowTokenClient
+ * @see Context#createWindowContext(Display, int, Bundle)
+ *
+ * @hide
+ */
+ @TestApi
+ @Nullable
+ public final IBinder getWindowContextToken() {
+ return mWindowContextToken;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int parcelableFlags) {
+ out.writeInt(width);
+ out.writeInt(height);
+ out.writeInt(x);
+ out.writeInt(y);
+ out.writeInt(type);
+ out.writeInt(flags);
+ out.writeInt(privateFlags);
+ out.writeInt(softInputMode);
+ out.writeInt(layoutInDisplayCutoutMode);
+ out.writeInt(gravity);
+ out.writeFloat(horizontalMargin);
+ out.writeFloat(verticalMargin);
+ out.writeInt(format);
+ out.writeInt(windowAnimations);
+ out.writeFloat(alpha);
+ out.writeFloat(dimAmount);
+ out.writeFloat(screenBrightness);
+ out.writeFloat(buttonBrightness);
+ out.writeInt(rotationAnimation);
+ out.writeStrongBinder(token);
+ out.writeStrongBinder(mWindowContextToken);
+ out.writeString(packageName);
+ TextUtils.writeToParcel(mTitle, out, parcelableFlags);
+ out.writeInt(screenOrientation);
+ out.writeFloat(preferredRefreshRate);
+ out.writeInt(preferredDisplayModeId);
+ out.writeFloat(preferredMinDisplayRefreshRate);
+ out.writeFloat(preferredMaxDisplayRefreshRate);
+ out.writeInt(systemUiVisibility);
+ out.writeInt(subtreeSystemUiVisibility);
+ out.writeBoolean(hasSystemUiListeners);
+ out.writeInt(inputFeatures);
+ out.writeLong(userActivityTimeout);
+ out.writeInt(surfaceInsets.left);
+ out.writeInt(surfaceInsets.top);
+ out.writeInt(surfaceInsets.right);
+ out.writeInt(surfaceInsets.bottom);
+ out.writeBoolean(hasManualSurfaceInsets);
+ out.writeBoolean(receiveInsetsIgnoringZOrder);
+ out.writeBoolean(preservePreviousSurfaceInsets);
+ out.writeLong(accessibilityIdOfAnchor);
+ TextUtils.writeToParcel(accessibilityTitle, out, parcelableFlags);
+ out.writeInt(mColorMode);
+ out.writeLong(hideTimeoutMilliseconds);
+ out.writeInt(insetsFlags.appearance);
+ out.writeInt(insetsFlags.behavior);
+ out.writeInt(mFitInsetsTypes);
+ out.writeInt(mFitInsetsSides);
+ out.writeBoolean(mFitInsetsIgnoringVisibility);
+ out.writeBoolean(preferMinimalPostProcessing);
+ out.writeInt(mBlurBehindRadius);
+ if (providesInsetsTypes != null) {
+ out.writeInt(providesInsetsTypes.length);
+ out.writeIntArray(providesInsetsTypes);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<LayoutParams> CREATOR
+ = new Parcelable.Creator<LayoutParams>() {
+ public LayoutParams createFromParcel(Parcel in) {
+ return new LayoutParams(in);
+ }
+
+ public LayoutParams[] newArray(int size) {
+ return new LayoutParams[size];
+ }
+ };
+
+
+ public LayoutParams(Parcel in) {
+ width = in.readInt();
+ height = in.readInt();
+ x = in.readInt();
+ y = in.readInt();
+ type = in.readInt();
+ flags = in.readInt();
+ privateFlags = in.readInt();
+ softInputMode = in.readInt();
+ layoutInDisplayCutoutMode = in.readInt();
+ gravity = in.readInt();
+ horizontalMargin = in.readFloat();
+ verticalMargin = in.readFloat();
+ format = in.readInt();
+ windowAnimations = in.readInt();
+ alpha = in.readFloat();
+ dimAmount = in.readFloat();
+ screenBrightness = in.readFloat();
+ buttonBrightness = in.readFloat();
+ rotationAnimation = in.readInt();
+ token = in.readStrongBinder();
+ mWindowContextToken = in.readStrongBinder();
+ packageName = in.readString();
+ mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ screenOrientation = in.readInt();
+ preferredRefreshRate = in.readFloat();
+ preferredDisplayModeId = in.readInt();
+ preferredMinDisplayRefreshRate = in.readFloat();
+ preferredMaxDisplayRefreshRate = in.readFloat();
+ systemUiVisibility = in.readInt();
+ subtreeSystemUiVisibility = in.readInt();
+ hasSystemUiListeners = in.readBoolean();
+ inputFeatures = in.readInt();
+ userActivityTimeout = in.readLong();
+ surfaceInsets.left = in.readInt();
+ surfaceInsets.top = in.readInt();
+ surfaceInsets.right = in.readInt();
+ surfaceInsets.bottom = in.readInt();
+ hasManualSurfaceInsets = in.readBoolean();
+ receiveInsetsIgnoringZOrder = in.readBoolean();
+ preservePreviousSurfaceInsets = in.readBoolean();
+ accessibilityIdOfAnchor = in.readLong();
+ accessibilityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mColorMode = in.readInt();
+ hideTimeoutMilliseconds = in.readLong();
+ insetsFlags.appearance = in.readInt();
+ insetsFlags.behavior = in.readInt();
+ mFitInsetsTypes = in.readInt();
+ mFitInsetsSides = in.readInt();
+ mFitInsetsIgnoringVisibility = in.readBoolean();
+ preferMinimalPostProcessing = in.readBoolean();
+ mBlurBehindRadius = in.readInt();
+ int insetsTypesLength = in.readInt();
+ if (insetsTypesLength > 0) {
+ providesInsetsTypes = new int[insetsTypesLength];
+ in.readIntArray(providesInsetsTypes);
+ }
+ }
+
+ @SuppressWarnings({"PointlessBitwiseExpression"})
+ public static final int LAYOUT_CHANGED = 1<<0;
+ public static final int TYPE_CHANGED = 1<<1;
+ public static final int FLAGS_CHANGED = 1<<2;
+ public static final int FORMAT_CHANGED = 1<<3;
+ public static final int ANIMATION_CHANGED = 1<<4;
+ public static final int DIM_AMOUNT_CHANGED = 1<<5;
+ public static final int TITLE_CHANGED = 1<<6;
+ public static final int ALPHA_CHANGED = 1<<7;
+ public static final int MEMORY_TYPE_CHANGED = 1<<8;
+ public static final int SOFT_INPUT_MODE_CHANGED = 1<<9;
+ public static final int SCREEN_ORIENTATION_CHANGED = 1<<10;
+ public static final int SCREEN_BRIGHTNESS_CHANGED = 1<<11;
+ public static final int ROTATION_ANIMATION_CHANGED = 1<<12;
+ /** {@hide} */
+ public static final int BUTTON_BRIGHTNESS_CHANGED = 1<<13;
+ /** {@hide} */
+ public static final int SYSTEM_UI_VISIBILITY_CHANGED = 1<<14;
+ /** {@hide} */
+ public static final int SYSTEM_UI_LISTENER_CHANGED = 1<<15;
+ /** {@hide} */
+ public static final int INPUT_FEATURES_CHANGED = 1<<16;
+ /** {@hide} */
+ public static final int PRIVATE_FLAGS_CHANGED = 1<<17;
+ /** {@hide} */
+ public static final int USER_ACTIVITY_TIMEOUT_CHANGED = 1<<18;
+ /** {@hide} */
+ public static final int TRANSLUCENT_FLAGS_CHANGED = 1<<19;
+ /** {@hide} */
+ public static final int SURFACE_INSETS_CHANGED = 1<<20;
+ /** {@hide} */
+ public static final int PREFERRED_REFRESH_RATE_CHANGED = 1 << 21;
+ /** {@hide} */
+ public static final int PREFERRED_DISPLAY_MODE_ID = 1 << 23;
+ /** {@hide} */
+ public static final int ACCESSIBILITY_ANCHOR_CHANGED = 1 << 24;
+ /** {@hide} */
+ @TestApi
+ public static final int ACCESSIBILITY_TITLE_CHANGED = 1 << 25;
+ /** {@hide} */
+ public static final int COLOR_MODE_CHANGED = 1 << 26;
+ /** {@hide} */
+ public static final int INSET_FLAGS_CHANGED = 1 << 27;
+ /** {@hide} */
+ public static final int MINIMAL_POST_PROCESSING_PREFERENCE_CHANGED = 1 << 28;
+ /** {@hide} */
+ public static final int BLUR_BEHIND_RADIUS_CHANGED = 1 << 29;
+ /** {@hide} */
+ public static final int PREFERRED_MIN_DISPLAY_REFRESH_RATE = 1 << 30;
+ /** {@hide} */
+ public static final int PREFERRED_MAX_DISPLAY_REFRESH_RATE = 1 << 31;
+
+ // internal buffer to backup/restore parameters under compatibility mode.
+ private int[] mCompatibilityParamsBackup = null;
+
+ public final int copyFrom(LayoutParams o) {
+ int changes = 0;
+
+ if (width != o.width) {
+ width = o.width;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (height != o.height) {
+ height = o.height;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (x != o.x) {
+ x = o.x;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (y != o.y) {
+ y = o.y;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (horizontalWeight != o.horizontalWeight) {
+ horizontalWeight = o.horizontalWeight;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (verticalWeight != o.verticalWeight) {
+ verticalWeight = o.verticalWeight;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (horizontalMargin != o.horizontalMargin) {
+ horizontalMargin = o.horizontalMargin;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (verticalMargin != o.verticalMargin) {
+ verticalMargin = o.verticalMargin;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (type != o.type) {
+ type = o.type;
+ changes |= TYPE_CHANGED;
+ }
+ if (flags != o.flags) {
+ final int diff = flags ^ o.flags;
+ if ((diff & (FLAG_TRANSLUCENT_STATUS | FLAG_TRANSLUCENT_NAVIGATION)) != 0) {
+ changes |= TRANSLUCENT_FLAGS_CHANGED;
+ }
+ flags = o.flags;
+ changes |= FLAGS_CHANGED;
+ }
+ if (privateFlags != o.privateFlags) {
+ privateFlags = o.privateFlags;
+ changes |= PRIVATE_FLAGS_CHANGED;
+ }
+ if (softInputMode != o.softInputMode) {
+ softInputMode = o.softInputMode;
+ changes |= SOFT_INPUT_MODE_CHANGED;
+ }
+ if (layoutInDisplayCutoutMode != o.layoutInDisplayCutoutMode) {
+ layoutInDisplayCutoutMode = o.layoutInDisplayCutoutMode;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (gravity != o.gravity) {
+ gravity = o.gravity;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (format != o.format) {
+ format = o.format;
+ changes |= FORMAT_CHANGED;
+ }
+ if (windowAnimations != o.windowAnimations) {
+ windowAnimations = o.windowAnimations;
+ changes |= ANIMATION_CHANGED;
+ }
+ if (token == null) {
+ // NOTE: token only copied if the recipient doesn't
+ // already have one.
+ token = o.token;
+ }
+ if (mWindowContextToken == null) {
+ // NOTE: token only copied if the recipient doesn't
+ // already have one.
+ mWindowContextToken = o.mWindowContextToken;
+ }
+ if (packageName == null) {
+ // NOTE: packageName only copied if the recipient doesn't
+ // already have one.
+ packageName = o.packageName;
+ }
+ if (!Objects.equals(mTitle, o.mTitle) && o.mTitle != null) {
+ // NOTE: mTitle only copied if the originator set one.
+ mTitle = o.mTitle;
+ changes |= TITLE_CHANGED;
+ }
+ if (alpha != o.alpha) {
+ alpha = o.alpha;
+ changes |= ALPHA_CHANGED;
+ }
+ if (dimAmount != o.dimAmount) {
+ dimAmount = o.dimAmount;
+ changes |= DIM_AMOUNT_CHANGED;
+ }
+ if (screenBrightness != o.screenBrightness) {
+ screenBrightness = o.screenBrightness;
+ changes |= SCREEN_BRIGHTNESS_CHANGED;
+ }
+ if (buttonBrightness != o.buttonBrightness) {
+ buttonBrightness = o.buttonBrightness;
+ changes |= BUTTON_BRIGHTNESS_CHANGED;
+ }
+ if (rotationAnimation != o.rotationAnimation) {
+ rotationAnimation = o.rotationAnimation;
+ changes |= ROTATION_ANIMATION_CHANGED;
+ }
+
+ if (screenOrientation != o.screenOrientation) {
+ screenOrientation = o.screenOrientation;
+ changes |= SCREEN_ORIENTATION_CHANGED;
+ }
+
+ if (preferredRefreshRate != o.preferredRefreshRate) {
+ preferredRefreshRate = o.preferredRefreshRate;
+ changes |= PREFERRED_REFRESH_RATE_CHANGED;
+ }
+
+ if (preferredDisplayModeId != o.preferredDisplayModeId) {
+ preferredDisplayModeId = o.preferredDisplayModeId;
+ changes |= PREFERRED_DISPLAY_MODE_ID;
+ }
+
+ if (preferredMinDisplayRefreshRate != o.preferredMinDisplayRefreshRate) {
+ preferredMinDisplayRefreshRate = o.preferredMinDisplayRefreshRate;
+ changes |= PREFERRED_MIN_DISPLAY_REFRESH_RATE;
+ }
+
+ if (preferredMaxDisplayRefreshRate != o.preferredMaxDisplayRefreshRate) {
+ preferredMaxDisplayRefreshRate = o.preferredMaxDisplayRefreshRate;
+ changes |= PREFERRED_MAX_DISPLAY_REFRESH_RATE;
+ }
+
+ if (systemUiVisibility != o.systemUiVisibility
+ || subtreeSystemUiVisibility != o.subtreeSystemUiVisibility) {
+ systemUiVisibility = o.systemUiVisibility;
+ subtreeSystemUiVisibility = o.subtreeSystemUiVisibility;
+ changes |= SYSTEM_UI_VISIBILITY_CHANGED;
+ }
+
+ if (hasSystemUiListeners != o.hasSystemUiListeners) {
+ hasSystemUiListeners = o.hasSystemUiListeners;
+ changes |= SYSTEM_UI_LISTENER_CHANGED;
+ }
+
+ if (inputFeatures != o.inputFeatures) {
+ inputFeatures = o.inputFeatures;
+ changes |= INPUT_FEATURES_CHANGED;
+ }
+
+ if (userActivityTimeout != o.userActivityTimeout) {
+ userActivityTimeout = o.userActivityTimeout;
+ changes |= USER_ACTIVITY_TIMEOUT_CHANGED;
+ }
+
+ if (!surfaceInsets.equals(o.surfaceInsets)) {
+ surfaceInsets.set(o.surfaceInsets);
+ changes |= SURFACE_INSETS_CHANGED;
+ }
+
+ if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) {
+ hasManualSurfaceInsets = o.hasManualSurfaceInsets;
+ changes |= SURFACE_INSETS_CHANGED;
+ }
+
+ if (receiveInsetsIgnoringZOrder != o.receiveInsetsIgnoringZOrder) {
+ receiveInsetsIgnoringZOrder = o.receiveInsetsIgnoringZOrder;
+ changes |= SURFACE_INSETS_CHANGED;
+ }
+
+ if (preservePreviousSurfaceInsets != o.preservePreviousSurfaceInsets) {
+ preservePreviousSurfaceInsets = o.preservePreviousSurfaceInsets;
+ changes |= SURFACE_INSETS_CHANGED;
+ }
+
+ if (accessibilityIdOfAnchor != o.accessibilityIdOfAnchor) {
+ accessibilityIdOfAnchor = o.accessibilityIdOfAnchor;
+ changes |= ACCESSIBILITY_ANCHOR_CHANGED;
+ }
+
+ if (!Objects.equals(accessibilityTitle, o.accessibilityTitle)
+ && o.accessibilityTitle != null) {
+ // NOTE: accessibilityTitle only copied if the originator set one.
+ accessibilityTitle = o.accessibilityTitle;
+ changes |= ACCESSIBILITY_TITLE_CHANGED;
+ }
+
+ if (mColorMode != o.mColorMode) {
+ mColorMode = o.mColorMode;
+ changes |= COLOR_MODE_CHANGED;
+ }
+
+ if (preferMinimalPostProcessing != o.preferMinimalPostProcessing) {
+ preferMinimalPostProcessing = o.preferMinimalPostProcessing;
+ changes |= MINIMAL_POST_PROCESSING_PREFERENCE_CHANGED;
+ }
+
+ if (mBlurBehindRadius != o.mBlurBehindRadius) {
+ mBlurBehindRadius = o.mBlurBehindRadius;
+ changes |= BLUR_BEHIND_RADIUS_CHANGED;
+ }
+
+ // This can't change, it's only set at window creation time.
+ hideTimeoutMilliseconds = o.hideTimeoutMilliseconds;
+
+ if (insetsFlags.appearance != o.insetsFlags.appearance) {
+ insetsFlags.appearance = o.insetsFlags.appearance;
+ changes |= INSET_FLAGS_CHANGED;
+ }
+
+ if (insetsFlags.behavior != o.insetsFlags.behavior) {
+ insetsFlags.behavior = o.insetsFlags.behavior;
+ changes |= INSET_FLAGS_CHANGED;
+ }
+
+ if (mFitInsetsTypes != o.mFitInsetsTypes) {
+ mFitInsetsTypes = o.mFitInsetsTypes;
+ changes |= LAYOUT_CHANGED;
+ }
+
+ if (mFitInsetsSides != o.mFitInsetsSides) {
+ mFitInsetsSides = o.mFitInsetsSides;
+ changes |= LAYOUT_CHANGED;
+ }
+
+ if (mFitInsetsIgnoringVisibility != o.mFitInsetsIgnoringVisibility) {
+ mFitInsetsIgnoringVisibility = o.mFitInsetsIgnoringVisibility;
+ changes |= LAYOUT_CHANGED;
+ }
+
+ if (!Arrays.equals(providesInsetsTypes, o.providesInsetsTypes)) {
+ providesInsetsTypes = o.providesInsetsTypes;
+ changes |= LAYOUT_CHANGED;
+ }
+
+ return changes;
+ }
+
+ @Override
+ public String debug(String output) {
+ output += "Contents of " + this + ":";
+ Log.d("Debug", output);
+ output = super.debug("");
+ Log.d("Debug", output);
+ Log.d("Debug", "");
+ Log.d("Debug", "WindowManager.LayoutParams={title=" + mTitle + "}");
+ return "";
+ }
+
+ @Override
+ public String toString() {
+ return toString("");
+ }
+
+ /**
+ * @hide
+ */
+ public void dumpDimensions(StringBuilder sb) {
+ sb.append('(');
+ sb.append(x);
+ sb.append(',');
+ sb.append(y);
+ sb.append(")(");
+ sb.append((width == MATCH_PARENT ? "fill" : (width == WRAP_CONTENT
+ ? "wrap" : String.valueOf(width))));
+ sb.append('x');
+ sb.append((height == MATCH_PARENT ? "fill" : (height == WRAP_CONTENT
+ ? "wrap" : String.valueOf(height))));
+ sb.append(")");
+ }
+
+ /**
+ * @hide
+ */
+ public String toString(String prefix) {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append('{');
+ dumpDimensions(sb);
+ if (horizontalMargin != 0) {
+ sb.append(" hm=");
+ sb.append(horizontalMargin);
+ }
+ if (verticalMargin != 0) {
+ sb.append(" vm=");
+ sb.append(verticalMargin);
+ }
+ if (gravity != 0) {
+ sb.append(" gr=");
+ sb.append(Gravity.toString(gravity));
+ }
+ if (softInputMode != 0) {
+ sb.append(" sim={");
+ sb.append(softInputModeToString(softInputMode));
+ sb.append('}');
+ }
+ if (layoutInDisplayCutoutMode != 0) {
+ sb.append(" layoutInDisplayCutoutMode=");
+ sb.append(layoutInDisplayCutoutModeToString(layoutInDisplayCutoutMode));
+ }
+ sb.append(" ty=");
+ sb.append(ViewDebug.intToString(LayoutParams.class, "type", type));
+ if (format != PixelFormat.OPAQUE) {
+ sb.append(" fmt=");
+ sb.append(PixelFormat.formatToString(format));
+ }
+ if (windowAnimations != 0) {
+ sb.append(" wanim=0x");
+ sb.append(Integer.toHexString(windowAnimations));
+ }
+ if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
+ sb.append(" or=");
+ sb.append(ActivityInfo.screenOrientationToString(screenOrientation));
+ }
+ if (alpha != 1.0f) {
+ sb.append(" alpha=");
+ sb.append(alpha);
+ }
+ if (screenBrightness != BRIGHTNESS_OVERRIDE_NONE) {
+ sb.append(" sbrt=");
+ sb.append(screenBrightness);
+ }
+ if (buttonBrightness != BRIGHTNESS_OVERRIDE_NONE) {
+ sb.append(" bbrt=");
+ sb.append(buttonBrightness);
+ }
+ if (rotationAnimation != ROTATION_ANIMATION_ROTATE) {
+ sb.append(" rotAnim=");
+ sb.append(rotationAnimationToString(rotationAnimation));
+ }
+ if (preferredRefreshRate != 0) {
+ sb.append(" preferredRefreshRate=");
+ sb.append(preferredRefreshRate);
+ }
+ if (preferredDisplayModeId != 0) {
+ sb.append(" preferredDisplayMode=");
+ sb.append(preferredDisplayModeId);
+ }
+ if (preferredMinDisplayRefreshRate != 0) {
+ sb.append(" preferredMinDisplayRefreshRate=");
+ sb.append(preferredMinDisplayRefreshRate);
+ }
+ if (preferredMaxDisplayRefreshRate != 0) {
+ sb.append(" preferredMaxDisplayRefreshRate=");
+ sb.append(preferredMaxDisplayRefreshRate);
+ }
+ if (hasSystemUiListeners) {
+ sb.append(" sysuil=");
+ sb.append(hasSystemUiListeners);
+ }
+ if (inputFeatures != 0) {
+ sb.append(" if=").append(inputFeatureToString(inputFeatures));
+ }
+ if (userActivityTimeout >= 0) {
+ sb.append(" userActivityTimeout=").append(userActivityTimeout);
+ }
+ if (surfaceInsets.left != 0 || surfaceInsets.top != 0 || surfaceInsets.right != 0 ||
+ surfaceInsets.bottom != 0 || hasManualSurfaceInsets
+ || !preservePreviousSurfaceInsets) {
+ sb.append(" surfaceInsets=").append(surfaceInsets);
+ if (hasManualSurfaceInsets) {
+ sb.append(" (manual)");
+ }
+ if (!preservePreviousSurfaceInsets) {
+ sb.append(" (!preservePreviousSurfaceInsets)");
+ }
+ }
+ if (receiveInsetsIgnoringZOrder) {
+ sb.append(" receive insets ignoring z-order");
+ }
+ if (mColorMode != COLOR_MODE_DEFAULT) {
+ sb.append(" colorMode=").append(ActivityInfo.colorModeToString(mColorMode));
+ }
+ if (preferMinimalPostProcessing) {
+ sb.append(" preferMinimalPostProcessing=");
+ sb.append(preferMinimalPostProcessing);
+ }
+ if (mBlurBehindRadius != 0) {
+ sb.append(" blurBehindRadius=");
+ sb.append(mBlurBehindRadius);
+ }
+ 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));
+ }
+ if (insetsFlags.appearance != 0) {
+ sb.append(System.lineSeparator());
+ sb.append(prefix).append(" apr=").append(ViewDebug.flagsToString(
+ InsetsFlags.class, "appearance", insetsFlags.appearance));
+ }
+ if (insetsFlags.behavior != 0) {
+ sb.append(System.lineSeparator());
+ sb.append(prefix).append(" bhv=").append(ViewDebug.flagsToString(
+ InsetsFlags.class, "behavior", insetsFlags.behavior));
+ }
+ if (mFitInsetsTypes != 0) {
+ sb.append(System.lineSeparator());
+ sb.append(prefix).append(" fitTypes=").append(ViewDebug.flagsToString(
+ LayoutParams.class, "mFitInsetsTypes", mFitInsetsTypes));
+ }
+ if (mFitInsetsSides != Side.all()) {
+ sb.append(System.lineSeparator());
+ sb.append(prefix).append(" fitSides=").append(ViewDebug.flagsToString(
+ LayoutParams.class, "mFitInsetsSides", mFitInsetsSides));
+ }
+ if (mFitInsetsIgnoringVisibility) {
+ sb.append(System.lineSeparator());
+ sb.append(prefix).append(" fitIgnoreVis");
+ }
+ if (providesInsetsTypes != null) {
+ sb.append(System.lineSeparator());
+ sb.append(prefix).append(" insetsTypes=");
+ for (int i = 0; i < providesInsetsTypes.length; ++i) {
+ if (i > 0) sb.append(' ');
+ sb.append(InsetsState.typeToString(providesInsetsTypes[i]));
+ }
+ }
+
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * @hide
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(TYPE, type);
+ proto.write(X, x);
+ proto.write(Y, y);
+ proto.write(WIDTH, width);
+ proto.write(HEIGHT, height);
+ proto.write(HORIZONTAL_MARGIN, horizontalMargin);
+ proto.write(VERTICAL_MARGIN, verticalMargin);
+ proto.write(GRAVITY, gravity);
+ proto.write(SOFT_INPUT_MODE, softInputMode);
+ proto.write(FORMAT, format);
+ proto.write(WINDOW_ANIMATIONS, windowAnimations);
+ proto.write(ALPHA, alpha);
+ proto.write(SCREEN_BRIGHTNESS, screenBrightness);
+ proto.write(BUTTON_BRIGHTNESS, buttonBrightness);
+ proto.write(ROTATION_ANIMATION, rotationAnimation);
+ proto.write(PREFERRED_REFRESH_RATE, preferredRefreshRate);
+ proto.write(WindowLayoutParamsProto.PREFERRED_DISPLAY_MODE_ID, preferredDisplayModeId);
+ proto.write(HAS_SYSTEM_UI_LISTENERS, hasSystemUiListeners);
+ proto.write(INPUT_FEATURE_FLAGS, inputFeatures);
+ proto.write(USER_ACTIVITY_TIMEOUT, userActivityTimeout);
+ proto.write(COLOR_MODE, mColorMode);
+ proto.write(FLAGS, flags);
+ proto.write(PRIVATE_FLAGS, privateFlags);
+ proto.write(SYSTEM_UI_VISIBILITY_FLAGS, systemUiVisibility);
+ proto.write(SUBTREE_SYSTEM_UI_VISIBILITY_FLAGS, subtreeSystemUiVisibility);
+ proto.write(APPEARANCE, insetsFlags.appearance);
+ proto.write(BEHAVIOR, insetsFlags.behavior);
+ proto.write(FIT_INSETS_TYPES, mFitInsetsTypes);
+ proto.write(FIT_INSETS_SIDES, mFitInsetsSides);
+ proto.write(FIT_IGNORE_VISIBILITY, mFitInsetsIgnoringVisibility);
+ proto.end(token);
+ }
+
+ /**
+ * Scale the layout params' coordinates and size.
+ * @hide
+ */
+ public void scale(float scale) {
+ x = (int) (x * scale + 0.5f);
+ y = (int) (y * scale + 0.5f);
+ if (width > 0) {
+ width = (int) (width * scale + 0.5f);
+ }
+ if (height > 0) {
+ height = (int) (height * scale + 0.5f);
+ }
+ }
+
+ /**
+ * Backup the layout parameters used in compatibility mode.
+ * @see LayoutParams#restore()
+ */
+ @UnsupportedAppUsage
+ void backup() {
+ int[] backup = mCompatibilityParamsBackup;
+ if (backup == null) {
+ // we backup 4 elements, x, y, width, height
+ backup = mCompatibilityParamsBackup = new int[4];
+ }
+ backup[0] = x;
+ backup[1] = y;
+ backup[2] = width;
+ backup[3] = height;
+ }
+
+ /**
+ * Restore the layout params' coordinates, size and gravity
+ * @see LayoutParams#backup()
+ */
+ @UnsupportedAppUsage
+ void restore() {
+ int[] backup = mCompatibilityParamsBackup;
+ if (backup != null) {
+ x = backup[0];
+ y = backup[1];
+ width = backup[2];
+ height = backup[3];
+ }
+ }
+
+ private CharSequence mTitle = null;
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("x", x);
+ encoder.addProperty("y", y);
+ encoder.addProperty("horizontalWeight", horizontalWeight);
+ encoder.addProperty("verticalWeight", verticalWeight);
+ encoder.addProperty("type", type);
+ encoder.addProperty("flags", flags);
+ }
+
+ /**
+ * @hide
+ * @return True if the layout parameters will cause the window to cover the full screen;
+ * false otherwise.
+ */
+ public boolean isFullscreen() {
+ return x == 0 && y == 0
+ && width == WindowManager.LayoutParams.MATCH_PARENT
+ && height == WindowManager.LayoutParams.MATCH_PARENT;
+ }
+
+ private static String layoutInDisplayCutoutModeToString(
+ @LayoutInDisplayCutoutMode int mode) {
+ switch (mode) {
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:
+ return "default";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS:
+ return "always";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:
+ return "never";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:
+ return "shortEdges";
+ default:
+ return "unknown(" + mode + ")";
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Holds the WM lock for the specified amount of milliseconds.
+ * Intended for use by the tests that need to imitate lock contention.
+ * The token should be obtained by
+ * {@link android.content.pm.PackageManager#getHoldLockToken()}.
+ * @hide
+ */
+ @TestApi
+ default void holdLock(IBinder token, int durationMs) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Used for testing to check if the system supports TaskSnapshot mechanism.
+ * @hide
+ */
+ @TestApi
+ default boolean isTaskSnapshotSupported() {
+ return false;
+ }
+}
diff --git a/android/view/WindowManagerGlobal.java b/android/view/WindowManagerGlobal.java
new file mode 100644
index 0000000..18013e8
--- /dev/null
+++ b/android/view/WindowManagerGlobal.java
@@ -0,0 +1,719 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.util.AndroidRuntimeException;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Provides low-level communication with the system window manager for
+ * operations that are not associated with any particular context.
+ *
+ * This class is only used internally to implement global functions where
+ * the caller already knows the display and relevant compatibility information
+ * for the operation. For most purposes, you should use {@link WindowManager} instead
+ * since it is bound to a context.
+ *
+ * @see WindowManagerImpl
+ * @hide
+ */
+public final class WindowManagerGlobal {
+ private static final String TAG = "WindowManager";
+
+ private static boolean sUseBLASTAdapter = false;
+
+ /**
+ * The user is navigating with keys (not the touch screen), so
+ * navigational focus should be shown.
+ */
+ public static final int RELAYOUT_RES_IN_TOUCH_MODE = 0x1;
+
+ /**
+ * This is the first time the window is being drawn,
+ * so the client must call drawingFinished() when done
+ */
+ public static final int RELAYOUT_RES_FIRST_TIME = 0x2;
+
+ /**
+ * The window manager has changed the surface from the last call.
+ */
+ public static final int RELAYOUT_RES_SURFACE_CHANGED = 0x4;
+
+ /**
+ * The window is being resized by dragging on the docked divider. The client should render
+ * at (0, 0) and extend its background to the background frame passed into
+ * {@link IWindow#resized}.
+ */
+ public static final int RELAYOUT_RES_DRAG_RESIZING_DOCKED = 0x8;
+
+ /**
+ * The window is being resized by dragging one of the window corners,
+ * in this case the surface would be fullscreen-sized. The client should
+ * render to the actual frame location (instead of (0,curScrollY)).
+ */
+ public static final int RELAYOUT_RES_DRAG_RESIZING_FREEFORM = 0x10;
+
+ /**
+ * The window manager has changed the size of the surface from the last call.
+ */
+ public static final int RELAYOUT_RES_SURFACE_RESIZED = 0x20;
+
+ /**
+ * In multi-window we force show the system bars. Because we don't want that the surface size
+ * changes in this mode, we instead have a flag whether the system bar sizes should always be
+ * consumed, so the app is treated like there is no virtual system bars at all.
+ */
+ public static final int RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS = 0x40;
+
+ /**
+ * This flag indicates the client should not directly submit it's next frame,
+ * but instead should pass it in the postDrawTransaction of
+ * {@link WindowManagerService#finishDrawing}. This is used by the WM
+ * BLASTSyncEngine to synchronize rendering of multiple windows.
+ */
+ public static final int RELAYOUT_RES_BLAST_SYNC = 0x80;
+
+ /**
+ * Flag for relayout: the client will be later giving
+ * internal insets; as a result, the window will not impact other window
+ * layouts until the insets are given.
+ */
+ public static final int RELAYOUT_INSETS_PENDING = 0x1;
+
+ public static final int ADD_FLAG_IN_TOUCH_MODE = 0x1;
+ public static final int ADD_FLAG_APP_VISIBLE = 0x2;
+ public static final int ADD_FLAG_USE_BLAST = 0x8;
+
+ /**
+ * Like {@link #RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS}, but as a "hint" when adding the
+ * window.
+ */
+ public static final int ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS = 0x4;
+
+ public static final int ADD_OKAY = 0;
+ public static final int ADD_BAD_APP_TOKEN = -1;
+ public static final int ADD_BAD_SUBWINDOW_TOKEN = -2;
+ public static final int ADD_NOT_APP_TOKEN = -3;
+ public static final int ADD_APP_EXITING = -4;
+ public static final int ADD_DUPLICATE_ADD = -5;
+ public static final int ADD_STARTING_NOT_NEEDED = -6;
+ public static final int ADD_MULTIPLE_SINGLETON = -7;
+ public static final int ADD_PERMISSION_DENIED = -8;
+ public static final int ADD_INVALID_DISPLAY = -9;
+ public static final int ADD_INVALID_TYPE = -10;
+ public static final int ADD_INVALID_USER = -11;
+
+ @UnsupportedAppUsage
+ private static WindowManagerGlobal sDefaultWindowManager;
+ @UnsupportedAppUsage
+ private static IWindowManager sWindowManagerService;
+ @UnsupportedAppUsage
+ private static IWindowSession sWindowSession;
+
+ @UnsupportedAppUsage
+ private final Object mLock = new Object();
+
+ @UnsupportedAppUsage
+ private final ArrayList<View> mViews = new ArrayList<View>();
+ @UnsupportedAppUsage
+ private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
+ @UnsupportedAppUsage
+ private final ArrayList<WindowManager.LayoutParams> mParams =
+ new ArrayList<WindowManager.LayoutParams>();
+ private final ArraySet<View> mDyingViews = new ArraySet<View>();
+
+ private Runnable mSystemPropertyUpdater;
+
+ private WindowManagerGlobal() {
+ }
+
+ @UnsupportedAppUsage
+ public static void initialize() {
+ getWindowManagerService();
+ }
+
+ @UnsupportedAppUsage
+ public static WindowManagerGlobal getInstance() {
+ synchronized (WindowManagerGlobal.class) {
+ if (sDefaultWindowManager == null) {
+ sDefaultWindowManager = new WindowManagerGlobal();
+ }
+ return sDefaultWindowManager;
+ }
+ }
+
+ @UnsupportedAppUsage
+ public static IWindowManager getWindowManagerService() {
+ synchronized (WindowManagerGlobal.class) {
+ if (sWindowManagerService == null) {
+ sWindowManagerService = IWindowManager.Stub.asInterface(
+ ServiceManager.getService("window"));
+ try {
+ if (sWindowManagerService != null) {
+ ValueAnimator.setDurationScale(
+ sWindowManagerService.getCurrentAnimatorScale());
+ sUseBLASTAdapter = sWindowManagerService.useBLAST();
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return sWindowManagerService;
+ }
+ }
+
+ @UnsupportedAppUsage
+ public static IWindowSession getWindowSession() {
+ synchronized (WindowManagerGlobal.class) {
+ if (sWindowSession == null) {
+ try {
+ // Emulate the legacy behavior. The global instance of InputMethodManager
+ // was instantiated here.
+ // TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
+ InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
+ IWindowManager windowManager = getWindowManagerService();
+ sWindowSession = windowManager.openSession(
+ new IWindowSessionCallback.Stub() {
+ @Override
+ public void onAnimatorScaleChanged(float scale) {
+ ValueAnimator.setDurationScale(scale);
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return sWindowSession;
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static IWindowSession peekWindowSession() {
+ synchronized (WindowManagerGlobal.class) {
+ return sWindowSession;
+ }
+ }
+
+ /**
+ * Whether or not to use BLAST for ViewRootImpl
+ */
+ public static boolean useBLAST() {
+ return sUseBLASTAdapter;
+ }
+
+ @UnsupportedAppUsage
+ public String[] getViewRootNames() {
+ synchronized (mLock) {
+ final int numRoots = mRoots.size();
+ String[] mViewRoots = new String[numRoots];
+ for (int i = 0; i < numRoots; ++i) {
+ mViewRoots[i] = getWindowName(mRoots.get(i));
+ }
+ return mViewRoots;
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public ArrayList<ViewRootImpl> getRootViews(IBinder token) {
+ ArrayList<ViewRootImpl> views = new ArrayList<>();
+ synchronized (mLock) {
+ final int numRoots = mRoots.size();
+ for (int i = 0; i < numRoots; ++i) {
+ WindowManager.LayoutParams params = mParams.get(i);
+ if (params.token == null) {
+ continue;
+ }
+ if (params.token != token) {
+ boolean isChild = false;
+ if (params.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW
+ && params.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
+ for (int j = 0 ; j < numRoots; ++j) {
+ View viewj = mViews.get(j);
+ WindowManager.LayoutParams paramsj = mParams.get(j);
+ if (params.token == viewj.getWindowToken()
+ && paramsj.token == token) {
+ isChild = true;
+ break;
+ }
+ }
+ }
+ if (!isChild) {
+ continue;
+ }
+ }
+ views.add(mRoots.get(i));
+ }
+ }
+ return views;
+ }
+
+ /**
+ * @return the list of all views attached to the global window manager
+ */
+ @NonNull
+ public ArrayList<View> getWindowViews() {
+ synchronized (mLock) {
+ return new ArrayList<>(mViews);
+ }
+ }
+
+ public View getWindowView(IBinder windowToken) {
+ synchronized (mLock) {
+ final int numViews = mViews.size();
+ for (int i = 0; i < numViews; ++i) {
+ final View view = mViews.get(i);
+ if (view.getWindowToken() == windowToken) {
+ return view;
+ }
+ }
+ }
+ return null;
+ }
+
+ @UnsupportedAppUsage
+ public View getRootView(String name) {
+ synchronized (mLock) {
+ for (int i = mRoots.size() - 1; i >= 0; --i) {
+ final ViewRootImpl root = mRoots.get(i);
+ if (name.equals(getWindowName(root))) return root.getView();
+ }
+ }
+
+ return null;
+ }
+
+ public void addView(View view, ViewGroup.LayoutParams params,
+ Display display, Window parentWindow, int userId) {
+ if (view == null) {
+ throw new IllegalArgumentException("view must not be null");
+ }
+ if (display == null) {
+ throw new IllegalArgumentException("display must not be null");
+ }
+ if (!(params instanceof WindowManager.LayoutParams)) {
+ throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
+ }
+
+ final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
+ if (parentWindow != null) {
+ parentWindow.adjustLayoutParamsForSubWindow(wparams);
+ } else {
+ // If there's no parent, then hardware acceleration for this view is
+ // set from the application's hardware acceleration setting.
+ final Context context = view.getContext();
+ if (context != null
+ && (context.getApplicationInfo().flags
+ & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
+ wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+ }
+ }
+
+ ViewRootImpl root;
+ View panelParentView = null;
+
+ synchronized (mLock) {
+ // Start watching for system property changes.
+ if (mSystemPropertyUpdater == null) {
+ mSystemPropertyUpdater = new Runnable() {
+ @Override public void run() {
+ synchronized (mLock) {
+ for (int i = mRoots.size() - 1; i >= 0; --i) {
+ mRoots.get(i).loadSystemProperties();
+ }
+ }
+ }
+ };
+ SystemProperties.addChangeCallback(mSystemPropertyUpdater);
+ }
+
+ int index = findViewLocked(view, false);
+ if (index >= 0) {
+ if (mDyingViews.contains(view)) {
+ // Don't wait for MSG_DIE to make it's way through root's queue.
+ mRoots.get(index).doDie();
+ } else {
+ throw new IllegalStateException("View " + view
+ + " has already been added to the window manager.");
+ }
+ // The previous removeView() had not completed executing. Now it has.
+ }
+
+ // If this is a panel window, then find the window it is being
+ // attached to for future reference.
+ if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
+ wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
+ final int count = mViews.size();
+ for (int i = 0; i < count; i++) {
+ if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
+ panelParentView = mViews.get(i);
+ }
+ }
+ }
+
+ root = new ViewRootImpl(view.getContext(), display);
+
+ view.setLayoutParams(wparams);
+
+ mViews.add(view);
+ mRoots.add(root);
+ mParams.add(wparams);
+
+ // do this last because it fires off messages to start doing things
+ try {
+ root.setView(view, wparams, panelParentView, userId);
+ } catch (RuntimeException e) {
+ // BadTokenException or InvalidDisplayException, clean up.
+ if (index >= 0) {
+ removeViewLocked(index, true);
+ }
+ throw e;
+ }
+ }
+ }
+
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
+ if (view == null) {
+ throw new IllegalArgumentException("view must not be null");
+ }
+ if (!(params instanceof WindowManager.LayoutParams)) {
+ throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
+ }
+
+ final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
+
+ view.setLayoutParams(wparams);
+
+ synchronized (mLock) {
+ int index = findViewLocked(view, true);
+ ViewRootImpl root = mRoots.get(index);
+ mParams.remove(index);
+ mParams.add(index, wparams);
+ root.setLayoutParams(wparams, false);
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void removeView(View view, boolean immediate) {
+ if (view == null) {
+ throw new IllegalArgumentException("view must not be null");
+ }
+
+ synchronized (mLock) {
+ int index = findViewLocked(view, true);
+ View curView = mRoots.get(index).getView();
+ removeViewLocked(index, immediate);
+ if (curView == view) {
+ return;
+ }
+
+ throw new IllegalStateException("Calling with view " + view
+ + " but the ViewAncestor is attached to " + curView);
+ }
+ }
+
+ /**
+ * Remove all roots with specified token.
+ *
+ * @param token app or window token.
+ * @param who name of caller, used in logs.
+ * @param what type of caller, used in logs.
+ */
+ public void closeAll(IBinder token, String who, String what) {
+ closeAllExceptView(token, null /* view */, who, what);
+ }
+
+ /**
+ * Remove all roots with specified token, except maybe one view.
+ *
+ * @param token app or window token.
+ * @param view view that should be should be preserved along with it's root.
+ * Pass null if everything should be removed.
+ * @param who name of caller, used in logs.
+ * @param what type of caller, used in logs.
+ */
+ public void closeAllExceptView(IBinder token, View view, String who, String what) {
+ synchronized (mLock) {
+ int count = mViews.size();
+ for (int i = 0; i < count; i++) {
+ if ((view == null || mViews.get(i) != view)
+ && (token == null || mParams.get(i).token == token)) {
+ ViewRootImpl root = mRoots.get(i);
+
+ if (who != null) {
+ WindowLeaked leak = new WindowLeaked(
+ what + " " + who + " has leaked window "
+ + root.getView() + " that was originally added here");
+ leak.setStackTrace(root.getLocation().getStackTrace());
+ Log.e(TAG, "", leak);
+ }
+
+ removeViewLocked(i, false);
+ }
+ }
+ }
+ }
+
+ private void removeViewLocked(int index, boolean immediate) {
+ ViewRootImpl root = mRoots.get(index);
+ View view = root.getView();
+
+ if (root != null) {
+ root.getImeFocusController().onWindowDismissed();
+ }
+ boolean deferred = root.die(immediate);
+ if (view != null) {
+ view.assignParent(null);
+ if (deferred) {
+ mDyingViews.add(view);
+ }
+ }
+ }
+
+ void doRemoveView(ViewRootImpl root) {
+ boolean allViewsRemoved;
+ synchronized (mLock) {
+ final int index = mRoots.indexOf(root);
+ if (index >= 0) {
+ mRoots.remove(index);
+ mParams.remove(index);
+ final View view = mViews.remove(index);
+ mDyingViews.remove(view);
+ }
+ allViewsRemoved = mRoots.isEmpty();
+ }
+ if (ThreadedRenderer.sTrimForeground) {
+ doTrimForeground();
+ }
+
+ // If we don't have any views anymore in our process, we no longer need the
+ // InsetsAnimationThread to save some resources.
+ if (allViewsRemoved) {
+ InsetsAnimationThread.release();
+ }
+ }
+
+ private int findViewLocked(View view, boolean required) {
+ final int index = mViews.indexOf(view);
+ if (required && index < 0) {
+ throw new IllegalArgumentException("View=" + view + " not attached to window manager");
+ }
+ return index;
+ }
+
+ public static boolean shouldDestroyEglContext(int trimLevel) {
+ // On low-end gfx devices we trim when memory is moderate;
+ // on high-end devices we do this when low.
+ if (trimLevel >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
+ return true;
+ }
+ if (trimLevel >= ComponentCallbacks2.TRIM_MEMORY_MODERATE
+ && !ActivityManager.isHighEndGfx()) {
+ return true;
+ }
+ return false;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ public void trimMemory(int level) {
+
+ if (shouldDestroyEglContext(level)) {
+ // Destroy all hardware surfaces and resources associated to
+ // known windows
+ synchronized (mLock) {
+ for (int i = mRoots.size() - 1; i >= 0; --i) {
+ mRoots.get(i).destroyHardwareResources();
+ }
+ }
+ // Force a full memory flush
+ level = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
+ }
+
+ ThreadedRenderer.trimMemory(level);
+
+ if (ThreadedRenderer.sTrimForeground) {
+ doTrimForeground();
+ }
+ }
+
+ public static void trimForeground() {
+ if (ThreadedRenderer.sTrimForeground) {
+ WindowManagerGlobal wm = WindowManagerGlobal.getInstance();
+ wm.doTrimForeground();
+ }
+ }
+
+ private void doTrimForeground() {
+ boolean hasVisibleWindows = false;
+ synchronized (mLock) {
+ for (int i = mRoots.size() - 1; i >= 0; --i) {
+ final ViewRootImpl root = mRoots.get(i);
+ if (root.mView != null && root.getHostVisibility() == View.VISIBLE
+ && root.mAttachInfo.mThreadedRenderer != null) {
+ hasVisibleWindows = true;
+ } else {
+ root.destroyHardwareResources();
+ }
+ }
+ }
+ if (!hasVisibleWindows) {
+ ThreadedRenderer.trimMemory(
+ ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
+ }
+ }
+
+ public void dumpGfxInfo(FileDescriptor fd, String[] args) {
+ FileOutputStream fout = new FileOutputStream(fd);
+ PrintWriter pw = new FastPrintWriter(fout);
+ try {
+ synchronized (mLock) {
+ final int count = mViews.size();
+
+ pw.println("Profile data in ms:");
+
+ for (int i = 0; i < count; i++) {
+ ViewRootImpl root = mRoots.get(i);
+ String name = getWindowName(root);
+ pw.printf("\n\t%s (visibility=%d)", name, root.getHostVisibility());
+
+ ThreadedRenderer renderer =
+ root.getView().mAttachInfo.mThreadedRenderer;
+ if (renderer != null) {
+ renderer.dumpGfxInfo(pw, fd, args);
+ }
+ }
+
+ pw.println("\nView hierarchy:\n");
+
+ ViewRootImpl.GfxInfo totals = new ViewRootImpl.GfxInfo();
+
+ for (int i = 0; i < count; i++) {
+ ViewRootImpl root = mRoots.get(i);
+ ViewRootImpl.GfxInfo info = root.getGfxInfo();
+ totals.add(info);
+
+ String name = getWindowName(root);
+ pw.printf(" %s\n %d views, %.2f kB of render nodes",
+ name, info.viewCount, info.renderNodeMemoryUsage / 1024.f);
+ pw.printf("\n\n");
+ }
+
+ pw.printf("\nTotal %-15s: %d\n", "ViewRootImpl", count);
+ pw.printf("Total %-15s: %d\n", "attached Views", totals.viewCount);
+ pw.printf("Total %-15s: %.2f kB (used) / %.2f kB (capacity)\n\n", "RenderNode",
+ totals.renderNodeMemoryUsage / 1024.0f,
+ totals.renderNodeMemoryAllocated / 1024.0f);
+ }
+ } finally {
+ pw.flush();
+ }
+ }
+
+ private static String getWindowName(ViewRootImpl root) {
+ return root.mWindowAttributes.getTitle() + "/" +
+ root.getClass().getName() + '@' + Integer.toHexString(root.hashCode());
+ }
+
+ public void setStoppedState(IBinder token, boolean stopped) {
+ ArrayList<ViewRootImpl> nonCurrentThreadRoots = null;
+ synchronized (mLock) {
+ int count = mViews.size();
+ for (int i = count - 1; i >= 0; i--) {
+ if (token == null || mParams.get(i).token == token) {
+ ViewRootImpl root = mRoots.get(i);
+ // Client might remove the view by "stopped" event.
+ if (root.mThread == Thread.currentThread()) {
+ root.setWindowStopped(stopped);
+ } else {
+ if (nonCurrentThreadRoots == null) {
+ nonCurrentThreadRoots = new ArrayList<>();
+ }
+ nonCurrentThreadRoots.add(root);
+ }
+ // Recursively forward stopped state to View's attached
+ // to this Window rather than the root application token,
+ // e.g. PopupWindow's.
+ setStoppedState(root.mAttachInfo.mWindowToken, stopped);
+ }
+ }
+ }
+
+ // Update the stopped state synchronously to ensure the surface won't be used after server
+ // side has destroyed it. This operation should be outside the lock to avoid any potential
+ // paths from setWindowStopped to WindowManagerGlobal which may cause deadlocks.
+ if (nonCurrentThreadRoots != null) {
+ for (int i = nonCurrentThreadRoots.size() - 1; i >= 0; i--) {
+ ViewRootImpl root = nonCurrentThreadRoots.get(i);
+ root.mHandler.runWithScissors(() -> root.setWindowStopped(stopped), 0);
+ }
+ }
+ }
+
+ public void reportNewConfiguration(Configuration config) {
+ synchronized (mLock) {
+ int count = mViews.size();
+ config = new Configuration(config);
+ for (int i=0; i < count; i++) {
+ ViewRootImpl root = mRoots.get(i);
+ root.requestUpdateConfiguration(config);
+ }
+ }
+ }
+
+ /** @hide */
+ public void changeCanvasOpacity(IBinder token, boolean opaque) {
+ if (token == null) {
+ return;
+ }
+ synchronized (mLock) {
+ for (int i = mParams.size() - 1; i >= 0; --i) {
+ if (mParams.get(i).token == token) {
+ mRoots.get(i).changeCanvasOpacity(opaque);
+ return;
+ }
+ }
+ }
+ }
+}
+
+final class WindowLeaked extends AndroidRuntimeException {
+ @UnsupportedAppUsage
+ public WindowLeaked(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/view/WindowManagerGlobal_Delegate.java b/android/view/WindowManagerGlobal_Delegate.java
new file mode 100644
index 0000000..2606e55
--- /dev/null
+++ b/android/view/WindowManagerGlobal_Delegate.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+/**
+ * Delegate used to provide new implementation of a select few methods of
+ * {@link WindowManagerGlobal}
+ *
+ * Through the layoutlib_create tool, the original methods of WindowManagerGlobal have been
+ * replaced by calls to methods of the same name in this delegate class.
+ *
+ */
+public class WindowManagerGlobal_Delegate {
+
+ private static IWindowManager sService;
+
+ @LayoutlibDelegate
+ public static IWindowManager getWindowManagerService() {
+ return sService;
+ }
+
+ // ---- internal implementation stuff ----
+
+ public static void setWindowManagerService(IWindowManager service) {
+ sService = service;
+ }
+}
diff --git a/android/view/WindowManagerImpl.java b/android/view/WindowManagerImpl.java
new file mode 100644
index 0000000..36b7165
--- /dev/null
+++ b/android/view/WindowManagerImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package android.view;
+
+import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import android.app.ResourcesManager;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.view.Display.Mode;
+
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+
+public class WindowManagerImpl implements WindowManager {
+
+ private final Context mContext;
+ private final DisplayMetrics mMetrics;
+ private final Display mDisplay;
+
+ public WindowManagerImpl(Context context, DisplayMetrics metrics) {
+ mContext = context;
+ mMetrics = metrics;
+
+ DisplayInfo info = new DisplayInfo();
+ info.logicalHeight = mMetrics.heightPixels;
+ info.logicalWidth = mMetrics.widthPixels;
+ info.supportedModes = new Mode[] {
+ new Mode(0, mMetrics.widthPixels, mMetrics.heightPixels, 60f)
+ };
+ info.logicalDensityDpi = mMetrics.densityDpi;
+ mDisplay = new Display(null, Display.DEFAULT_DISPLAY, info,
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS);
+ }
+
+ public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
+ Bridge.getLog().fidelityWarning(ILayoutLog.TAG_UNSUPPORTED,
+ "The preview does not support multiple windows.",
+ null, null, null);
+ return this;
+ }
+
+ public WindowManagerImpl createPresentationWindowManager(Context displayContext) {
+ Bridge.getLog().fidelityWarning(ILayoutLog.TAG_UNSUPPORTED,
+ "The preview does not support multiple windows.",
+ null, null, null);
+ return this;
+ }
+
+ /**
+ * Sets the window token to assign when none is specified by the client or
+ * available from the parent window.
+ *
+ * @param token The default token to assign.
+ */
+ public void setDefaultToken(IBinder token) {
+
+ }
+
+ @Override
+ public Display getDefaultDisplay() {
+ return mDisplay;
+ }
+
+
+ @Override
+ public void addView(View arg0, android.view.ViewGroup.LayoutParams arg1) {
+ // pass
+ }
+
+ @Override
+ public void removeView(View arg0) {
+ // pass
+ }
+
+ @Override
+ public void updateViewLayout(View arg0, android.view.ViewGroup.LayoutParams arg1) {
+ // pass
+ }
+
+
+ @Override
+ public void removeViewImmediate(View arg0) {
+ // pass
+ }
+
+ @Override
+ public void requestAppKeyboardShortcuts(
+ KeyboardShortcutsReceiver receiver, int deviceId) {
+ }
+
+ @Override
+ public Region getCurrentImeTouchRegion() {
+ return null;
+ }
+
+ @Override
+ public void setShouldShowWithInsecureKeyguard(int displayId, boolean shouldShow) {
+ // pass
+ }
+
+ @Override
+ public void setShouldShowSystemDecors(int displayId, boolean shouldShow) {
+ // pass
+ }
+
+ @Override
+ public void setDisplayImePolicy(int displayId, int imePolicy) {
+ // pass
+ }
+
+ @Override
+ public WindowMetrics getCurrentWindowMetrics() {
+ final Rect bound = getCurrentBounds(mContext);
+
+ return new WindowMetrics(bound, computeWindowInsets());
+ }
+
+ private static Rect getCurrentBounds(Context context) {
+ synchronized (ResourcesManager.getInstance()) {
+ return context.getResources().getConfiguration().windowConfiguration.getBounds();
+ }
+ }
+
+ @Override
+ public WindowMetrics getMaximumWindowMetrics() {
+ return new WindowMetrics(getMaximumBounds(), computeWindowInsets());
+ }
+
+ private Rect getMaximumBounds() {
+ final Point displaySize = new Point();
+ mDisplay.getRealSize(displaySize);
+ return new Rect(0, 0, displaySize.x, displaySize.y);
+ }
+
+ private WindowInsets computeWindowInsets() {
+ try {
+ final InsetsState insetsState = new InsetsState();
+ final boolean alwaysConsumeSystemBars =
+ WindowManagerGlobal.getWindowManagerService().getWindowInsets(
+ new WindowManager.LayoutParams(), mContext.getDisplayId(), insetsState);
+ final Configuration config = mContext.getResources().getConfiguration();
+ final boolean isScreenRound = config.isScreenRound();
+ final int windowingMode = config.windowConfiguration.getWindowingMode();
+ return insetsState.calculateInsets(getCurrentBounds(mContext),
+ null /* ignoringVisibilityState*/, isScreenRound, alwaysConsumeSystemBars,
+ SOFT_INPUT_ADJUST_NOTHING, 0, SYSTEM_UI_FLAG_VISIBLE, TYPE_APPLICATION,
+ windowingMode, null /* typeSideMap */);
+ } catch (RemoteException ignore) {
+ }
+ return null;
+ }
+}
diff --git a/android/view/WindowManagerPolicyConstants.java b/android/view/WindowManagerPolicyConstants.java
new file mode 100644
index 0000000..bbef3e6
--- /dev/null
+++ b/android/view/WindowManagerPolicyConstants.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.IInputConstants.POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+
+import android.annotation.IntDef;
+import android.os.PowerManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Constants for interfacing with WindowManagerService and WindowManagerPolicyInternal.
+ * @hide
+ */
+public interface WindowManagerPolicyConstants {
+ // Policy flags. These flags are also defined in frameworks/base/include/ui/Input.h and
+ // frameworks/native/libs/input/android/os/IInputConstants.aidl
+ int FLAG_WAKE = 0x00000001;
+ int FLAG_VIRTUAL = 0x00000002;
+
+ int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+ int FLAG_INJECTED = 0x01000000;
+ int FLAG_TRUSTED = 0x02000000;
+ int FLAG_FILTERED = 0x04000000;
+ int FLAG_DISABLE_KEY_REPEAT = 0x08000000;
+
+ int FLAG_INTERACTIVE = 0x20000000;
+ int FLAG_PASS_TO_USER = 0x40000000;
+
+ // Flags for IActivityTaskManager.keyguardGoingAway()
+ int KEYGUARD_GOING_AWAY_FLAG_TO_SHADE = 1 << 0;
+ int KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS = 1 << 1;
+ int KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER = 1 << 2;
+ int KEYGUARD_GOING_AWAY_FLAG_SUBTLE_WINDOW_ANIMATIONS = 1 << 3;
+
+ // Flags used for indicating whether the internal and/or external input devices
+ // of some type are available.
+ int PRESENCE_INTERNAL = 1 << 0;
+ int PRESENCE_EXTERNAL = 1 << 1;
+
+ // Alternate bars position values
+ int ALT_BAR_UNKNOWN = -1;
+ int ALT_BAR_LEFT = 1 << 0;
+ int ALT_BAR_RIGHT = 1 << 1;
+ int ALT_BAR_BOTTOM = 1 << 2;
+ int ALT_BAR_TOP = 1 << 3;
+
+ // Navigation bar position values
+ int NAV_BAR_INVALID = -1;
+ int NAV_BAR_LEFT = 1 << 0;
+ int NAV_BAR_RIGHT = 1 << 1;
+ int NAV_BAR_BOTTOM = 1 << 2;
+
+ // Navigation bar interaction modes
+ int NAV_BAR_MODE_3BUTTON = 0;
+ int NAV_BAR_MODE_2BUTTON = 1;
+ int NAV_BAR_MODE_GESTURAL = 2;
+
+ // Associated overlays for each nav bar mode
+ String NAV_BAR_MODE_3BUTTON_OVERLAY = "com.android.internal.systemui.navbar.threebutton";
+ String NAV_BAR_MODE_2BUTTON_OVERLAY = "com.android.internal.systemui.navbar.twobutton";
+ String NAV_BAR_MODE_GESTURAL_OVERLAY = "com.android.internal.systemui.navbar.gestural";
+
+ /**
+ * Sticky broadcast of the current HDMI plugged state.
+ */
+ String ACTION_HDMI_PLUGGED = "android.intent.action.HDMI_PLUGGED";
+
+ /**
+ * Extra in {@link #ACTION_HDMI_PLUGGED} indicating the state: true if
+ * plugged in to HDMI, false if not.
+ */
+ String EXTRA_HDMI_PLUGGED_STATE = "state";
+
+ /**
+ * Set to {@code true} when intent was invoked from pressing the home key.
+ * @hide
+ */
+ String EXTRA_FROM_HOME_KEY = "android.intent.extra.FROM_HOME_KEY";
+
+ /**
+ * Extra for the start reason of the HOME intent.
+ * Will be {@link PowerManager#WAKE_REASON_WAKE_KEY} or
+ * {@link PowerManager#WAKE_REASON_POWER_BUTTON} when intent was sent through
+ * {@link PhoneWindowManager#shouldWakeUpWithHomeIntent}.
+ * @hide
+ */
+ String EXTRA_START_REASON = "android.intent.extra.EXTRA_START_REASON";
+
+ // TODO: move this to a more appropriate place.
+ interface PointerEventListener {
+ /**
+ * 1. onPointerEvent will be called on the service.UiThread.
+ * 2. motionEvent will be recycled after onPointerEvent returns so if it is needed later a
+ * copy() must be made and the copy must be recycled.
+ **/
+ void onPointerEvent(MotionEvent motionEvent);
+ }
+
+ @IntDef(prefix = { "OFF_BECAUSE_OF_" }, value = {
+ OFF_BECAUSE_OF_ADMIN,
+ OFF_BECAUSE_OF_USER,
+ OFF_BECAUSE_OF_TIMEOUT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OffReason{}
+
+ static @OffReason int translateSleepReasonToOffReason(
+ @PowerManager.GoToSleepReason int reason) {
+ switch (reason) {
+ case PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN:
+ return OFF_BECAUSE_OF_ADMIN;
+ case PowerManager.GO_TO_SLEEP_REASON_TIMEOUT:
+ case PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE:
+ return OFF_BECAUSE_OF_TIMEOUT;
+ default:
+ return OFF_BECAUSE_OF_USER;
+ }
+ }
+
+ /** Screen turned off because of a device admin */
+ int OFF_BECAUSE_OF_ADMIN = 1;
+ /** Screen turned off because of power button */
+ int OFF_BECAUSE_OF_USER = 2;
+ /** Screen turned off because of timeout */
+ int OFF_BECAUSE_OF_TIMEOUT = 3;
+
+ @IntDef(prefix = { "ON_BECAUSE_OF_" }, value = {
+ ON_BECAUSE_OF_USER,
+ ON_BECAUSE_OF_APPLICATION,
+ ON_BECAUSE_OF_UNKNOWN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OnReason{}
+
+ /** Convert the on reason to a human readable format */
+ static String onReasonToString(@OnReason int why) {
+ switch (why) {
+ case ON_BECAUSE_OF_USER:
+ return "ON_BECAUSE_OF_USER";
+ case ON_BECAUSE_OF_APPLICATION:
+ return "ON_BECAUSE_OF_APPLICATION";
+ case ON_BECAUSE_OF_UNKNOWN:
+ return "ON_BECAUSE_OF_UNKNOWN";
+ default:
+ return Integer.toString(why);
+ }
+ }
+
+ static @OnReason int translateWakeReasonToOnReason(@PowerManager.WakeReason int reason) {
+ switch (reason) {
+ case PowerManager.WAKE_REASON_POWER_BUTTON:
+ case PowerManager.WAKE_REASON_PLUGGED_IN:
+ case PowerManager.WAKE_REASON_GESTURE:
+ case PowerManager.WAKE_REASON_CAMERA_LAUNCH:
+ case PowerManager.WAKE_REASON_WAKE_KEY:
+ case PowerManager.WAKE_REASON_WAKE_MOTION:
+ case PowerManager.WAKE_REASON_LID:
+ return ON_BECAUSE_OF_USER;
+ case PowerManager.WAKE_REASON_APPLICATION:
+ return ON_BECAUSE_OF_APPLICATION;
+ default:
+ return ON_BECAUSE_OF_UNKNOWN;
+ }
+ }
+
+ /** Screen turned on because of a user-initiated action. */
+ int ON_BECAUSE_OF_USER = 1;
+ /** Screen turned on because of an application request or event */
+ int ON_BECAUSE_OF_APPLICATION = 2;
+ /** Screen turned on for an unknown reason */
+ int ON_BECAUSE_OF_UNKNOWN = 3;
+
+ int APPLICATION_LAYER = 2;
+ int APPLICATION_MEDIA_SUBLAYER = -2;
+ int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1;
+ int APPLICATION_PANEL_SUBLAYER = 1;
+ int APPLICATION_SUB_PANEL_SUBLAYER = 2;
+ int APPLICATION_ABOVE_SUB_PANEL_SUBLAYER = 3;
+
+ /**
+ * Convert the off reason to a human readable format.
+ */
+ static String offReasonToString(int why) {
+ switch (why) {
+ case OFF_BECAUSE_OF_ADMIN:
+ return "OFF_BECAUSE_OF_ADMIN";
+ case OFF_BECAUSE_OF_USER:
+ return "OFF_BECAUSE_OF_USER";
+ case OFF_BECAUSE_OF_TIMEOUT:
+ return "OFF_BECAUSE_OF_TIMEOUT";
+ default:
+ return Integer.toString(why);
+ }
+ }
+}
diff --git a/android/view/WindowMetrics.java b/android/view/WindowMetrics.java
new file mode 100644
index 0000000..52e4e15
--- /dev/null
+++ b/android/view/WindowMetrics.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Metrics about a Window, consisting of the bounds and {@link WindowInsets}.
+ * <p>
+ * This is usually obtained from {@link WindowManager#getCurrentWindowMetrics()} and
+ * {@link WindowManager#getMaximumWindowMetrics()}.
+ *
+ * @see WindowInsets#getInsets(int)
+ * @see WindowManager#getCurrentWindowMetrics()
+ * @see WindowManager#getMaximumWindowMetrics()
+ */
+public final class WindowMetrics {
+ private final @NonNull Rect mBounds;
+ private final @NonNull WindowInsets mWindowInsets;
+
+ public WindowMetrics(@NonNull Rect bounds, @NonNull WindowInsets windowInsets) {
+ mBounds = bounds;
+ mWindowInsets = windowInsets;
+ }
+
+ /**
+ * Returns the bounds of the area associated with this window or visual context.
+ * <p>
+ * <b>Note that the size of the reported bounds can have different size than
+ * {@link Display#getSize(Point)}.</b> This method reports the window size including all system
+ * bar areas, while {@link Display#getSize(Point)} reports the area excluding navigation bars
+ * and display cutout areas. The value reported by {@link Display#getSize(Point)} can be
+ * obtained by using:
+ * <pre class="prettyprint">
+ * final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
+ * // Gets all excluding insets
+ * final WindowInsets windowInsets = metrics.getWindowInsets();
+ * Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
+ * | WindowInsets.Type.displayCutout());
+ *
+ * int insetsWidth = insets.right + insets.left;
+ * int insetsHeight = insets.top + insets.bottom;
+ *
+ * // Legacy size that Display#getSize reports
+ * final Rect bounds = metrics.getBounds();
+ * final Size legacySize = new Size(bounds.width() - insetsWidth,
+ * bounds.height() - insetsHeight);
+ * </pre>
+ * </p>
+ *
+ * @return window bounds in pixels.
+ */
+ public @NonNull Rect getBounds() {
+ return mBounds;
+ }
+
+ /**
+ * Returns the {@link WindowInsets} of the area associated with this window or visual context.
+ *
+ * @return the {@link WindowInsets} of the visual area.
+ */
+ public @NonNull WindowInsets getWindowInsets() {
+ return mWindowInsets;
+ }
+}
diff --git a/android/view/WindowlessWindowManager.java b/android/view/WindowlessWindowManager.java
new file mode 100644
index 0000000..ae54f51
--- /dev/null
+++ b/android/view/WindowlessWindowManager.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.IBinder;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.MergedConfiguration;
+import android.window.ClientWindowFrames;
+
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+* A simplistic implementation of IWindowSession. Rather than managing Surfaces
+* as children of the display, it manages Surfaces as children of a given root.
+*
+* By parcelling the root surface, the app can offer another app content for embedding.
+* @hide
+*/
+public class WindowlessWindowManager implements IWindowSession {
+ private final static String TAG = "WindowlessWindowManager";
+
+ private class State {
+ SurfaceControl mSurfaceControl;
+ WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+ int mDisplayId;
+ IBinder mInputChannelToken;
+ Region mInputRegion;
+ State(SurfaceControl sc, WindowManager.LayoutParams p, int displayId,
+ IBinder inputChannelToken) {
+ mSurfaceControl = sc;
+ mParams.copyFrom(p);
+ mDisplayId = displayId;
+ mInputChannelToken = inputChannelToken;
+ }
+ };
+
+ /**
+ * Used to store SurfaceControl we've built for clients to
+ * reconfigure them if relayout is called.
+ */
+ final HashMap<IBinder, State> mStateForWindow = new HashMap<IBinder, State>();
+
+ public interface ResizeCompleteCallback {
+ public void finished(SurfaceControl.Transaction completion);
+ }
+
+ final HashMap<IBinder, ResizeCompleteCallback> mResizeCompletionForWindow =
+ new HashMap<IBinder, ResizeCompleteCallback>();
+
+ private final SurfaceSession mSurfaceSession = new SurfaceSession();
+ protected final SurfaceControl mRootSurface;
+ private final Configuration mConfiguration;
+ private final IWindowSession mRealWm;
+ private final IBinder mHostInputToken;
+
+ private int mForceHeight = -1;
+ private int mForceWidth = -1;
+
+ public WindowlessWindowManager(Configuration c, SurfaceControl rootSurface,
+ IBinder hostInputToken) {
+ mRootSurface = rootSurface;
+ mConfiguration = new Configuration(c);
+ mRealWm = WindowManagerGlobal.getWindowSession();
+ mHostInputToken = hostInputToken;
+ }
+
+ protected void setConfiguration(Configuration configuration) {
+ mConfiguration.setTo(configuration);
+ }
+
+ /**
+ * Utility API.
+ */
+ void setCompletionCallback(IBinder window, ResizeCompleteCallback callback) {
+ if (mResizeCompletionForWindow.get(window) != null) {
+ Log.w(TAG, "Unsupported overlapping resizes");
+ }
+ mResizeCompletionForWindow.put(window, callback);
+ }
+
+ protected void setTouchRegion(IBinder window, @Nullable Region region) {
+ State state;
+ synchronized (this) {
+ // Do everything while locked so that we synchronize with relayout. This should be a
+ // very infrequent operation.
+ state = mStateForWindow.get(window);
+ if (state == null) {
+ return;
+ }
+ if (Objects.equals(region, state.mInputRegion)) {
+ return;
+ }
+ state.mInputRegion = region != null ? new Region(region) : null;
+ if (state.mInputChannelToken != null) {
+ try {
+ mRealWm.updateInputChannel(state.mInputChannelToken, state.mDisplayId,
+ state.mSurfaceControl, state.mParams.flags, state.mParams.privateFlags,
+ state.mInputRegion);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to update surface input channel: ", e);
+ }
+ }
+ }
+ }
+
+ protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+ b.setParent(mRootSurface);
+ }
+
+ /**
+ * IWindowSession implementation.
+ */
+ @Override
+ public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
+ int viewVisibility, int displayId, InsetsState requestedVisibility,
+ InputChannel outInputChannel, InsetsState outInsetsState,
+ InsetsSourceControl[] outActiveControls) {
+ final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession)
+ .setFormat(attrs.format)
+ .setBLASTLayer()
+ .setName(attrs.getTitle().toString())
+ .setCallsite("WindowlessWindowManager.addToDisplay");
+ attachToParentSurface(window, b);
+ final SurfaceControl sc = b.build();
+
+ if (((attrs.inputFeatures &
+ WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0)) {
+ try {
+ if(mRealWm instanceof IWindowSession.Stub) {
+ mRealWm.grantInputChannel(displayId,
+ new SurfaceControl(sc, "WindowlessWindowManager.addToDisplay"),
+ window, mHostInputToken, attrs.flags, attrs.privateFlags, attrs.type,
+ outInputChannel);
+ } else {
+ mRealWm.grantInputChannel(displayId, sc, window, mHostInputToken, attrs.flags,
+ attrs.privateFlags, attrs.type, outInputChannel);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to grant input to surface: ", e);
+ }
+ }
+
+ final State state = new State(sc, attrs, displayId,
+ outInputChannel != null ? outInputChannel.getToken() : null);
+ synchronized (this) {
+ mStateForWindow.put(window.asBinder(), state);
+ }
+
+ final int res = WindowManagerGlobal.ADD_OKAY | WindowManagerGlobal.ADD_FLAG_APP_VISIBLE |
+ WindowManagerGlobal.ADD_FLAG_USE_BLAST;
+
+ // Include whether the window is in touch mode.
+ return isInTouchMode() ? res | WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE : res;
+ }
+
+ /**
+ * IWindowSession implementation. Currently this class doesn't need to support for multi-user.
+ */
+ @Override
+ public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
+ int viewVisibility, int displayId, int userId, InsetsState requestedVisibility,
+ InputChannel outInputChannel, InsetsState outInsetsState,
+ InsetsSourceControl[] outActiveControls) {
+ return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibility,
+ outInputChannel, outInsetsState, outActiveControls);
+ }
+
+ @Override
+ public int addToDisplayWithoutInputChannel(android.view.IWindow window,
+ android.view.WindowManager.LayoutParams attrs, int viewVisibility, int layerStackId,
+ android.view.InsetsState insetsState) {
+ return 0;
+ }
+
+ @Override
+ public void remove(android.view.IWindow window) throws RemoteException {
+ mRealWm.remove(window);
+ State state;
+ synchronized (this) {
+ state = mStateForWindow.remove(window.asBinder());
+ }
+ if (state == null) {
+ throw new IllegalArgumentException(
+ "Invalid window token (never added or removed already)");
+ }
+
+ try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
+ t.remove(state.mSurfaceControl).apply();
+ }
+ }
+
+ private boolean isOpaque(WindowManager.LayoutParams attrs) {
+ if (attrs.surfaceInsets != null && attrs.surfaceInsets.left != 0 ||
+ attrs.surfaceInsets.top != 0 || attrs.surfaceInsets.right != 0 ||
+ attrs.surfaceInsets.bottom != 0) {
+ return false;
+ }
+ return !PixelFormat.formatHasAlpha(attrs.format);
+ }
+
+ private boolean isInTouchMode() {
+ try {
+ return WindowManagerGlobal.getWindowSession().getInTouchMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to check if the window is in touch mode", e);
+ }
+ return false;
+ }
+
+ /** Access to package members for SystemWindow leashing
+ * @hide
+ */
+ protected IBinder getWindowBinder(View rootView) {
+ final ViewRootImpl root = rootView.getViewRootImpl();
+ if (root == null) {
+ return null;
+ }
+ return root.mWindow.asBinder();
+ }
+
+ /** @hide */
+ @Nullable
+ protected SurfaceControl getSurfaceControl(View rootView) {
+ final ViewRootImpl root = rootView.getViewRootImpl();
+ if (root == null) {
+ return null;
+ }
+ return getSurfaceControl(root.mWindow);
+ }
+
+ /** @hide */
+ @Nullable
+ protected SurfaceControl getSurfaceControl(IWindow window) {
+ final State s = mStateForWindow.get(window.asBinder());
+ if (s == null) {
+ return null;
+ }
+ return s.mSurfaceControl;
+ }
+
+ @Override
+ public int relayout(IWindow window, WindowManager.LayoutParams inAttrs,
+ int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
+ ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
+ SurfaceControl outSurfaceControl, InsetsState outInsetsState,
+ InsetsSourceControl[] outActiveControls, Point outSurfaceSize) {
+ final State state;
+ synchronized (this) {
+ state = mStateForWindow.get(window.asBinder());
+ }
+ if (state == null) {
+ throw new IllegalArgumentException(
+ "Invalid window token (never added or removed already)");
+ }
+ SurfaceControl sc = state.mSurfaceControl;
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+
+ int attrChanges = 0;
+ if (inAttrs != null) {
+ attrChanges = state.mParams.copyFrom(inAttrs);
+ }
+ WindowManager.LayoutParams attrs = state.mParams;
+
+ if (viewFlags == View.VISIBLE) {
+ outSurfaceSize.set(getSurfaceWidth(attrs), getSurfaceHeight(attrs));
+ t.setOpaque(sc, isOpaque(attrs)).show(sc).apply();
+ outSurfaceControl.copyFrom(sc, "WindowlessWindowManager.relayout");
+ } else {
+ t.hide(sc).apply();
+ outSurfaceControl.release();
+ }
+ outFrames.frame.set(0, 0, attrs.width, attrs.height);
+ outFrames.displayFrame.set(outFrames.frame);
+
+ mergedConfiguration.setConfiguration(mConfiguration, mConfiguration);
+
+ if ((attrChanges & WindowManager.LayoutParams.FLAGS_CHANGED) != 0
+ && state.mInputChannelToken != null) {
+ try {
+ if(mRealWm instanceof IWindowSession.Stub) {
+ mRealWm.updateInputChannel(state.mInputChannelToken, state.mDisplayId,
+ new SurfaceControl(sc, "WindowlessWindowManager.relayout"),
+ attrs.flags, attrs.privateFlags, state.mInputRegion);
+ } else {
+ mRealWm.updateInputChannel(state.mInputChannelToken, state.mDisplayId, sc,
+ attrs.flags, attrs.privateFlags, state.mInputRegion);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to update surface input channel: ", e);
+ }
+ }
+
+ // Include whether the window is in touch mode.
+ return isInTouchMode() ? WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE : 0;
+ }
+
+ @Override
+ public void prepareToReplaceWindows(android.os.IBinder appToken, boolean childrenOnly) {
+ }
+
+ @Override
+ public boolean outOfMemory(android.view.IWindow window) {
+ return false;
+ }
+
+ @Override
+ public void setInsets(android.view.IWindow window, int touchableInsets,
+ android.graphics.Rect contentInsets, android.graphics.Rect visibleInsets,
+ android.graphics.Region touchableRegion) {
+ }
+
+ @Override
+ public void finishDrawing(android.view.IWindow window,
+ android.view.SurfaceControl.Transaction postDrawTransaction) {
+ synchronized (this) {
+ final ResizeCompleteCallback c =
+ mResizeCompletionForWindow.get(window.asBinder());
+ if (c == null) {
+ // No one wanted the callback, but it wasn't necessarily unexpected.
+ postDrawTransaction.apply();
+ return;
+ }
+ c.finished(postDrawTransaction);
+ mResizeCompletionForWindow.remove(window.asBinder());
+ }
+ }
+
+ @Override
+ public void setInTouchMode(boolean showFocus) {
+ }
+
+ @Override
+ public boolean getInTouchMode() {
+ return false;
+ }
+
+ @Override
+ public boolean performHapticFeedback(int effectId, boolean always) {
+ return false;
+ }
+
+ @Override
+ public android.os.IBinder performDrag(android.view.IWindow window, int flags,
+ android.view.SurfaceControl surface, int touchSource, float touchX, float touchY,
+ float thumbCenterX, float thumbCenterY, android.content.ClipData data) {
+ return null;
+ }
+
+ @Override
+ public void reportDropResult(android.view.IWindow window, boolean consumed) {
+ }
+
+ @Override
+ public void cancelDragAndDrop(android.os.IBinder dragToken, boolean skipAnimation) {
+ }
+
+ @Override
+ public void dragRecipientEntered(android.view.IWindow window) {
+ }
+
+ @Override
+ public void dragRecipientExited(android.view.IWindow window) {
+ }
+
+ @Override
+ public void setWallpaperPosition(android.os.IBinder windowToken, float x, float y,
+ float xstep, float ystep) {
+ }
+
+ @Override
+ public void setWallpaperZoomOut(android.os.IBinder windowToken, float zoom) {
+ }
+
+ @Override
+ public void setShouldZoomOutWallpaper(android.os.IBinder windowToken, boolean shouldZoom) {
+ }
+
+ @Override
+ public void wallpaperOffsetsComplete(android.os.IBinder window) {
+ }
+
+ @Override
+ public void setWallpaperDisplayOffset(android.os.IBinder windowToken, int x, int y) {
+ }
+
+ @Override
+ public android.os.Bundle sendWallpaperCommand(android.os.IBinder window,
+ java.lang.String action, int x, int y, int z, android.os.Bundle extras, boolean sync) {
+ return null;
+ }
+
+ @Override
+ public void wallpaperCommandComplete(android.os.IBinder window, android.os.Bundle result) {
+ }
+
+ @Override
+ public void onRectangleOnScreenRequested(android.os.IBinder token,
+ android.graphics.Rect rectangle) {
+ }
+
+ @Override
+ public android.view.IWindowId getWindowId(android.os.IBinder window) {
+ return null;
+ }
+
+ @Override
+ public void pokeDrawLock(android.os.IBinder window) {
+ }
+
+ @Override
+ public boolean startMovingTask(android.view.IWindow window, float startX, float startY) {
+ return false;
+ }
+
+ @Override
+ public void finishMovingTask(android.view.IWindow window) {
+ }
+
+ @Override
+ public void updatePointerIcon(android.view.IWindow window) {
+ }
+
+ @Override
+ public void updateDisplayContentLocation(android.view.IWindow window, int x, int y,
+ int displayId) {
+ }
+
+ @Override
+ public void updateTapExcludeRegion(android.view.IWindow window,
+ android.graphics.Region region) {
+ }
+
+ @Override
+ public void insetsModified(android.view.IWindow window, android.view.InsetsState state) {
+ }
+
+ @Override
+ public void reportSystemGestureExclusionChanged(android.view.IWindow window,
+ java.util.List<android.graphics.Rect> exclusionRects) {
+ }
+
+ @Override
+ public void grantInputChannel(int displayId, SurfaceControl surface, IWindow window,
+ IBinder hostInputToken, int flags, int privateFlags, int type,
+ InputChannel outInputChannel) {
+ }
+
+ @Override
+ public void updateInputChannel(IBinder channelToken, int displayId, SurfaceControl surface,
+ int flags, int privateFlags, Region region) {
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+
+ private int getSurfaceWidth(WindowManager.LayoutParams attrs) {
+ final Rect surfaceInsets = attrs.surfaceInsets;
+ return surfaceInsets != null
+ ? attrs.width + surfaceInsets.left + surfaceInsets.right : attrs.width;
+ }
+ private int getSurfaceHeight(WindowManager.LayoutParams attrs) {
+ final Rect surfaceInsets = attrs.surfaceInsets;
+ return surfaceInsets != null
+ ? attrs.height + surfaceInsets.top + surfaceInsets.bottom : attrs.height;
+ }
+
+ @Override
+ public void grantEmbeddedWindowFocus(IWindow callingWindow, IBinder targetInputToken,
+ boolean grantFocus) {
+ }
+
+ @Override
+ public void generateDisplayHash(IWindow window, Rect boundsInWindow, String hashAlgorithm,
+ RemoteCallback callback) {
+ }
+}
diff --git a/android/view/accessibility/AccessibilityCache.java b/android/view/accessibility/AccessibilityCache.java
new file mode 100644
index 0000000..d14dc6e
--- /dev/null
+++ b/android/view/accessibility/AccessibilityCache.java
@@ -0,0 +1,689 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.os.Build;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.LongArray;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos.
+ * It is updated when windows change or nodes change.
+ * @hide
+ */
+public class AccessibilityCache {
+
+ private static final String LOG_TAG = "AccessibilityCache";
+
+ private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) && Build.IS_DEBUGGABLE;
+
+ private static final boolean VERBOSE =
+ Log.isLoggable(LOG_TAG, Log.VERBOSE) && Build.IS_DEBUGGABLE;
+
+ private static final boolean CHECK_INTEGRITY = Build.IS_ENG;
+
+ /**
+ * {@link AccessibilityEvent} types that are critical for the cache to stay up to date
+ *
+ * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to
+ * make sure that the events are delivered to cache regardless of
+ * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes}
+ */
+ public static final int CACHE_CRITICAL_EVENTS_MASK =
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
+ | AccessibilityEvent.TYPE_VIEW_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_SELECTED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_SCROLLED
+ | AccessibilityEvent.TYPE_WINDOWS_CHANGED
+ | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
+
+ private final Object mLock = new Object();
+
+ private final AccessibilityNodeRefresher mAccessibilityNodeRefresher;
+
+ private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+
+ private int mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+
+ private boolean mIsAllWindowsCached;
+
+ // The SparseArray of all {@link AccessibilityWindowInfo}s on all displays.
+ // The key of outer SparseArray is display ID and the key of inner SparseArray is window ID.
+ private final SparseArray<SparseArray<AccessibilityWindowInfo>> mWindowCacheByDisplay =
+ new SparseArray<>();
+
+ private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
+ new SparseArray<>();
+
+ private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
+ new SparseArray<>();
+
+ public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) {
+ mAccessibilityNodeRefresher = nodeRefresher;
+ }
+
+ /**
+ * Sets all {@link AccessibilityWindowInfo}s of all displays into the cache.
+ * The key of SparseArray is display ID.
+ *
+ * @param windowsOnAllDisplays The accessibility windows of all displays.
+ */
+ public void setWindowsOnAllDisplays(
+ SparseArray<List<AccessibilityWindowInfo>> windowsOnAllDisplays) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Set windows");
+ }
+ clearWindowCacheLocked();
+ if (windowsOnAllDisplays == null) {
+ return;
+ }
+
+ final int displayCounts = windowsOnAllDisplays.size();
+ for (int i = 0; i < displayCounts; i++) {
+ final List<AccessibilityWindowInfo> windowsOfDisplay =
+ windowsOnAllDisplays.valueAt(i);
+
+ if (windowsOfDisplay == null) {
+ continue;
+ }
+
+ final int displayId = windowsOnAllDisplays.keyAt(i);
+ final int windowCount = windowsOfDisplay.size();
+ for (int j = 0; j < windowCount; j++) {
+ addWindowByDisplayLocked(displayId, windowsOfDisplay.get(j));
+ }
+ }
+ mIsAllWindowsCached = true;
+ }
+ }
+
+ /**
+ * Sets an {@link AccessibilityWindowInfo} into the cache.
+ *
+ * @param window The accessibility window.
+ */
+ public void addWindow(AccessibilityWindowInfo window) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Caching window: " + window.getId() + " at display Id [ "
+ + window.getDisplayId() + " ]");
+ }
+ addWindowByDisplayLocked(window.getDisplayId(), window);
+ }
+ }
+
+ private void addWindowByDisplayLocked(int displayId, AccessibilityWindowInfo window) {
+ SparseArray<AccessibilityWindowInfo> windows = mWindowCacheByDisplay.get(displayId);
+ if (windows == null) {
+ windows = new SparseArray<>();
+ mWindowCacheByDisplay.put(displayId, windows);
+ }
+ final int windowId = window.getId();
+ windows.put(windowId, new AccessibilityWindowInfo(window));
+ }
+ /**
+ * Notifies the cache that the something in the UI changed. As a result
+ * the cache will either refresh some nodes or evict some nodes.
+ *
+ * Note: any event that ends up affecting the cache should also be present in
+ * {@link #CACHE_CRITICAL_EVENTS_MASK}
+ *
+ * @param event An event.
+ */
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ AccessibilityNodeInfo nodeToRefresh = null;
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "onAccessibilityEvent(" + event + ")");
+ }
+ final int eventType = event.getEventType();
+ switch (eventType) {
+ case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
+ if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
+ removeCachedNodeLocked(mAccessibilityFocusedWindow, mAccessibilityFocus);
+ }
+ mAccessibilityFocus = event.getSourceNodeId();
+ mAccessibilityFocusedWindow = event.getWindowId();
+ nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow,
+ mAccessibilityFocus);
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
+ if (mAccessibilityFocus == event.getSourceNodeId()
+ && mAccessibilityFocusedWindow == event.getWindowId()) {
+ nodeToRefresh = removeCachedNodeLocked(mAccessibilityFocusedWindow,
+ mAccessibilityFocus);
+ mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
+ if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
+ removeCachedNodeLocked(event.getWindowId(), mInputFocus);
+ }
+ mInputFocus = event.getSourceNodeId();
+ nodeToRefresh = removeCachedNodeLocked(event.getWindowId(), mInputFocus);
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_SELECTED:
+ case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
+ case AccessibilityEvent.TYPE_VIEW_CLICKED:
+ case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
+ nodeToRefresh = removeCachedNodeLocked(event.getWindowId(),
+ event.getSourceNodeId());
+ } break;
+
+ case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
+ synchronized (mLock) {
+ final int windowId = event.getWindowId();
+ final long sourceId = event.getSourceNodeId();
+ if ((event.getContentChangeTypes()
+ & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
+ clearSubTreeLocked(windowId, sourceId);
+ } else {
+ nodeToRefresh = removeCachedNodeLocked(windowId, sourceId);
+ }
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
+ clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
+ } break;
+
+ case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
+ if (event.getWindowChanges()
+ == AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED) {
+ // Don't need to clear all cache. Unless the changes are related to
+ // content, we won't clear all cache here with clear().
+ clearWindowCacheLocked();
+ break;
+ }
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
+ clear();
+ } break;
+ }
+ }
+
+ if (nodeToRefresh != null) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Refreshing and re-adding cached node.");
+ }
+ if (mAccessibilityNodeRefresher.refreshNode(nodeToRefresh, true)) {
+ add(nodeToRefresh);
+ }
+ }
+ if (CHECK_INTEGRITY) {
+ checkIntegrity();
+ }
+ }
+
+ private AccessibilityNodeInfo removeCachedNodeLocked(int windowId, long sourceId) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Removing cached node.");
+ }
+ LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
+ if (nodes == null) {
+ return null;
+ }
+ AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
+ // If the source is not in the cache - nothing to do.
+ if (cachedInfo == null) {
+ return null;
+ }
+ nodes.remove(sourceId);
+ return cachedInfo;
+ }
+
+ /**
+ * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
+ * window and the accessibility id of the node.
+ *
+ * @param windowId The id of the window hosting the node.
+ * @param accessibilityNodeId The info accessibility node id.
+ * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
+ */
+ public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
+ synchronized(mLock) {
+ LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
+ if (nodes == null) {
+ return null;
+ }
+ AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
+ if (info != null) {
+ // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
+ // will wipe the data of the cached info.
+ info = new AccessibilityNodeInfo(info);
+ }
+ if (VERBOSE) {
+ Log.i(LOG_TAG, "get(0x" + Long.toHexString(accessibilityNodeId) + ") = " + info);
+ }
+ return info;
+ }
+ }
+
+ /**
+ * Gets all {@link AccessibilityWindowInfo}s of all displays from the cache.
+ *
+ * @return All cached {@link AccessibilityWindowInfo}s of all displays
+ * or null if such not found. The key of SparseArray is display ID.
+ */
+ public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() {
+ synchronized (mLock) {
+ if (!mIsAllWindowsCached) {
+ return null;
+ }
+ final SparseArray<List<AccessibilityWindowInfo>> returnWindows = new SparseArray<>();
+ final int displayCounts = mWindowCacheByDisplay.size();
+
+ if (displayCounts > 0) {
+ for (int i = 0; i < displayCounts; i++) {
+ final int displayId = mWindowCacheByDisplay.keyAt(i);
+ final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
+ mWindowCacheByDisplay.valueAt(i);
+
+ if (windowsOfDisplay == null) {
+ continue;
+ }
+
+ final int windowCount = windowsOfDisplay.size();
+ if (windowCount > 0) {
+ // Careful to return the windows in a decreasing layer order.
+ SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
+ sortedWindows.clear();
+
+ for (int j = 0; j < windowCount; j++) {
+ AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j);
+ sortedWindows.put(window.getLayer(), window);
+ }
+
+ // It's possible in transient conditions for two windows to share the same
+ // layer, which results in sortedWindows being smaller than
+ // mWindowCacheByDisplay
+ final int sortedWindowCount = sortedWindows.size();
+ List<AccessibilityWindowInfo> windows =
+ new ArrayList<>(sortedWindowCount);
+ for (int j = sortedWindowCount - 1; j >= 0; j--) {
+ AccessibilityWindowInfo window = sortedWindows.valueAt(j);
+ windows.add(new AccessibilityWindowInfo(window));
+ sortedWindows.removeAt(j);
+ }
+ returnWindows.put(displayId, windows);
+ }
+ }
+ return returnWindows;
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Gets an {@link AccessibilityWindowInfo} by windowId.
+ *
+ * @param windowId The id of the window.
+ *
+ * @return The {@link AccessibilityWindowInfo} or null if such not found.
+ */
+ public AccessibilityWindowInfo getWindow(int windowId) {
+ synchronized (mLock) {
+ final int displayCounts = mWindowCacheByDisplay.size();
+ for (int i = 0; i < displayCounts; i++) {
+ final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
+ mWindowCacheByDisplay.valueAt(i);
+ if (windowsOfDisplay == null) {
+ continue;
+ }
+
+ AccessibilityWindowInfo window = windowsOfDisplay.get(windowId);
+ if (window != null) {
+ return new AccessibilityWindowInfo(window);
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Caches an {@link AccessibilityNodeInfo}.
+ *
+ * @param info The node to cache.
+ */
+ public void add(AccessibilityNodeInfo info) {
+ synchronized(mLock) {
+ if (VERBOSE) {
+ Log.i(LOG_TAG, "add(" + info + ")");
+ }
+
+ final int windowId = info.getWindowId();
+ LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
+ if (nodes == null) {
+ nodes = new LongSparseArray<>();
+ mNodeCache.put(windowId, nodes);
+ }
+
+ final long sourceId = info.getSourceNodeId();
+ AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
+ if (oldInfo != null) {
+ // If the added node is in the cache we have to be careful if
+ // the new one represents a source state where some of the
+ // children have been removed to remove the descendants that
+ // are no longer present.
+ final LongArray newChildrenIds = info.getChildNodeIds();
+
+ final int oldChildCount = oldInfo.getChildCount();
+ for (int i = 0; i < oldChildCount; i++) {
+ final long oldChildId = oldInfo.getChildId(i);
+ // If the child is no longer present, remove the sub-tree.
+ if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
+ clearSubTreeLocked(windowId, oldChildId);
+ }
+ if (nodes.get(sourceId) == null) {
+ // We've removed (and thus recycled) this node because it was its own
+ // ancestor (the app gave us bad data), we can't continue using it.
+ // Clear the cache for this window and give up on adding the node.
+ clearNodesForWindowLocked(windowId);
+ return;
+ }
+ }
+
+ // Also be careful if the parent has changed since the new
+ // parent may be a predecessor of the old parent which will
+ // add cycles to the cache.
+ final long oldParentId = oldInfo.getParentNodeId();
+ if (info.getParentNodeId() != oldParentId) {
+ clearSubTreeLocked(windowId, oldParentId);
+ }
+ }
+
+ // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
+ // will wipe the data of the cached info.
+ AccessibilityNodeInfo clone = new AccessibilityNodeInfo(info);
+ nodes.put(sourceId, clone);
+ if (clone.isAccessibilityFocused()) {
+ if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID
+ && mAccessibilityFocus != sourceId) {
+ removeCachedNodeLocked(windowId, mAccessibilityFocus);
+ }
+ mAccessibilityFocus = sourceId;
+ mAccessibilityFocusedWindow = windowId;
+ } else if (mAccessibilityFocus == sourceId) {
+ mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ }
+ if (clone.isFocused()) {
+ mInputFocus = sourceId;
+ }
+ }
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public void clear() {
+ synchronized(mLock) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "clear()");
+ }
+ clearWindowCacheLocked();
+ final int nodesForWindowCount = mNodeCache.size();
+ for (int i = nodesForWindowCount - 1; i >= 0; i--) {
+ final int windowId = mNodeCache.keyAt(i);
+ clearNodesForWindowLocked(windowId);
+ }
+
+ mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+
+ mAccessibilityFocusedWindow = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ }
+ }
+
+ private void clearWindowCacheLocked() {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "clearWindowCacheLocked");
+ }
+ final int displayCounts = mWindowCacheByDisplay.size();
+
+ if (displayCounts > 0) {
+ for (int i = displayCounts - 1; i >= 0; i--) {
+ final int displayId = mWindowCacheByDisplay.keyAt(i);
+ final SparseArray<AccessibilityWindowInfo> windows =
+ mWindowCacheByDisplay.get(displayId);
+ if (windows != null) {
+ windows.clear();
+ }
+ mWindowCacheByDisplay.remove(displayId);
+ }
+ }
+ mIsAllWindowsCached = false;
+ }
+
+ /**
+ * Clears nodes for the window with the given id
+ */
+ private void clearNodesForWindowLocked(int windowId) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
+ }
+ LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
+ if (nodes == null) {
+ return;
+ }
+ mNodeCache.remove(windowId);
+ }
+
+ /**
+ * Clears a subtree rooted at the node with the given id that is
+ * hosted in a given window.
+ *
+ * @param windowId The id of the hosting window.
+ * @param rootNodeId The root id.
+ */
+ private void clearSubTreeLocked(int windowId, long rootNodeId) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Clearing cached subtree.");
+ }
+ LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
+ if (nodes != null) {
+ clearSubTreeRecursiveLocked(nodes, rootNodeId);
+ }
+ }
+
+ /**
+ * Clears a subtree given a pointer to the root id and the nodes
+ * in the hosting window.
+ *
+ * @param nodes The nodes in the hosting window.
+ * @param rootNodeId The id of the root to evict.
+ *
+ * @return {@code true} if the cache was cleared
+ */
+ private boolean clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
+ long rootNodeId) {
+ AccessibilityNodeInfo current = nodes.get(rootNodeId);
+ if (current == null) {
+ // The node isn't in the cache, but its descendents might be.
+ clear();
+ return true;
+ }
+ nodes.remove(rootNodeId);
+ final int childCount = current.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final long childNodeId = current.getChildId(i);
+ if (clearSubTreeRecursiveLocked(nodes, childNodeId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check the integrity of the cache which is nodes from different windows
+ * are not mixed, there is a single active window, there is a single focused
+ * window, for every window there are no duplicates nodes, all nodes for a
+ * window are connected, for every window there is a single input focused
+ * node, and for every window there is a single accessibility focused node.
+ */
+ public void checkIntegrity() {
+ synchronized (mLock) {
+ // Get the root.
+ if (mWindowCacheByDisplay.size() <= 0 && mNodeCache.size() == 0) {
+ return;
+ }
+
+ AccessibilityWindowInfo focusedWindow = null;
+ AccessibilityWindowInfo activeWindow = null;
+
+ final int displayCounts = mWindowCacheByDisplay.size();
+ for (int i = 0; i < displayCounts; i++) {
+ final SparseArray<AccessibilityWindowInfo> windowsOfDisplay =
+ mWindowCacheByDisplay.valueAt(i);
+
+ if (windowsOfDisplay == null) {
+ continue;
+ }
+
+ final int windowCount = windowsOfDisplay.size();
+ for (int j = 0; j < windowCount; j++) {
+ final AccessibilityWindowInfo window = windowsOfDisplay.valueAt(j);
+
+ // Check for one active window.
+ if (window.isActive()) {
+ if (activeWindow != null) {
+ Log.e(LOG_TAG, "Duplicate active window:" + window);
+ } else {
+ activeWindow = window;
+ }
+ }
+ // Check for one focused window.
+ if (window.isFocused()) {
+ if (focusedWindow != null) {
+ Log.e(LOG_TAG, "Duplicate focused window:" + window);
+ } else {
+ focusedWindow = window;
+ }
+ }
+ }
+ }
+
+ // Traverse the tree and do some checks.
+ AccessibilityNodeInfo accessFocus = null;
+ AccessibilityNodeInfo inputFocus = null;
+
+ final int nodesForWindowCount = mNodeCache.size();
+ for (int i = 0; i < nodesForWindowCount; i++) {
+ LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
+ if (nodes.size() <= 0) {
+ continue;
+ }
+
+ ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
+ final int windowId = mNodeCache.keyAt(i);
+
+ final int nodeCount = nodes.size();
+ for (int j = 0; j < nodeCount; j++) {
+ AccessibilityNodeInfo node = nodes.valueAt(j);
+
+ // Check for duplicates
+ if (!seen.add(node)) {
+ Log.e(LOG_TAG, "Duplicate node: " + node
+ + " in window:" + windowId);
+ // Stop now as we potentially found a loop.
+ continue;
+ }
+
+ // Check for one accessibility focus.
+ if (node.isAccessibilityFocused()) {
+ if (accessFocus != null) {
+ Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
+ + " in window:" + windowId);
+ } else {
+ accessFocus = node;
+ }
+ }
+
+ // Check for one input focus.
+ if (node.isFocused()) {
+ if (inputFocus != null) {
+ Log.e(LOG_TAG, "Duplicate input focus: " + node
+ + " in window:" + windowId);
+ } else {
+ inputFocus = node;
+ }
+ }
+
+ // The node should be a child of its parent if we have the parent.
+ AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
+ if (nodeParent != null) {
+ boolean childOfItsParent = false;
+ final int childCount = nodeParent.getChildCount();
+ for (int k = 0; k < childCount; k++) {
+ AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
+ if (child == node) {
+ childOfItsParent = true;
+ break;
+ }
+ }
+ if (!childOfItsParent) {
+ Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
+ + nodeParent + " and child: " + node);
+ }
+ }
+
+ // The node should be the parent of its child if we have the child.
+ final int childCount = node.getChildCount();
+ for (int k = 0; k < childCount; k++) {
+ AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
+ if (child != null) {
+ AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
+ if (parent != node) {
+ Log.e(LOG_TAG, "Invalid child-parent relation between child: "
+ + node + " and parent: " + nodeParent);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Layer of indirection included to break dependency chain for testing
+ public static class AccessibilityNodeRefresher {
+ /** Refresh the given AccessibilityNodeInfo object. */
+ public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) {
+ return info.refresh(null, bypassCache);
+ }
+
+ /** Refresh the given AccessibilityWindowInfo object. */
+ public boolean refreshWindow(AccessibilityWindowInfo info) {
+ return info.refresh();
+ }
+ }
+}
diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java
new file mode 100644
index 0000000..f6d6fde
--- /dev/null
+++ b/android/view/accessibility/AccessibilityEvent.java
@@ -0,0 +1,1499 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.internal.util.BitUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * This class represents accessibility events that are sent by the system when
+ * something notable happens in the user interface. For example, when a
+ * {@link android.widget.Button} is clicked, a {@link android.view.View} is focused, etc.
+ * </p>
+ * <p>
+ * An accessibility event is fired by an individual view which populates the event with
+ * data for its state and requests from its parent to send the event to interested
+ * parties. The parent can optionally modify or even block the event based on its broader
+ * understanding of the user interface's context.
+ * </p>
+ * <p>
+ * The main purpose of an accessibility event is to communicate changes in the UI to an
+ * {@link android.accessibilityservice.AccessibilityService}. The service may then inspect,
+ * if needed the user interface by examining the View hierarchy, as represented by a tree of
+ * {@link AccessibilityNodeInfo}s (snapshot of a View state)
+ * which can be used for exploring the window content. Note that the privilege for accessing
+ * an event's source, thus the window content, has to be explicitly requested. For more
+ * details refer to {@link android.accessibilityservice.AccessibilityService}. If an
+ * accessibility service has not requested to retrieve the window content the event will
+ * not contain reference to its source. Also for events of type
+ * {@link #TYPE_NOTIFICATION_STATE_CHANGED} the source is never available.
+ * </p>
+ * <p>
+ * This class represents various semantically different accessibility event
+ * types. Each event type has an associated set of related properties. In other
+ * words, each event type is characterized via a subset of the properties exposed
+ * by this class. For each event type there is a corresponding constant defined
+ * in this class. Follows a specification of the event types and their associated properties:
+ * </p>
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating and processing AccessibilityEvents, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ * <p>
+ * <b>VIEW TYPES</b></br>
+ * </p>
+ * <p>
+ * <b>View clicked</b> - represents the event of clicking on a {@link android.view.View}
+ * like {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.</br>
+ * <em>Type:</em>{@link #TYPE_VIEW_CLICKED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>View long clicked</b> - represents the event of long clicking on a {@link android.view.View}
+ * like {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc </br>
+ * <em>Type:</em>{@link #TYPE_VIEW_LONG_CLICKED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>View selected</b> - represents the event of selecting an item usually in
+ * the context of an {@link android.widget.AdapterView}.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_SELECTED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>View focused</b> - represents the event of focusing a
+ * {@link android.view.View}.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_FOCUSED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>View text changed</b> - represents the event of changing the text of an
+ * {@link android.widget.EditText}.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_TEXT_CHANGED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getText()} - The new text of the source.</li>
+ * <li>{@link #getBeforeText()} - The text of the source before the change.</li>
+ * <li>{@link #getFromIndex()} - The text change start index.</li>
+ * <li>{@link #getAddedCount()} - The number of added characters.</li>
+ * <li>{@link #getRemovedCount()} - The number of removed characters.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>View text selection changed</b> - represents the event of changing the text
+ * selection of an {@link android.widget.EditText}.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_TEXT_SELECTION_CHANGED} </br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <b>View text traversed at movement granularity</b> - represents the event of traversing the
+ * text of a view at a given granularity. For example, moving to the next word.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY} </br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
+ * was traversed.</li>
+ * <li>{@link #getText()} - The text of the source's sub-tree.</li>
+ * <li>{@link #getFromIndex()} - The start the text that was skipped over in this movement.
+ * This is the starting point when moving forward through the text, but not when moving
+ * back.</li>
+ * <li>{@link #getToIndex()} - The end of the text that was skipped over in this movement.
+ * This is the ending point when moving forward through the text, but not when moving
+ * back.</li>
+ * <li>{@link #getAction()} - Gets traversal action which specifies the direction.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>View scrolled</b> - represents the event of scrolling a view. </br>
+ * <em>Type:</em> {@link #TYPE_VIEW_SCROLLED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getScrollDeltaX()} - The difference in the horizontal position.</li>
+ * <li>{@link #getScrollDeltaY()} - The difference in the vertical position.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>TRANSITION TYPES</b></br>
+ * </p>
+ * <p>
+ * <b>Window state changed</b> - represents the event of a change to a section of
+ * the user interface that is visually distinct. Should be sent from either the
+ * root view of a window or from a view that is marked as a pane
+ * {@link android.view.View#setAccessibilityPaneTitle(CharSequence)}. Note that changes
+ * to true windows are represented by {@link #TYPE_WINDOWS_CHANGED}.</br>
+ * <em>Type:</em> {@link #TYPE_WINDOW_STATE_CHANGED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getContentChangeTypes()} - The type of state changes.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getText()} - The text of the source's sub-tree, including the pane titles.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>Window content changed</b> - represents the event of change in the
+ * content of a window. This change can be adding/removing view, changing
+ * a view size, etc.</br>
+ * </p>
+ * <p>
+ * <em>Type:</em> {@link #TYPE_WINDOW_CONTENT_CHANGED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getContentChangeTypes()} - The type of content changes.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>Windows changed</b> - represents a change in the windows shown on
+ * the screen such as a window appeared, a window disappeared, a window size changed,
+ * a window layer changed, etc. These events should only come from the system, which is responsible
+ * for managing windows. The list of windows is available from
+ * {@link android.accessibilityservice.AccessibilityService#getWindows()}.
+ * For regions of the user interface that are presented as windows but are
+ * controlled by an app's process, use {@link #TYPE_WINDOW_STATE_CHANGED}.</br>
+ * <em>Type:</em> {@link #TYPE_WINDOWS_CHANGED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getWindowChanges()}</li> - The specific change to the source window
+ * </ul>
+ * <em>Note:</em> You can retrieve the {@link AccessibilityWindowInfo} for the window
+ * source of the event by looking through the list returned by
+ * {@link android.accessibilityservice.AccessibilityService#getWindows()} for the window whose ID
+ * matches {@link #getWindowId()}.
+ * </p>
+ * <p>
+ * <b>NOTIFICATION TYPES</b></br>
+ * </p>
+ * <p>
+ * <b>Notification state changed</b> - represents the event showing a transient piece of information
+ * to the user. This information may be a {@link android.app.Notification} or
+ * {@link android.widget.Toast}.</br>
+ * <em>Type:</em> {@link #TYPE_NOTIFICATION_STATE_CHANGED}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getParcelableData()} - The posted {@link android.app.Notification}, if
+ * applicable.</li>
+ * <li>{@link #getText()} - Displayed text of the {@link android.widget.Toast}, if applicable,
+ * or may contain text from the {@link android.app.Notification}, although
+ * {@link #getParcelableData()} is a richer set of data for {@link android.app.Notification}.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>EXPLORATION TYPES</b></br>
+ * </p>
+ * <p>
+ * <b>View hover enter</b> - represents the event of beginning to hover
+ * over a {@link android.view.View}. The hover may be generated via
+ * exploring the screen by touch or via a pointing device.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_HOVER_ENTER}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <b>View hover exit</b> - represents the event of stopping to hover
+ * over a {@link android.view.View}. The hover may be generated via
+ * exploring the screen by touch or via a pointing device.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_HOVER_EXIT}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * <b>Touch interaction start</b> - represents the event of starting a touch
+ * interaction, which is the user starts touching the screen.</br>
+ * <em>Type:</em> {@link #TYPE_TOUCH_INTERACTION_START}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * </ul>
+ * <em>Note:</em> This event is fired only by the system and is not passed to the
+ * view tree to be populated.</br>
+ * </p>
+ * <p>
+ * <b>Touch interaction end</b> - represents the event of ending a touch
+ * interaction, which is the user stops touching the screen.</br>
+ * <em>Type:</em> {@link #TYPE_TOUCH_INTERACTION_END}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * </ul>
+ * <em>Note:</em> This event is fired only by the system and is not passed to the
+ * view tree to be populated.</br>
+ * </p>
+ * <p>
+ * <b>Touch exploration gesture start</b> - represents the event of starting a touch
+ * exploring gesture.</br>
+ * <em>Type:</em> {@link #TYPE_TOUCH_EXPLORATION_GESTURE_START}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * </ul>
+ * <em>Note:</em> This event is fired only by the system and is not passed to the
+ * view tree to be populated.</br>
+ * </p>
+ * <p>
+ * <b>Touch exploration gesture end</b> - represents the event of ending a touch
+ * exploring gesture.</br>
+ * <em>Type:</em> {@link #TYPE_TOUCH_EXPLORATION_GESTURE_END}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * </ul>
+ * <em>Note:</em> This event is fired only by the system and is not passed to the
+ * view tree to be populated.</br>
+ * </p>
+ * <p>
+ * <b>Touch gesture detection start</b> - represents the event of starting a user
+ * gesture detection.</br>
+ * <em>Type:</em> {@link #TYPE_GESTURE_DETECTION_START}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * </ul>
+ * <em>Note:</em> This event is fired only by the system and is not passed to the
+ * view tree to be populated.</br>
+ * </p>
+ * <p>
+ * <b>Touch gesture detection end</b> - represents the event of ending a user
+ * gesture detection.</br>
+ * <em>Type:</em> {@link #TYPE_GESTURE_DETECTION_END}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * </ul>
+ * <em>Note:</em> This event is fired only by the system and is not passed to the
+ * view tree to be populated.</br>
+ * </p>
+ * <p>
+ * <b>MISCELLANEOUS TYPES</b></br>
+ * </p>
+ * <p>
+ * <b>Announcement</b> - represents the event of an application requesting a screen reader to make
+ * an announcement. Because the event carries no semantic meaning, this event is appropriate only
+ * in exceptional situations where additional screen reader output is needed but other types of
+ * accessibility services do not need to be aware of the change.</br>
+ * <em>Type:</em> {@link #TYPE_ANNOUNCEMENT}</br>
+ * <em>Properties:</em></br>
+ * <ul>
+ * <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getSource()} - The source info (for registered clients).</li>
+ * <li>{@link #getClassName()} - The class name of the source.</li>
+ * <li>{@link #getPackageName()} - The package name of the source.</li>
+ * <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getText()} - The text of the announcement.</li>
+ * </ul>
+ * </p>
+ *
+ * @see android.view.accessibility.AccessibilityManager
+ * @see android.accessibilityservice.AccessibilityService
+ * @see AccessibilityNodeInfo
+ */
+public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable {
+ private static final String LOG_TAG = "AccessibilityEvent";
+
+ private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) && Build.IS_DEBUGGABLE;
+
+ /** @hide */
+ public static final boolean DEBUG_ORIGIN = false;
+
+ /**
+ * Invalid selection/focus position.
+ *
+ * @see #getCurrentItemIndex()
+ */
+ public static final int INVALID_POSITION = -1;
+
+ /**
+ * Maximum length of the text fields.
+ *
+ * @see #getBeforeText()
+ * @see #getText()
+ * </br>
+ * Note: This constant is no longer needed since there
+ * is no limit on the length of text that is contained
+ * in an accessibility event anymore.
+ */
+ @Deprecated
+ public static final int MAX_TEXT_LENGTH = 500;
+
+ /**
+ * Represents the event of clicking on a {@link android.view.View} like
+ * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
+ */
+ public static final int TYPE_VIEW_CLICKED = 0x00000001;
+
+ /**
+ * Represents the event of long clicking on a {@link android.view.View} like
+ * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
+ */
+ public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002;
+
+ /**
+ * Represents the event of selecting an item usually in the context of an
+ * {@link android.widget.AdapterView}.
+ */
+ public static final int TYPE_VIEW_SELECTED = 0x00000004;
+
+ /**
+ * Represents the event of setting input focus of a {@link android.view.View}.
+ */
+ public static final int TYPE_VIEW_FOCUSED = 0x00000008;
+
+ /**
+ * Represents the event of changing the text of an {@link android.widget.EditText}.
+ */
+ public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010;
+
+ /**
+ * Represents the event of a change to a visually distinct section of the user interface.
+ * These events should only be dispatched from {@link android.view.View}s that have
+ * accessibility pane titles, and replaces {@link #TYPE_WINDOW_CONTENT_CHANGED} for those
+ * sources. Details about the change are available from {@link #getContentChangeTypes()}.
+ */
+ public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020;
+
+ /**
+ * Represents the event showing a {@link android.app.Notification}.
+ */
+ public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040;
+
+ /**
+ * Represents the event of a hover enter over a {@link android.view.View}.
+ */
+ public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080;
+
+ /**
+ * Represents the event of a hover exit over a {@link android.view.View}.
+ */
+ public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100;
+
+ /**
+ * Represents the event of starting a touch exploration gesture.
+ */
+ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200;
+
+ /**
+ * Represents the event of ending a touch exploration gesture.
+ */
+ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400;
+
+ /**
+ * Represents the event of changing the content of a window and more
+ * specifically the sub-tree rooted at the event's source.
+ */
+ public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800;
+
+ /**
+ * Represents the event of scrolling a view. This event type is generally not sent directly.
+ * @see android.view.View#onScrollChanged(int, int, int, int)
+ */
+ public static final int TYPE_VIEW_SCROLLED = 0x00001000;
+
+ /**
+ * Represents the event of changing the selection in an {@link android.widget.EditText}.
+ */
+ public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000;
+
+ /**
+ * Represents the event of an application making an announcement.
+ */
+ public static final int TYPE_ANNOUNCEMENT = 0x00004000;
+
+ /**
+ * Represents the event of gaining accessibility focus.
+ */
+ public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000;
+
+ /**
+ * Represents the event of clearing accessibility focus.
+ */
+ public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000;
+
+ /**
+ * Represents the event of traversing the text of a view at a given movement granularity.
+ */
+ public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000;
+
+ /**
+ * Represents the event of beginning gesture detection.
+ */
+ public static final int TYPE_GESTURE_DETECTION_START = 0x00040000;
+
+ /**
+ * Represents the event of ending gesture detection.
+ */
+ public static final int TYPE_GESTURE_DETECTION_END = 0x00080000;
+
+ /**
+ * Represents the event of the user starting to touch the screen.
+ */
+ public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000;
+
+ /**
+ * Represents the event of the user ending to touch the screen.
+ */
+ public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000;
+
+ /**
+ * Represents the event change in the system windows shown on the screen. This event type should
+ * only be dispatched by the system.
+ */
+ public static final int TYPE_WINDOWS_CHANGED = 0x00400000;
+
+ /**
+ * Represents the event of a context click on a {@link android.view.View}.
+ */
+ public static final int TYPE_VIEW_CONTEXT_CLICKED = 0x00800000;
+
+ /**
+ * Represents the event of the assistant currently reading the users screen context.
+ */
+ public static final int TYPE_ASSIST_READING_CONTEXT = 0x01000000;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * The type of change is not defined.
+ */
+ public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * One or more content changes occurred in the the subtree rooted at the source node,
+ * or the subtree's structure changed when a node was added or removed.
+ */
+ public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * The node's text changed.
+ */
+ public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * The node's content description changed.
+ */
+ public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * The node's pane title changed.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 0x00000008;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * The node has a pane title, and either just appeared or just was assigned a title when it
+ * had none before.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 0x00000010;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * Can mean one of two slightly different things. The primary meaning is that the node has
+ * a pane title, and was removed from the node hierarchy. It will also be sent if the pane
+ * title is set to {@code null} after it contained a title.
+ * No source will be returned if the node is no longer on the screen. To make the change more
+ * clear for the user, the first entry in {@link #getText()} will return the value that would
+ * have been returned by {@code getSource().getPaneTitle()}.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 0x00000020;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * state description of the node as returned by
+ * {@link AccessibilityNodeInfo#getStateDescription} changed. If part of the state description
+ * changes, the changed part can be put into event text. For example, if state description
+ * changed from "on, wifi signal full" to "on, wifi three bars", "wifi three bars" can be put
+ * into the event text.
+ */
+ public static final int CONTENT_CHANGE_TYPE_STATE_DESCRIPTION = 0x00000040;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window was added.
+ */
+ public static final int WINDOWS_CHANGE_ADDED = 0x00000001;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * A window was removed.
+ */
+ public static final int WINDOWS_CHANGE_REMOVED = 0x00000002;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's title changed.
+ */
+ public static final int WINDOWS_CHANGE_TITLE = 0x00000004;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's bounds changed.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#R R}, this event implies the window's
+ * region changed. It's also possible that region changed but bounds doesn't.
+ * </p>
+ */
+ public static final int WINDOWS_CHANGE_BOUNDS = 0x00000008;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's layer changed.
+ */
+ public static final int WINDOWS_CHANGE_LAYER = 0x00000010;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isActive()} changed.
+ */
+ public static final int WINDOWS_CHANGE_ACTIVE = 0x00000020;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isFocused()} changed.
+ */
+ public static final int WINDOWS_CHANGE_FOCUSED = 0x00000040;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isAccessibilityFocused()} changed.
+ */
+ public static final int WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED = 0x00000080;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's parent changed.
+ */
+ public static final int WINDOWS_CHANGE_PARENT = 0x00000100;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's children changed.
+ */
+ public static final int WINDOWS_CHANGE_CHILDREN = 0x00000200;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window either entered or exited picture-in-picture mode.
+ */
+ public static final int WINDOWS_CHANGE_PIP = 0x00000400;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "WINDOWS_CHANGE_" }, value = {
+ WINDOWS_CHANGE_ADDED,
+ WINDOWS_CHANGE_REMOVED,
+ WINDOWS_CHANGE_TITLE,
+ WINDOWS_CHANGE_BOUNDS,
+ WINDOWS_CHANGE_LAYER,
+ WINDOWS_CHANGE_ACTIVE,
+ WINDOWS_CHANGE_FOCUSED,
+ WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED,
+ WINDOWS_CHANGE_PARENT,
+ WINDOWS_CHANGE_CHILDREN,
+ WINDOWS_CHANGE_PIP
+ })
+ public @interface WindowsChangeTypes {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "CONTENT_CHANGE_TYPE_" },
+ value = {
+ CONTENT_CHANGE_TYPE_UNDEFINED,
+ CONTENT_CHANGE_TYPE_SUBTREE,
+ CONTENT_CHANGE_TYPE_TEXT,
+ CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION,
+ CONTENT_CHANGE_TYPE_STATE_DESCRIPTION,
+ CONTENT_CHANGE_TYPE_PANE_TITLE,
+ CONTENT_CHANGE_TYPE_PANE_APPEARED,
+ CONTENT_CHANGE_TYPE_PANE_DISAPPEARED
+ })
+ public @interface ContentChangeTypes {}
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+ TYPE_VIEW_CLICKED,
+ TYPE_VIEW_LONG_CLICKED,
+ TYPE_VIEW_SELECTED,
+ TYPE_VIEW_FOCUSED,
+ TYPE_VIEW_TEXT_CHANGED,
+ TYPE_WINDOW_STATE_CHANGED,
+ TYPE_NOTIFICATION_STATE_CHANGED,
+ TYPE_VIEW_HOVER_ENTER,
+ TYPE_VIEW_HOVER_EXIT,
+ TYPE_TOUCH_EXPLORATION_GESTURE_START,
+ TYPE_TOUCH_EXPLORATION_GESTURE_END,
+ TYPE_WINDOW_CONTENT_CHANGED,
+ TYPE_VIEW_SCROLLED,
+ TYPE_VIEW_TEXT_SELECTION_CHANGED,
+ TYPE_ANNOUNCEMENT,
+ TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+ TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
+ TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+ TYPE_GESTURE_DETECTION_START,
+ TYPE_GESTURE_DETECTION_END,
+ TYPE_TOUCH_INTERACTION_START,
+ TYPE_TOUCH_INTERACTION_END,
+ TYPE_WINDOWS_CHANGED,
+ TYPE_VIEW_CONTEXT_CLICKED,
+ TYPE_ASSIST_READING_CONTEXT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EventType {}
+
+ /**
+ * Mask for {@link AccessibilityEvent} all types.
+ *
+ * @see #TYPE_VIEW_CLICKED
+ * @see #TYPE_VIEW_LONG_CLICKED
+ * @see #TYPE_VIEW_SELECTED
+ * @see #TYPE_VIEW_FOCUSED
+ * @see #TYPE_VIEW_TEXT_CHANGED
+ * @see #TYPE_WINDOW_STATE_CHANGED
+ * @see #TYPE_NOTIFICATION_STATE_CHANGED
+ * @see #TYPE_VIEW_HOVER_ENTER
+ * @see #TYPE_VIEW_HOVER_EXIT
+ * @see #TYPE_TOUCH_EXPLORATION_GESTURE_START
+ * @see #TYPE_TOUCH_EXPLORATION_GESTURE_END
+ * @see #TYPE_WINDOW_CONTENT_CHANGED
+ * @see #TYPE_VIEW_SCROLLED
+ * @see #TYPE_VIEW_TEXT_SELECTION_CHANGED
+ * @see #TYPE_ANNOUNCEMENT
+ * @see #TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY
+ * @see #TYPE_GESTURE_DETECTION_START
+ * @see #TYPE_GESTURE_DETECTION_END
+ * @see #TYPE_TOUCH_INTERACTION_START
+ * @see #TYPE_TOUCH_INTERACTION_END
+ * @see #TYPE_WINDOWS_CHANGED
+ * @see #TYPE_VIEW_CONTEXT_CLICKED
+ */
+ public static final int TYPES_ALL_MASK = 0xFFFFFFFF;
+
+ private static final int MAX_POOL_SIZE = 10;
+ private static final SynchronizedPool<AccessibilityEvent> sPool =
+ new SynchronizedPool<>(MAX_POOL_SIZE);
+
+ @UnsupportedAppUsage
+ private @EventType int mEventType;
+ private CharSequence mPackageName;
+ private long mEventTime;
+ int mMovementGranularity;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ int mAction;
+ int mContentChangeTypes;
+ int mWindowChangeTypes;
+
+ /**
+ * The stack trace describing where this event originated from on the app side.
+ * Only populated if {@link #DEBUG_ORIGIN} is enabled
+ * Can be inspected(e.g. printed) from an
+ * {@link android.accessibilityservice.AccessibilityService} to trace where particular events
+ * are being dispatched from.
+ *
+ * @hide
+ */
+ public StackTraceElement[] originStackTrace = null;
+
+ private ArrayList<AccessibilityRecord> mRecords;
+
+ /**
+ * Creates a new {@link AccessibilityEvent}.
+ */
+ public AccessibilityEvent() {
+ if (DEBUG_ORIGIN) originStackTrace = Thread.currentThread().getStackTrace();
+ }
+
+
+ /**
+ * Creates a new {@link AccessibilityEvent} with the given <code>eventType</code>.
+ *
+ * @param eventType The event type.
+ */
+ public AccessibilityEvent(int eventType) {
+ mEventType = eventType;
+ if (DEBUG_ORIGIN) originStackTrace = Thread.currentThread().getStackTrace();
+ }
+
+ /**
+ * Copy constructor. Creates a new {@link AccessibilityEvent}, and this instance is initialized
+ * from the given <code>event</code>.
+ *
+ * @param event The other event.
+ */
+ public AccessibilityEvent(@NonNull AccessibilityEvent event) {
+ init(event);
+ }
+
+ /**
+ * Initialize an event from another one.
+ *
+ * @param event The event to initialize from.
+ */
+ void init(AccessibilityEvent event) {
+ super.init(event);
+ mEventType = event.mEventType;
+ mMovementGranularity = event.mMovementGranularity;
+ mAction = event.mAction;
+ mContentChangeTypes = event.mContentChangeTypes;
+ mWindowChangeTypes = event.mWindowChangeTypes;
+ mEventTime = event.mEventTime;
+ mPackageName = event.mPackageName;
+ if (event.mRecords != null) {
+ final int recordCount = event.mRecords.size();
+ mRecords = new ArrayList<>(recordCount);
+ for (int i = 0; i < recordCount; i++) {
+ final AccessibilityRecord record = event.mRecords.get(i);
+ final AccessibilityRecord recordClone = new AccessibilityRecord(record);
+ mRecords.add(recordClone);
+ }
+ }
+ if (DEBUG_ORIGIN) originStackTrace = event.originStackTrace;
+ }
+
+ /**
+ * Sets if this instance is sealed.
+ *
+ * @param sealed Whether is sealed.
+ *
+ * @hide
+ */
+ @Override
+ public void setSealed(boolean sealed) {
+ super.setSealed(sealed);
+ final List<AccessibilityRecord> records = mRecords;
+ if (records != null) {
+ final int recordCount = records.size();
+ for (int i = 0; i < recordCount; i++) {
+ AccessibilityRecord record = records.get(i);
+ record.setSealed(sealed);
+ }
+ }
+ }
+
+ /**
+ * Gets the number of records contained in the event.
+ *
+ * @return The number of records.
+ */
+ public int getRecordCount() {
+ return mRecords == null ? 0 : mRecords.size();
+ }
+
+ /**
+ * Appends an {@link AccessibilityRecord} to the end of event records.
+ *
+ * @param record The record to append.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void appendRecord(AccessibilityRecord record) {
+ enforceNotSealed();
+ if (mRecords == null) {
+ mRecords = new ArrayList<AccessibilityRecord>();
+ }
+ mRecords.add(record);
+ }
+
+ /**
+ * Gets the record at a given index.
+ *
+ * @param index The index.
+ * @return The record at the specified index.
+ */
+ public AccessibilityRecord getRecord(int index) {
+ if (mRecords == null) {
+ throw new IndexOutOfBoundsException("Invalid index " + index + ", size is 0");
+ }
+ return mRecords.get(index);
+ }
+
+ /**
+ * Gets the event type.
+ *
+ * @return The event type.
+ */
+ public @EventType int getEventType() {
+ return mEventType;
+ }
+
+ /**
+ * Gets the bit mask of change types signaled by a
+ * {@link #TYPE_WINDOW_CONTENT_CHANGED} event or {@link #TYPE_WINDOW_STATE_CHANGED}. A single
+ * event may represent multiple change types.
+ *
+ * @return The bit mask of change types. One or more of:
+ * <ul>
+ * <li>{@link #CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
+ * <li>{@link #CONTENT_CHANGE_TYPE_STATE_DESCRIPTION}
+ * <li>{@link #CONTENT_CHANGE_TYPE_SUBTREE}
+ * <li>{@link #CONTENT_CHANGE_TYPE_TEXT}
+ * <li>{@link #CONTENT_CHANGE_TYPE_PANE_TITLE}
+ * <li>{@link #CONTENT_CHANGE_TYPE_UNDEFINED}
+ * <li>{@link #CONTENT_CHANGE_TYPE_PANE_APPEARED}
+ * <li>{@link #CONTENT_CHANGE_TYPE_PANE_DISAPPEARED}
+ * </ul>
+ */
+ @ContentChangeTypes
+ public int getContentChangeTypes() {
+ return mContentChangeTypes;
+ }
+
+ private static String contentChangeTypesToString(int types) {
+ return BitUtils.flagsToString(types, AccessibilityEvent::singleContentChangeTypeToString);
+ }
+
+ private static String singleContentChangeTypeToString(int type) {
+ switch (type) {
+ case CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION:
+ return "CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION";
+ case CONTENT_CHANGE_TYPE_STATE_DESCRIPTION:
+ return "CONTENT_CHANGE_TYPE_STATE_DESCRIPTION";
+ case CONTENT_CHANGE_TYPE_SUBTREE: return "CONTENT_CHANGE_TYPE_SUBTREE";
+ case CONTENT_CHANGE_TYPE_TEXT: return "CONTENT_CHANGE_TYPE_TEXT";
+ case CONTENT_CHANGE_TYPE_PANE_TITLE: return "CONTENT_CHANGE_TYPE_PANE_TITLE";
+ case CONTENT_CHANGE_TYPE_UNDEFINED: return "CONTENT_CHANGE_TYPE_UNDEFINED";
+ case CONTENT_CHANGE_TYPE_PANE_APPEARED: return "CONTENT_CHANGE_TYPE_PANE_APPEARED";
+ case CONTENT_CHANGE_TYPE_PANE_DISAPPEARED:
+ return "CONTENT_CHANGE_TYPE_PANE_DISAPPEARED";
+ default: return Integer.toHexString(type);
+ }
+ }
+
+ /**
+ * Sets the bit mask of node tree changes signaled by an
+ * {@link #TYPE_WINDOW_CONTENT_CHANGED} event.
+ *
+ * @param changeTypes The bit mask of change types.
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @see #getContentChangeTypes()
+ */
+ public void setContentChangeTypes(@ContentChangeTypes int changeTypes) {
+ enforceNotSealed();
+ mContentChangeTypes = changeTypes;
+ }
+
+ /**
+ * Get the bit mask of change types signaled by a {@link #TYPE_WINDOWS_CHANGED} event. A
+ * single event may represent multiple change types.
+ *
+ * @return The bit mask of change types.
+ */
+ @WindowsChangeTypes
+ public int getWindowChanges() {
+ return mWindowChangeTypes;
+ }
+
+ /** @hide */
+ public void setWindowChanges(@WindowsChangeTypes int changes) {
+ mWindowChangeTypes = changes;
+ }
+
+ private static String windowChangeTypesToString(@WindowsChangeTypes int types) {
+ return BitUtils.flagsToString(types, AccessibilityEvent::singleWindowChangeTypeToString);
+ }
+
+ private static String singleWindowChangeTypeToString(int type) {
+ switch (type) {
+ case WINDOWS_CHANGE_ADDED: return "WINDOWS_CHANGE_ADDED";
+ case WINDOWS_CHANGE_REMOVED: return "WINDOWS_CHANGE_REMOVED";
+ case WINDOWS_CHANGE_TITLE: return "WINDOWS_CHANGE_TITLE";
+ case WINDOWS_CHANGE_BOUNDS: return "WINDOWS_CHANGE_BOUNDS";
+ case WINDOWS_CHANGE_LAYER: return "WINDOWS_CHANGE_LAYER";
+ case WINDOWS_CHANGE_ACTIVE: return "WINDOWS_CHANGE_ACTIVE";
+ case WINDOWS_CHANGE_FOCUSED: return "WINDOWS_CHANGE_FOCUSED";
+ case WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED:
+ return "WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED";
+ case WINDOWS_CHANGE_PARENT: return "WINDOWS_CHANGE_PARENT";
+ case WINDOWS_CHANGE_CHILDREN: return "WINDOWS_CHANGE_CHILDREN";
+ case WINDOWS_CHANGE_PIP: return "WINDOWS_CHANGE_PIP";
+ default: return Integer.toHexString(type);
+ }
+ }
+
+ /**
+ * Sets the event type.
+ *
+ * @param eventType The event type.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setEventType(@EventType int eventType) {
+ enforceNotSealed();
+ mEventType = eventType;
+ }
+
+ /**
+ * Gets the time in which this event was sent.
+ *
+ * @return The event time.
+ */
+ public long getEventTime() {
+ return mEventTime;
+ }
+
+ /**
+ * Sets the time in which this event was sent.
+ *
+ * @param eventTime The event time.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setEventTime(long eventTime) {
+ enforceNotSealed();
+ mEventTime = eventTime;
+ }
+
+ /**
+ * Gets the package name of the source.
+ *
+ * @return The package name.
+ */
+ public CharSequence getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Sets the package name of the source.
+ *
+ * @param packageName The package name.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setPackageName(CharSequence packageName) {
+ enforceNotSealed();
+ mPackageName = packageName;
+ }
+
+ /**
+ * Sets the movement granularity that was traversed.
+ *
+ * @param granularity The granularity.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setMovementGranularity(int granularity) {
+ enforceNotSealed();
+ mMovementGranularity = granularity;
+ }
+
+ /**
+ * Gets the movement granularity that was traversed.
+ *
+ * @return The granularity.
+ */
+ public int getMovementGranularity() {
+ return mMovementGranularity;
+ }
+
+ /**
+ * Sets the performed action that triggered this event.
+ * <p>
+ * Valid actions are defined in {@link AccessibilityNodeInfo}:
+ * <ul>
+ * <li>{@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS}
+ * <li>{@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS}
+ * <li>{@link AccessibilityNodeInfo#ACTION_CLEAR_FOCUS}
+ * <li>{@link AccessibilityNodeInfo#ACTION_CLEAR_SELECTION}
+ * <li>{@link AccessibilityNodeInfo#ACTION_CLICK}
+ * <li>etc.
+ * </ul>
+ *
+ * @param action The action.
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @see AccessibilityNodeInfo#performAction(int)
+ */
+ public void setAction(int action) {
+ enforceNotSealed();
+ mAction = action;
+ }
+
+ /**
+ * Gets the performed action that triggered this event.
+ *
+ * @return The action.
+ */
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Convenience method to obtain a {@link #TYPE_WINDOWS_CHANGED} event for a specific window and
+ * change set.
+ *
+ * @param windowId The ID of the window that changed
+ * @param windowChangeTypes The changes to populate
+ * @return An instance of a TYPE_WINDOWS_CHANGED, populated with the requested fields and with
+ * importantForAccessibility set to {@code true}.
+ *
+ * @hide
+ */
+ public static AccessibilityEvent obtainWindowsChangedEvent(
+ int windowId, int windowChangeTypes) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(TYPE_WINDOWS_CHANGED);
+ event.setWindowId(windowId);
+ event.setWindowChanges(windowChangeTypes);
+ event.setImportantForAccessibility(true);
+ return event;
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * instantiated with its type property set.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityEvent(int)} instead.
+ *
+ * @param eventType The event type.
+ * @return An instance.
+ */
+ public static AccessibilityEvent obtain(int eventType) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(eventType);
+ return event;
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * created. The returned instance is initialized from the given
+ * <code>event</code>.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityEvent(AccessibilityEvent)} instead.
+ *
+ * @param event The other event.
+ * @return An instance.
+ */
+ public static AccessibilityEvent obtain(AccessibilityEvent event) {
+ AccessibilityEvent eventClone = AccessibilityEvent.obtain();
+ eventClone.init(event);
+ return eventClone;
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * instantiated.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityEvent()} instead.
+ *
+ * @return An instance.
+ */
+ public static AccessibilityEvent obtain() {
+ AccessibilityEvent event = sPool.acquire();
+ if (event == null) event = new AccessibilityEvent();
+ if (DEBUG_ORIGIN) event.originStackTrace = Thread.currentThread().getStackTrace();
+ return event;
+ }
+
+ /**
+ * Recycles an instance back to be reused.
+ * <p>
+ * <b>Note: You must not touch the object after calling this function.</b>
+ * </p>
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ *
+ * @throws IllegalStateException If the event is already recycled.
+ */
+ @Override
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ /**
+ * Clears the state of this instance.
+ *
+ * @hide
+ */
+ @Override
+ protected void clear() {
+ super.clear();
+ mEventType = 0;
+ mMovementGranularity = 0;
+ mAction = 0;
+ mContentChangeTypes = 0;
+ mWindowChangeTypes = 0;
+ mPackageName = null;
+ mEventTime = 0;
+ if (mRecords != null) {
+ while (!mRecords.isEmpty()) {
+ AccessibilityRecord record = mRecords.remove(0);
+ record.recycle();
+ }
+ }
+ if (DEBUG_ORIGIN) originStackTrace = null;
+ }
+
+ /**
+ * Creates a new instance from a {@link Parcel}.
+ *
+ * @param parcel A parcel containing the state of a {@link AccessibilityEvent}.
+ */
+ public void initFromParcel(Parcel parcel) {
+ mSealed = (parcel.readInt() == 1);
+ mEventType = parcel.readInt();
+ mMovementGranularity = parcel.readInt();
+ mAction = parcel.readInt();
+ mContentChangeTypes = parcel.readInt();
+ mWindowChangeTypes = parcel.readInt();
+ mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ mEventTime = parcel.readLong();
+ mConnectionId = parcel.readInt();
+ readAccessibilityRecordFromParcel(this, parcel);
+
+ // Read the records.
+ final int recordCount = parcel.readInt();
+ if (recordCount > 0) {
+ mRecords = new ArrayList<>(recordCount);
+ for (int i = 0; i < recordCount; i++) {
+ AccessibilityRecord record = AccessibilityRecord.obtain();
+ readAccessibilityRecordFromParcel(record, parcel);
+ record.mConnectionId = mConnectionId;
+ mRecords.add(record);
+ }
+ }
+
+ if (DEBUG_ORIGIN) {
+ originStackTrace = new StackTraceElement[parcel.readInt()];
+ for (int i = 0; i < originStackTrace.length; i++) {
+ originStackTrace[i] = new StackTraceElement(
+ parcel.readString(),
+ parcel.readString(),
+ parcel.readString(),
+ parcel.readInt());
+ }
+ }
+ }
+
+ /**
+ * Reads an {@link AccessibilityRecord} from a parcel.
+ *
+ * @param record The record to initialize.
+ * @param parcel The parcel to read from.
+ */
+ private void readAccessibilityRecordFromParcel(AccessibilityRecord record,
+ Parcel parcel) {
+ record.mBooleanProperties = parcel.readInt();
+ record.mCurrentItemIndex = parcel.readInt();
+ record.mItemCount = parcel.readInt();
+ record.mFromIndex = parcel.readInt();
+ record.mToIndex = parcel.readInt();
+ record.mScrollX = parcel.readInt();
+ record.mScrollY = parcel.readInt();
+ record.mScrollDeltaX = parcel.readInt();
+ record.mScrollDeltaY = parcel.readInt();
+ record.mMaxScrollX = parcel.readInt();
+ record.mMaxScrollY = parcel.readInt();
+ record.mAddedCount = parcel.readInt();
+ record.mRemovedCount = parcel.readInt();
+ record.mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ record.mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ record.mBeforeText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ record.mParcelableData = parcel.readParcelable(null);
+ parcel.readList(record.mText, null);
+ record.mSourceWindowId = parcel.readInt();
+ record.mSourceNodeId = parcel.readLong();
+ record.mSealed = (parcel.readInt() == 1);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(isSealed() ? 1 : 0);
+ parcel.writeInt(mEventType);
+ parcel.writeInt(mMovementGranularity);
+ parcel.writeInt(mAction);
+ parcel.writeInt(mContentChangeTypes);
+ parcel.writeInt(mWindowChangeTypes);
+ TextUtils.writeToParcel(mPackageName, parcel, 0);
+ parcel.writeLong(mEventTime);
+ parcel.writeInt(mConnectionId);
+ writeAccessibilityRecordToParcel(this, parcel, flags);
+
+ // Write the records.
+ final int recordCount = getRecordCount();
+ parcel.writeInt(recordCount);
+ for (int i = 0; i < recordCount; i++) {
+ AccessibilityRecord record = mRecords.get(i);
+ writeAccessibilityRecordToParcel(record, parcel, flags);
+ }
+
+ if (DEBUG_ORIGIN) {
+ if (originStackTrace == null) originStackTrace = Thread.currentThread().getStackTrace();
+ parcel.writeInt(originStackTrace.length);
+ for (StackTraceElement element : originStackTrace) {
+ parcel.writeString(element.getClassName());
+ parcel.writeString(element.getMethodName());
+ parcel.writeString(element.getFileName());
+ parcel.writeInt(element.getLineNumber());
+ }
+ }
+ }
+
+ /**
+ * Writes an {@link AccessibilityRecord} to a parcel.
+ *
+ * @param record The record to write.
+ * @param parcel The parcel to which to write.
+ */
+ private void writeAccessibilityRecordToParcel(AccessibilityRecord record, Parcel parcel,
+ int flags) {
+ parcel.writeInt(record.mBooleanProperties);
+ parcel.writeInt(record.mCurrentItemIndex);
+ parcel.writeInt(record.mItemCount);
+ parcel.writeInt(record.mFromIndex);
+ parcel.writeInt(record.mToIndex);
+ parcel.writeInt(record.mScrollX);
+ parcel.writeInt(record.mScrollY);
+ parcel.writeInt(record.mScrollDeltaX);
+ parcel.writeInt(record.mScrollDeltaY);
+ parcel.writeInt(record.mMaxScrollX);
+ parcel.writeInt(record.mMaxScrollY);
+ parcel.writeInt(record.mAddedCount);
+ parcel.writeInt(record.mRemovedCount);
+ TextUtils.writeToParcel(record.mClassName, parcel, flags);
+ TextUtils.writeToParcel(record.mContentDescription, parcel, flags);
+ TextUtils.writeToParcel(record.mBeforeText, parcel, flags);
+ parcel.writeParcelable(record.mParcelableData, flags);
+ parcel.writeList(record.mText);
+ parcel.writeInt(record.mSourceWindowId);
+ parcel.writeLong(record.mSourceNodeId);
+ parcel.writeInt(record.mSealed ? 1 : 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("EventType: ").append(eventTypeToString(mEventType));
+ builder.append("; EventTime: ").append(mEventTime);
+ builder.append("; PackageName: ").append(mPackageName);
+ if (!DEBUG_CONCISE_TOSTRING || mMovementGranularity != 0) {
+ builder.append("; MovementGranularity: ").append(mMovementGranularity);
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mAction != 0) {
+ builder.append("; Action: ").append(mAction);
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mContentChangeTypes != 0) {
+ builder.append("; ContentChangeTypes: ").append(
+ contentChangeTypesToString(mContentChangeTypes));
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) {
+ builder.append("; WindowChangeTypes: ").append(
+ windowChangeTypesToString(mWindowChangeTypes));
+ }
+ super.appendTo(builder);
+ if (DEBUG || DEBUG_CONCISE_TOSTRING) {
+ if (!DEBUG_CONCISE_TOSTRING) {
+ builder.append("\n");
+ }
+ if (DEBUG) {
+ builder.append("; SourceWindowId: 0x").append(Long.toHexString(mSourceWindowId));
+ builder.append("; SourceNodeId: 0x").append(Long.toHexString(mSourceNodeId));
+ }
+ for (int i = 0; i < getRecordCount(); i++) {
+ builder.append(" Record ").append(i).append(":");
+ getRecord(i).appendTo(builder).append("\n");
+ }
+ } else {
+ builder.append("; recordCount: ").append(getRecordCount());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Returns the string representation of an event type. For example,
+ * {@link #TYPE_VIEW_CLICKED} is represented by the string TYPE_VIEW_CLICKED.
+ *
+ * @param eventType The event type
+ * @return The string representation.
+ */
+ public static String eventTypeToString(int eventType) {
+ if (eventType == TYPES_ALL_MASK) {
+ return "TYPES_ALL_MASK";
+ }
+ StringBuilder builder = new StringBuilder();
+ int eventTypeCount = 0;
+ while (eventType != 0) {
+ final int eventTypeFlag = 1 << Integer.numberOfTrailingZeros(eventType);
+ eventType &= ~eventTypeFlag;
+
+ if (eventTypeCount > 0) {
+ builder.append(", ");
+ }
+ builder.append(singleEventTypeToString(eventTypeFlag));
+
+ eventTypeCount++;
+ }
+ if (eventTypeCount > 1) {
+ builder.insert(0, '[');
+ builder.append(']');
+ }
+ return builder.toString();
+ }
+
+ private static String singleEventTypeToString(int eventType) {
+ switch (eventType) {
+ case TYPE_VIEW_CLICKED: return "TYPE_VIEW_CLICKED";
+ case TYPE_VIEW_LONG_CLICKED: return "TYPE_VIEW_LONG_CLICKED";
+ case TYPE_VIEW_SELECTED: return "TYPE_VIEW_SELECTED";
+ case TYPE_VIEW_FOCUSED: return "TYPE_VIEW_FOCUSED";
+ case TYPE_VIEW_TEXT_CHANGED: return "TYPE_VIEW_TEXT_CHANGED";
+ case TYPE_WINDOW_STATE_CHANGED: return "TYPE_WINDOW_STATE_CHANGED";
+ case TYPE_VIEW_HOVER_ENTER: return "TYPE_VIEW_HOVER_ENTER";
+ case TYPE_VIEW_HOVER_EXIT: return "TYPE_VIEW_HOVER_EXIT";
+ case TYPE_NOTIFICATION_STATE_CHANGED: return "TYPE_NOTIFICATION_STATE_CHANGED";
+ case TYPE_TOUCH_EXPLORATION_GESTURE_START: {
+ return "TYPE_TOUCH_EXPLORATION_GESTURE_START";
+ }
+ case TYPE_TOUCH_EXPLORATION_GESTURE_END: return "TYPE_TOUCH_EXPLORATION_GESTURE_END";
+ case TYPE_WINDOW_CONTENT_CHANGED: return "TYPE_WINDOW_CONTENT_CHANGED";
+ case TYPE_VIEW_TEXT_SELECTION_CHANGED: return "TYPE_VIEW_TEXT_SELECTION_CHANGED";
+ case TYPE_VIEW_SCROLLED: return "TYPE_VIEW_SCROLLED";
+ case TYPE_ANNOUNCEMENT: return "TYPE_ANNOUNCEMENT";
+ case TYPE_VIEW_ACCESSIBILITY_FOCUSED: return "TYPE_VIEW_ACCESSIBILITY_FOCUSED";
+ case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
+ return "TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED";
+ }
+ case TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: {
+ return "TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY";
+ }
+ case TYPE_GESTURE_DETECTION_START: return "TYPE_GESTURE_DETECTION_START";
+ case TYPE_GESTURE_DETECTION_END: return "TYPE_GESTURE_DETECTION_END";
+ case TYPE_TOUCH_INTERACTION_START: return "TYPE_TOUCH_INTERACTION_START";
+ case TYPE_TOUCH_INTERACTION_END: return "TYPE_TOUCH_INTERACTION_END";
+ case TYPE_WINDOWS_CHANGED: return "TYPE_WINDOWS_CHANGED";
+ case TYPE_VIEW_CONTEXT_CLICKED: return "TYPE_VIEW_CONTEXT_CLICKED";
+ case TYPE_ASSIST_READING_CONTEXT: return "TYPE_ASSIST_READING_CONTEXT";
+ default: return Integer.toHexString(eventType);
+ }
+ }
+
+ /**
+ * @see Parcelable.Creator
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<AccessibilityEvent> CREATOR =
+ new Parcelable.Creator<AccessibilityEvent>() {
+ public AccessibilityEvent createFromParcel(Parcel parcel) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.initFromParcel(parcel);
+ return event;
+ }
+
+ public AccessibilityEvent[] newArray(int size) {
+ return new AccessibilityEvent[size];
+ }
+ };
+}
diff --git a/android/view/accessibility/AccessibilityEventSource.java b/android/view/accessibility/AccessibilityEventSource.java
new file mode 100644
index 0000000..525ba9e
--- /dev/null
+++ b/android/view/accessibility/AccessibilityEventSource.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+/**
+ * This interface is implemented by classes source of {@link AccessibilityEvent}s.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about making applications accessible, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ */
+public interface AccessibilityEventSource {
+
+ /**
+ * Handles the request for sending an {@link AccessibilityEvent} given
+ * the event type. The method must first check if accessibility is on
+ * via calling {@link AccessibilityManager#isEnabled() AccessibilityManager.isEnabled()},
+ * obtain an {@link AccessibilityEvent} from the event pool through calling
+ * {@link AccessibilityEvent#obtain(int) AccessibilityEvent.obtain(int)}, populate the
+ * event, and send it for dispatch via calling
+ * {@link AccessibilityManager#sendAccessibilityEvent(AccessibilityEvent)
+ * AccessibilityManager.sendAccessibilityEvent(AccessibilityEvent)}.
+ *
+ * @see AccessibilityEvent
+ * @see AccessibilityManager
+ *
+ * @param eventType The event type.
+ */
+ public void sendAccessibilityEvent(int eventType);
+
+ /**
+ * Handles the request for sending an {@link AccessibilityEvent}. The
+ * method does not guarantee to check if accessibility is on before
+ * sending the event for dispatch. It is responsibility of the caller
+ * to do the check via calling {@link AccessibilityManager#isEnabled()
+ * AccessibilityManager.isEnabled()}.
+ *
+ * @see AccessibilityEvent
+ * @see AccessibilityManager
+ *
+ * @param event The event.
+ */
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event);
+}
diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java
new file mode 100644
index 0000000..dd81dd9
--- /dev/null
+++ b/android/view/accessibility/AccessibilityInteractionClient.java
@@ -0,0 +1,1245 @@
+/*
+ ** Copyright 2011, The Android Open Source Project
+ **
+ ** Licensed under the Apache License, Version 2.0 (the "License");
+ ** you may not use this file except in compliance with the License.
+ ** You may obtain a copy of the License at
+ **
+ ** http://www.apache.org/licenses/LICENSE-2.0
+ **
+ ** Unless required by applicable law or agreed to in writing, software
+ ** distributed under the License is distributed on an "AS IS" BASIS,
+ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ** See the License for the specific language governing permissions and
+ ** limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+import android.util.SparseLongArray;
+import android.view.Display;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This class is a singleton that performs accessibility interaction
+ * which is it queries remote view hierarchies about snapshots of their
+ * views as well requests from these hierarchies to perform certain
+ * actions on their views.
+ *
+ * Rationale: The content retrieval APIs are synchronous from a client's
+ * perspective but internally they are asynchronous. The client thread
+ * calls into the system requesting an action and providing a callback
+ * to receive the result after which it waits up to a timeout for that
+ * result. The system enforces security and the delegates the request
+ * to a given view hierarchy where a message is posted (from a binder
+ * thread) describing what to be performed by the main UI thread the
+ * result of which it delivered via the mentioned callback. However,
+ * the blocked client thread and the main UI thread of the target view
+ * hierarchy can be the same thread, for example an accessibility service
+ * and an activity run in the same process, thus they are executed on the
+ * same main thread. In such a case the retrieval will fail since the UI
+ * thread that has to process the message describing the work to be done
+ * is blocked waiting for a result is has to compute! To avoid this scenario
+ * when making a call the client also passes its process and thread ids so
+ * the accessed view hierarchy can detect if the client making the request
+ * is running in its main UI thread. In such a case the view hierarchy,
+ * specifically the binder thread performing the IPC to it, does not post a
+ * message to be run on the UI thread but passes it to the singleton
+ * interaction client through which all interactions occur and the latter is
+ * responsible to execute the message before starting to wait for the
+ * asynchronous result delivered via the callback. In this case the expected
+ * result is already received so no waiting is performed.
+ *
+ * @hide
+ */
+public final class AccessibilityInteractionClient
+ extends IAccessibilityInteractionConnectionCallback.Stub {
+
+ public static final int NO_ID = -1;
+
+ public static final String CALL_STACK = "call_stack";
+
+ private static final String LOG_TAG = "AccessibilityInteractionClient";
+
+ private static final boolean DEBUG = false;
+
+ private static final boolean CHECK_INTEGRITY = true;
+
+ private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
+
+ private static final long DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS =
+ (long) (ViewConfiguration.getSendRecurringAccessibilityEventsInterval() * 1.5);
+
+ private static final Object sStaticLock = new Object();
+
+ private static final LongSparseArray<AccessibilityInteractionClient> sClients =
+ new LongSparseArray<>();
+
+ private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
+ new SparseArray<>();
+
+ /** List of timestamps which indicate the latest time an a11y service receives a scroll event
+ from a window, mapping from windowId -> timestamp. */
+ private static final SparseLongArray sScrollingWindows = new SparseLongArray();
+
+ private static AccessibilityCache sAccessibilityCache =
+ new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher());
+
+ private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
+
+ private final Object mInstanceLock = new Object();
+
+ private final AccessibilityManager mAccessibilityManager;
+
+ private volatile int mInteractionId = -1;
+ private volatile int mCallingUid = Process.INVALID_UID;
+
+ private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
+
+ private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult;
+
+ private boolean mPerformAccessibilityActionResult;
+
+ private Message mSameThreadMessage;
+
+ private int mInteractionIdWaitingForPrefetchResult = -1;
+ private int mConnectionIdWaitingForPrefetchResult;
+ private String[] mPackageNamesForNextPrefetchResult;
+
+ /**
+ * @return The client for the current thread.
+ */
+ @UnsupportedAppUsage()
+ public static AccessibilityInteractionClient getInstance() {
+ final long threadId = Thread.currentThread().getId();
+ return getInstanceForThread(threadId);
+ }
+
+ /**
+ * <strong>Note:</strong> We keep one instance per interrogating thread since
+ * the instance contains state which can lead to undesired thread interleavings.
+ * We do not have a thread local variable since other threads should be able to
+ * look up the correct client knowing a thread id. See ViewRootImpl for details.
+ *
+ * @return The client for a given <code>threadId</code>.
+ */
+ public static AccessibilityInteractionClient getInstanceForThread(long threadId) {
+ synchronized (sStaticLock) {
+ AccessibilityInteractionClient client = sClients.get(threadId);
+ if (client == null) {
+ client = new AccessibilityInteractionClient();
+ sClients.put(threadId, client);
+ }
+ return client;
+ }
+ }
+
+ /**
+ * @return The client for the current thread.
+ */
+ public static AccessibilityInteractionClient getInstance(Context context) {
+ final long threadId = Thread.currentThread().getId();
+ if (context != null) {
+ return getInstanceForThread(threadId, context);
+ }
+ return getInstanceForThread(threadId);
+ }
+
+ /**
+ * <strong>Note:</strong> We keep one instance per interrogating thread since
+ * the instance contains state which can lead to undesired thread interleavings.
+ * We do not have a thread local variable since other threads should be able to
+ * look up the correct client knowing a thread id. See ViewRootImpl for details.
+ *
+ * @return The client for a given <code>threadId</code>.
+ */
+ public static AccessibilityInteractionClient getInstanceForThread(
+ long threadId, Context context) {
+ synchronized (sStaticLock) {
+ AccessibilityInteractionClient client = sClients.get(threadId);
+ if (client == null) {
+ client = new AccessibilityInteractionClient(context);
+ sClients.put(threadId, client);
+ }
+ return client;
+ }
+ }
+
+ /**
+ * Gets a cached accessibility service connection.
+ *
+ * @param connectionId The connection id.
+ * @return The cached connection if such.
+ */
+ public static IAccessibilityServiceConnection getConnection(int connectionId) {
+ synchronized (sConnectionCache) {
+ return sConnectionCache.get(connectionId);
+ }
+ }
+
+ /**
+ * Adds a cached accessibility service connection.
+ *
+ * @param connectionId The connection id.
+ * @param connection The connection.
+ */
+ public static void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
+ synchronized (sConnectionCache) {
+ sConnectionCache.put(connectionId, connection);
+ }
+ }
+
+ /**
+ * Removes a cached accessibility service connection.
+ *
+ * @param connectionId The connection id.
+ */
+ public static void removeConnection(int connectionId) {
+ synchronized (sConnectionCache) {
+ sConnectionCache.remove(connectionId);
+ }
+ }
+
+ /**
+ * This method is only for testing. Replacing the cache is a generally terrible idea, but
+ * tests need to be able to verify this class's interactions with the cache
+ */
+ @VisibleForTesting
+ public static void setCache(AccessibilityCache cache) {
+ sAccessibilityCache = cache;
+ }
+
+ private AccessibilityInteractionClient() {
+ /* reducing constructor visibility */
+ mAccessibilityManager = null;
+ }
+
+ private AccessibilityInteractionClient(Context context) {
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ }
+
+ /**
+ * Sets the message to be processed if the interacted view hierarchy
+ * and the interacting client are running in the same thread.
+ *
+ * @param message The message.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void setSameThreadMessage(Message message) {
+ synchronized (mInstanceLock) {
+ mSameThreadMessage = message;
+ mInstanceLock.notifyAll();
+ }
+ }
+
+ /**
+ * Gets the root {@link AccessibilityNodeInfo} in the currently active window.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
+ */
+ public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
+ return findAccessibilityNodeInfoByAccessibilityId(connectionId,
+ AccessibilityWindowInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
+ false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
+ }
+
+ /**
+ * Gets the info for a window.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @return The {@link AccessibilityWindowInfo}.
+ */
+ public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId) {
+ return getWindow(connectionId, accessibilityWindowId, /* bypassCache */ false);
+ }
+
+ /**
+ * Gets the info for a window.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param bypassCache Whether to bypass the cache.
+ * @return The {@link AccessibilityWindowInfo}.
+ */
+ public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId,
+ boolean bypassCache) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ AccessibilityWindowInfo window;
+ if (!bypassCache) {
+ window = sAccessibilityCache.getWindow(accessibilityWindowId);
+ if (window != null) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Window cache hit");
+ }
+ return window;
+ }
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Window cache miss");
+ }
+ }
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ window = connection.getWindow(accessibilityWindowId);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ if (window != null) {
+ if (!bypassCache) {
+ sAccessibilityCache.addWindow(window);
+ }
+ return window;
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while calling remote getWindow", re);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the info for all windows of the default display.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @return The {@link AccessibilityWindowInfo} list.
+ */
+ public List<AccessibilityWindowInfo> getWindows(int connectionId) {
+ final SparseArray<List<AccessibilityWindowInfo>> windows =
+ getWindowsOnAllDisplays(connectionId);
+ if (windows.size() > 0) {
+ return windows.valueAt(Display.DEFAULT_DISPLAY);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Gets the info for all windows of all displays.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @return The SparseArray of {@link AccessibilityWindowInfo} list.
+ * The key of SparseArray is display ID.
+ */
+ public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays(int connectionId) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ SparseArray<List<AccessibilityWindowInfo>> windows =
+ sAccessibilityCache.getWindowsOnAllDisplays();
+ if (windows != null) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Windows cache hit");
+ }
+ return windows;
+ }
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Windows cache miss");
+ }
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ windows = connection.getWindows();
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ if (windows != null) {
+ sAccessibilityCache.setWindowsOnAllDisplays(windows);
+ return windows;
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while calling remote getWindowsOnAllDisplays", re);
+ }
+
+ final SparseArray<List<AccessibilityWindowInfo>> emptyWindows = new SparseArray<>();
+ return emptyWindows;
+ }
+
+
+ /**
+ * Finds an {@link AccessibilityNodeInfo} by accessibility id and given leash token instead of
+ * window id. This method is used to find the leashed node on the embedded view hierarchy.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param leashToken The token of the embedded hierarchy.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param bypassCache Whether to bypass the cache while looking for the node.
+ * @param prefetchFlags flags to guide prefetching.
+ * @param arguments Optional action arguments.
+ * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
+ */
+ public @Nullable AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(
+ int connectionId, @NonNull IBinder leashToken, long accessibilityNodeId,
+ boolean bypassCache, int prefetchFlags, Bundle arguments) {
+ if (leashToken == null) {
+ return null;
+ }
+ int windowId = -1;
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ windowId = connection.getWindowIdForLeashToken(leashToken);
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while calling remote getWindowIdForLeashToken", re);
+ }
+ if (windowId == -1) {
+ return null;
+ }
+ return findAccessibilityNodeInfoByAccessibilityId(connectionId, windowId,
+ accessibilityNodeId, bypassCache, prefetchFlags, arguments);
+ }
+
+ /**
+ * Finds an {@link AccessibilityNodeInfo} by accessibility id.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param bypassCache Whether to bypass the cache while looking for the node.
+ * @param prefetchFlags flags to guide prefetching.
+ * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
+ */
+ public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
+ int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
+ int prefetchFlags, Bundle arguments) {
+ if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0
+ && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) {
+ throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS"
+ + " requires FLAG_PREFETCH_PREDECESSORS");
+ }
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ if (!bypassCache) {
+ AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode(
+ accessibilityWindowId, accessibilityNodeId);
+ if (cachedInfo != null) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Node cache hit for "
+ + idToString(accessibilityWindowId, accessibilityNodeId));
+ }
+ return cachedInfo;
+ }
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Node cache miss for "
+ + idToString(accessibilityWindowId, accessibilityNodeId));
+ }
+ } else {
+ // No need to prefech nodes in bypass cache case.
+ prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
+ }
+ // Skip prefetching if window is scrolling.
+ if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
+ && isWindowScrolling(accessibilityWindowId)) {
+ prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
+ }
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final String[] packageNames;
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(
+ accessibilityWindowId, accessibilityNodeId, interactionId, this,
+ prefetchFlags, Thread.currentThread().getId(), arguments);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ if (packageNames != null) {
+ AccessibilityNodeInfo info =
+ getFindAccessibilityNodeInfoResultAndClear(interactionId);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(connection, "findAccessibilityNodeInfoByAccessibilityId",
+ "InteractionId:" + interactionId + ";Result: " + info
+ + ";connectionId=" + connectionId
+ + ";accessibilityWindowId="
+ + accessibilityWindowId + ";accessibilityNodeId="
+ + accessibilityNodeId + ";bypassCache=" + bypassCache
+ + ";prefetchFlags=" + prefetchFlags
+ + ";arguments=" + arguments);
+ }
+ if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
+ && info != null) {
+ setInteractionWaitingForPrefetchResult(interactionId, connectionId,
+ packageNames);
+ }
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
+ bypassCache, packageNames);
+ return info;
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while calling remote"
+ + " findAccessibilityNodeInfoByAccessibilityId", re);
+ }
+ return null;
+ }
+
+ private void setInteractionWaitingForPrefetchResult(int interactionId, int connectionId,
+ String[] packageNames) {
+ synchronized (mInstanceLock) {
+ mInteractionIdWaitingForPrefetchResult = interactionId;
+ mConnectionIdWaitingForPrefetchResult = connectionId;
+ mPackageNamesForNextPrefetchResult = packageNames;
+ }
+ }
+
+ private static String idToString(int accessibilityWindowId, long accessibilityNodeId) {
+ return accessibilityWindowId + "/"
+ + AccessibilityNodeInfo.idToString(accessibilityNodeId);
+ }
+
+ /**
+ * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
+ * the window whose id is specified and starts from the node whose accessibility
+ * id is specified.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param viewId The fully qualified resource name of the view id to find.
+ * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
+ */
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
+ int accessibilityWindowId, long accessibilityNodeId, String viewId) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final String[] packageNames;
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ packageNames = connection.findAccessibilityNodeInfosByViewId(
+ accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
+ Thread.currentThread().getId());
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ if (packageNames != null) {
+ List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
+ interactionId);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(connection, "findAccessibilityNodeInfosByViewId", "InteractionId="
+ + interactionId + ":Result: " + infos + ";connectionId="
+ + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
+ + ";accessibilityNodeId=" + accessibilityNodeId + ";viewId="
+ + viewId);
+ }
+ if (infos != null) {
+ finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
+ false, packageNames);
+ return infos;
+ }
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling remote"
+ + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
+ * insensitive containment. The search is performed in the window whose
+ * id is specified and starts from the node whose accessibility id is
+ * specified.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param text The searched text.
+ * @return A list of found {@link AccessibilityNodeInfo}s.
+ */
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
+ int accessibilityWindowId, long accessibilityNodeId, String text) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final String[] packageNames;
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ packageNames = connection.findAccessibilityNodeInfosByText(
+ accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
+ Thread.currentThread().getId());
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ if (packageNames != null) {
+ List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
+ interactionId);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(connection, "findAccessibilityNodeInfosByText", "InteractionId="
+ + interactionId + ":Result: " + infos + ";connectionId="
+ + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
+ + ";accessibilityNodeId=" + accessibilityNodeId + ";text=" + text);
+ }
+ if (infos != null) {
+ finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
+ false, packageNames);
+ return infos;
+ }
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling remote"
+ + " findAccessibilityNodeInfosByViewText", re);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
+ * specified focus type. The search is performed in the window whose id is specified
+ * and starts from the node whose accessibility id is specified.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param focusType The focus type.
+ * @return The accessibility focused {@link AccessibilityNodeInfo}.
+ */
+ public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
+ long accessibilityNodeId, int focusType) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final String[] packageNames;
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ packageNames = connection.findFocus(accessibilityWindowId,
+ accessibilityNodeId, focusType, interactionId, this,
+ Thread.currentThread().getId());
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ if (packageNames != null) {
+ AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
+ interactionId);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(connection, "findFocus", "InteractionId=" + interactionId
+ + ":Result: " + info + ";connectionId=" + connectionId
+ + ";accessibilityWindowId=" + accessibilityWindowId
+ + ";accessibilityNodeId=" + accessibilityNodeId + ";focusType="
+ + focusType);
+ }
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
+ return info;
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling remote findFocus", re);
+ }
+ return null;
+ }
+
+ /**
+ * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
+ * The search is performed in the window whose id is specified and starts from the
+ * node whose accessibility id is specified.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param direction The direction in which to search for focusable.
+ * @return The accessibility focused {@link AccessibilityNodeInfo}.
+ */
+ public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
+ long accessibilityNodeId, int direction) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final String[] packageNames;
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ packageNames = connection.focusSearch(accessibilityWindowId,
+ accessibilityNodeId, direction, interactionId, this,
+ Thread.currentThread().getId());
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ if (packageNames != null) {
+ AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
+ interactionId);
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(connection, "focusSearch", "InteractionId=" + interactionId
+ + ":Result: " + info + ";connectionId=" + connectionId
+ + ";accessibilityWindowId=" + accessibilityWindowId
+ + ";accessibilityNodeId=" + accessibilityNodeId + ";direction="
+ + direction);
+ }
+ return info;
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
+ }
+ return null;
+ }
+
+ /**
+ * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
+ *
+ * @param connectionId The id of a connection for interacting with the system.
+ * @param accessibilityWindowId A unique window id. Use
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
+ * to query the currently active window.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use
+ * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
+ * to start from the root.
+ * @param action The action to perform.
+ * @param arguments Optional action arguments.
+ * @return Whether the action was performed.
+ */
+ public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
+ long accessibilityNodeId, int action, Bundle arguments) {
+ try {
+ IAccessibilityServiceConnection connection = getConnection(connectionId);
+ if (connection != null) {
+ final int interactionId = mInteractionIdCounter.getAndIncrement();
+ final boolean success;
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ success = connection.performAccessibilityAction(
+ accessibilityWindowId, accessibilityNodeId, action, arguments,
+ interactionId, this, Thread.currentThread().getId());
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ if (success) {
+ final boolean result =
+ getPerformAccessibilityActionResultAndClear(interactionId);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(connection, "performAccessibilityAction", "InteractionId="
+ + interactionId + ":Result: " + result + ";connectionId="
+ + connectionId + ";accessibilityWindowId=" + accessibilityWindowId
+ + ";accessibilityNodeId=" + accessibilityNodeId + ";action="
+ + action + ";arguments=" + arguments);
+ }
+ return result;
+ }
+ } else {
+ if (DEBUG) {
+ Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
+ }
+ }
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
+ }
+ return false;
+ }
+
+ /**
+ * Clears the accessibility cache.
+ */
+ @UnsupportedAppUsage()
+ public void clearCache() {
+ sAccessibilityCache.clear();
+ }
+
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ switch (event.getEventType()) {
+ case AccessibilityEvent.TYPE_VIEW_SCROLLED:
+ updateScrollingWindow(event.getWindowId(), SystemClock.uptimeMillis());
+ break;
+ case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
+ if (event.getWindowChanges() == AccessibilityEvent.WINDOWS_CHANGE_REMOVED) {
+ deleteScrollingWindow(event.getWindowId());
+ }
+ break;
+ default:
+ break;
+ }
+ sAccessibilityCache.onAccessibilityEvent(event);
+ }
+
+ /**
+ * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
+ *
+ * @param interactionId The interaction id to match the result with the request.
+ * @return The result {@link AccessibilityNodeInfo}.
+ */
+ private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
+ synchronized (mInstanceLock) {
+ final boolean success = waitForResultTimedLocked(interactionId);
+ AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
+ clearResultLocked();
+ return result;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
+ int interactionId) {
+ synchronized (mInstanceLock) {
+ if (interactionId > mInteractionId) {
+ mFindAccessibilityNodeInfoResult = info;
+ mInteractionId = interactionId;
+ mCallingUid = Binder.getCallingUid();
+ }
+ mInstanceLock.notifyAll();
+ }
+ }
+
+ /**
+ * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
+ *
+ * @param interactionId The interaction id to match the result with the request.
+ * @return The result {@link AccessibilityNodeInfo}s.
+ */
+ private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
+ int interactionId) {
+ synchronized (mInstanceLock) {
+ final boolean success = waitForResultTimedLocked(interactionId);
+ final List<AccessibilityNodeInfo> result;
+ if (success) {
+ result = mFindAccessibilityNodeInfosResult;
+ } else {
+ result = Collections.emptyList();
+ }
+ clearResultLocked();
+ if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
+ checkFindAccessibilityNodeInfoResultIntegrity(result);
+ }
+ return result;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
+ int interactionId) {
+ synchronized (mInstanceLock) {
+ if (interactionId > mInteractionId) {
+ if (infos != null) {
+ // If the call is not an IPC, i.e. it is made from the same process, we need to
+ // instantiate new result list to avoid passing internal instances to clients.
+ final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
+ if (!isIpcCall) {
+ mFindAccessibilityNodeInfosResult = new ArrayList<>(infos);
+ } else {
+ mFindAccessibilityNodeInfosResult = infos;
+ }
+ } else {
+ mFindAccessibilityNodeInfosResult = Collections.emptyList();
+ }
+ mInteractionId = interactionId;
+ mCallingUid = Binder.getCallingUid();
+ }
+ mInstanceLock.notifyAll();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setPrefetchAccessibilityNodeInfoResult(@NonNull List<AccessibilityNodeInfo> infos,
+ int interactionId) {
+ int interactionIdWaitingForPrefetchResultCopy = -1;
+ int connectionIdWaitingForPrefetchResultCopy = -1;
+ String[] packageNamesForNextPrefetchResultCopy = null;
+
+ if (infos.isEmpty()) {
+ return;
+ }
+
+ synchronized (mInstanceLock) {
+ if (mInteractionIdWaitingForPrefetchResult == interactionId) {
+ interactionIdWaitingForPrefetchResultCopy = mInteractionIdWaitingForPrefetchResult;
+ connectionIdWaitingForPrefetchResultCopy =
+ mConnectionIdWaitingForPrefetchResult;
+ if (mPackageNamesForNextPrefetchResult != null) {
+ packageNamesForNextPrefetchResultCopy =
+ new String[mPackageNamesForNextPrefetchResult.length];
+ for (int i = 0; i < mPackageNamesForNextPrefetchResult.length; i++) {
+ packageNamesForNextPrefetchResultCopy[i] =
+ mPackageNamesForNextPrefetchResult[i];
+ }
+ }
+ }
+ }
+
+ if (interactionIdWaitingForPrefetchResultCopy == interactionId) {
+ finalizeAndCacheAccessibilityNodeInfos(
+ infos, connectionIdWaitingForPrefetchResultCopy, false,
+ packageNamesForNextPrefetchResultCopy);
+ if (mAccessibilityManager != null
+ && mAccessibilityManager.isAccessibilityTracingEnabled()) {
+ logTrace(getConnection(connectionIdWaitingForPrefetchResultCopy),
+ "setPrefetchAccessibilityNodeInfoResult",
+ "InteractionId:" + interactionId + ";Result: " + infos
+ + ";connectionId=" + connectionIdWaitingForPrefetchResultCopy,
+ Binder.getCallingUid());
+ }
+ } else if (DEBUG) {
+ Log.w(LOG_TAG, "Prefetching for interaction with id " + interactionId + " dropped "
+ + infos.size() + " nodes");
+ }
+ }
+
+ /**
+ * Gets the result of a request to perform an accessibility action.
+ *
+ * @param interactionId The interaction id to match the result with the request.
+ * @return Whether the action was performed.
+ */
+ private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
+ synchronized (mInstanceLock) {
+ final boolean success = waitForResultTimedLocked(interactionId);
+ final boolean result = success ? mPerformAccessibilityActionResult : false;
+ clearResultLocked();
+ return result;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
+ synchronized (mInstanceLock) {
+ if (interactionId > mInteractionId) {
+ mPerformAccessibilityActionResult = succeeded;
+ mInteractionId = interactionId;
+ mCallingUid = Binder.getCallingUid();
+ }
+ mInstanceLock.notifyAll();
+ }
+ }
+
+ /**
+ * Clears the result state.
+ */
+ private void clearResultLocked() {
+ mInteractionId = -1;
+ mFindAccessibilityNodeInfoResult = null;
+ mFindAccessibilityNodeInfosResult = null;
+ mPerformAccessibilityActionResult = false;
+ }
+
+ /**
+ * Waits up to a given bound for a result of a request and returns it.
+ *
+ * @param interactionId The interaction id to match the result with the request.
+ * @return Whether the result was received.
+ */
+ private boolean waitForResultTimedLocked(int interactionId) {
+ long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ while (true) {
+ try {
+ Message sameProcessMessage = getSameProcessMessageAndClear();
+ if (sameProcessMessage != null) {
+ sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
+ }
+
+ if (mInteractionId == interactionId) {
+ return true;
+ }
+ if (mInteractionId > interactionId) {
+ return false;
+ }
+ final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+ waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
+ if (waitTimeMillis <= 0) {
+ return false;
+ }
+ mInstanceLock.wait(waitTimeMillis);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ }
+
+ /**
+ * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
+ *
+ * @param info The info.
+ * @param connectionId The id of the connection to the system.
+ * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if
+ * this value is {@code false}
+ * @param packageNames The valid package names a node can come from.
+ */
+ private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
+ int connectionId, boolean bypassCache, String[] packageNames) {
+ if (info != null) {
+ info.setConnectionId(connectionId);
+ // Empty array means any package name is Okay
+ if (!ArrayUtils.isEmpty(packageNames)) {
+ CharSequence packageName = info.getPackageName();
+ if (packageName == null
+ || !ArrayUtils.contains(packageNames, packageName.toString())) {
+ // If the node package not one of the valid ones, pick the top one - this
+ // is one of the packages running in the introspected UID.
+ info.setPackageName(packageNames[0]);
+ }
+ }
+ info.setSealed(true);
+ if (!bypassCache) {
+ sAccessibilityCache.add(info);
+ }
+ }
+ }
+
+ /**
+ * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
+ *
+ * @param infos The {@link AccessibilityNodeInfo}s.
+ * @param connectionId The id of the connection to the system.
+ * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if
+ * this value is {@code false}
+ * @param packageNames The valid package names a node can come from.
+ */
+ private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
+ int connectionId, boolean bypassCache, String[] packageNames) {
+ if (infos != null) {
+ final int infosCount = infos.size();
+ for (int i = 0; i < infosCount; i++) {
+ AccessibilityNodeInfo info = infos.get(i);
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
+ bypassCache, packageNames);
+ }
+ }
+ }
+
+ /**
+ * Gets the message stored if the interacted and interacting
+ * threads are the same.
+ *
+ * @return The message.
+ */
+ private Message getSameProcessMessageAndClear() {
+ synchronized (mInstanceLock) {
+ Message result = mSameThreadMessage;
+ mSameThreadMessage = null;
+ return result;
+ }
+ }
+
+ /**
+ * Checks whether the infos are a fully connected tree with no duplicates.
+ *
+ * @param infos The result list to check.
+ */
+ private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
+ if (infos.size() == 0) {
+ return;
+ }
+ // Find the root node.
+ AccessibilityNodeInfo root = infos.get(0);
+ final int infoCount = infos.size();
+ for (int i = 1; i < infoCount; i++) {
+ for (int j = i; j < infoCount; j++) {
+ AccessibilityNodeInfo candidate = infos.get(j);
+ if (root.getParentNodeId() == candidate.getSourceNodeId()) {
+ root = candidate;
+ break;
+ }
+ }
+ }
+ if (root == null) {
+ Log.e(LOG_TAG, "No root.");
+ }
+ // Check for duplicates.
+ HashSet<AccessibilityNodeInfo> seen = new HashSet<>();
+ Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
+ fringe.add(root);
+ while (!fringe.isEmpty()) {
+ AccessibilityNodeInfo current = fringe.poll();
+ if (!seen.add(current)) {
+ Log.e(LOG_TAG, "Duplicate node.");
+ return;
+ }
+ final int childCount = current.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final long childId = current.getChildId(i);
+ for (int j = 0; j < infoCount; j++) {
+ AccessibilityNodeInfo child = infos.get(j);
+ if (child.getSourceNodeId() == childId) {
+ fringe.add(child);
+ }
+ }
+ }
+ }
+ final int disconnectedCount = infos.size() - seen.size();
+ if (disconnectedCount > 0) {
+ Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
+ }
+ }
+
+ /**
+ * Update scroll event timestamp of a given window.
+ *
+ * @param windowId The window id.
+ * @param uptimeMillis Device uptime millis.
+ */
+ private void updateScrollingWindow(int windowId, long uptimeMillis) {
+ synchronized (sScrollingWindows) {
+ sScrollingWindows.put(windowId, uptimeMillis);
+ }
+ }
+
+ /**
+ * Remove a window from the scrolling windows list.
+ *
+ * @param windowId The window id.
+ */
+ private void deleteScrollingWindow(int windowId) {
+ synchronized (sScrollingWindows) {
+ sScrollingWindows.delete(windowId);
+ }
+ }
+
+ /**
+ * Whether or not the window is scrolling.
+ *
+ * @param windowId
+ * @return true if it's scrolling.
+ */
+ private boolean isWindowScrolling(int windowId) {
+ synchronized (sScrollingWindows) {
+ final long latestScrollingTime = sScrollingWindows.get(windowId);
+ if (latestScrollingTime == 0) {
+ return false;
+ }
+ final long currentUptime = SystemClock.uptimeMillis();
+ if (currentUptime > (latestScrollingTime + DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS)) {
+ sScrollingWindows.delete(windowId);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void logTrace(
+ IAccessibilityServiceConnection connection, String method, String params,
+ int callingUid) {
+ try {
+ Bundle b = new Bundle();
+ ArrayList<StackTraceElement> callStack = new ArrayList<StackTraceElement>(
+ Arrays.asList(Thread.currentThread().getStackTrace()));
+ b.putSerializable(CALL_STACK, callStack);
+ connection.logTrace(SystemClock.elapsedRealtimeNanos(),
+ LOG_TAG + ".callback for " + method, params, Process.myPid(),
+ Thread.currentThread().getId(), callingUid, b);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed to log trace. " + e);
+ }
+ }
+
+ private void logTrace(
+ IAccessibilityServiceConnection connection, String method, String params) {
+ logTrace(connection, method, params, mCallingUid);
+ }
+}
diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java
new file mode 100644
index 0000000..4454f29
--- /dev/null
+++ b/android/view/accessibility/AccessibilityManager.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.view.IWindow;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent.EventType;
+
+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,
+ * 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}.
+ *
+ * @see AccessibilityEvent
+ * @see android.content.Context#getSystemService
+ */
+@SuppressWarnings("UnusedDeclaration")
+public final class AccessibilityManager {
+
+ private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0);
+
+
+ /**
+ * Listener for the accessibility state.
+ */
+ public interface AccessibilityStateChangeListener {
+
+ /**
+ * Called back on change in the accessibility state.
+ *
+ * @param enabled Whether accessibility is enabled.
+ */
+ public void onAccessibilityStateChanged(boolean enabled);
+ }
+
+ /**
+ * Listener for the system touch exploration state. To listen for changes to
+ * the touch exploration state on the device, implement this interface and
+ * register it with the system by calling
+ * {@link #addTouchExplorationStateChangeListener}.
+ */
+ public interface TouchExplorationStateChangeListener {
+
+ /**
+ * Called when the touch exploration enabled state changes.
+ *
+ * @param enabled Whether touch exploration is enabled.
+ */
+ public void onTouchExplorationStateChanged(boolean enabled);
+ }
+
+ /**
+ * Listener for the system high text contrast state. To listen for changes to
+ * the high text contrast state on the device, implement this interface and
+ * register it with the system by calling
+ * {@link #addHighTextContrastStateChangeListener}.
+ */
+ public interface HighTextContrastChangeListener {
+
+ /**
+ * Called when the high text contrast enabled state changes.
+ *
+ * @param enabled Whether high text contrast is enabled.
+ */
+ public void onHighTextContrastStateChanged(boolean enabled);
+ }
+
+ /**
+ * Policy to inject behavior into the accessibility manager.
+ *
+ * @hide
+ */
+ public interface AccessibilityPolicy {
+ /**
+ * Checks whether accessibility is enabled.
+ *
+ * @param accessibilityEnabled Whether the accessibility layer is enabled.
+ * @return whether accessibility is enabled.
+ */
+ boolean isEnabled(boolean accessibilityEnabled);
+
+ /**
+ * Notifies the policy for an accessibility event.
+ *
+ * @param event The event.
+ * @param accessibilityEnabled Whether the accessibility layer is enabled.
+ * @param relevantEventTypes The events relevant events.
+ * @return The event to dispatch or null.
+ */
+ @Nullable AccessibilityEvent onAccessibilityEvent(@NonNull AccessibilityEvent event,
+ boolean accessibilityEnabled, @EventType int relevantEventTypes);
+
+ /**
+ * Gets the list of relevant events.
+ *
+ * @param relevantEventTypes The relevant events.
+ * @return The relevant events to report.
+ */
+ @EventType int getRelevantEventTypes(@EventType int relevantEventTypes);
+
+ /**
+ * Gets the list of installed services to report.
+ *
+ * @param installedService The installed services.
+ * @return The services to report.
+ */
+ @NonNull List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
+ @Nullable List<AccessibilityServiceInfo> installedService);
+
+ /**
+ * Gets the list of enabled accessibility services.
+ *
+ * @param feedbackTypeFlags The feedback type to query for.
+ * @param enabledService The enabled services.
+ * @return The services to report.
+ */
+ @Nullable List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
+ @FeedbackType int feedbackTypeFlags,
+ @Nullable List<AccessibilityServiceInfo> enabledService);
+ }
+
+ private final IAccessibilityManagerClient.Stub mClient =
+ new IAccessibilityManagerClient.Stub() {
+ public void setState(int state) {
+ }
+
+ public void notifyServicesStateChanged(long updatedUiTimeout) {
+ }
+
+ public void setRelevantEventTypes(int eventTypes) {
+ }
+
+ public void setFocusAppearance(int strokeWidth, int color) {
+ }
+ };
+
+ /**
+ * Get an AccessibilityManager instance (create one if necessary).
+ *
+ */
+ public static AccessibilityManager getInstance(Context context) {
+ return sInstance;
+ }
+
+ /**
+ * Create an instance.
+ *
+ * @param context A {@link Context}.
+ */
+ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
+ }
+
+ public IAccessibilityManagerClient getClient() {
+ return mClient;
+ }
+
+ /**
+ * Returns if the {@link AccessibilityManager} is enabled.
+ *
+ * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ return false;
+ }
+
+ /**
+ * Returns if the touch exploration in the system is enabled.
+ *
+ * @return True if touch exploration is enabled, false otherwise.
+ */
+ public boolean isTouchExplorationEnabled() {
+ return true;
+ }
+
+ /**
+ * Returns if the high text contrast in the system is enabled.
+ * <p>
+ * <strong>Note:</strong> You need to query this only if you application is
+ * doing its own rendering and does not rely on the platform rendering pipeline.
+ * </p>
+ *
+ */
+ public boolean isHighTextContrastEnabled() {
+ return false;
+ }
+
+ /**
+ * Sends an {@link AccessibilityEvent}.
+ */
+ public void sendAccessibilityEvent(AccessibilityEvent event) {
+ }
+
+ /**
+ * Returns whether there are observers registered for this event type. If
+ * this method returns false you shuold not generate events of this type
+ * to conserve resources.
+ *
+ * @param type The event type.
+ * @return Whether the event is being observed.
+ */
+ public boolean isObservedEventType(@AccessibilityEvent.EventType int type) {
+ return false;
+ }
+
+ /**
+ * Requests interruption of the accessibility feedback from all accessibility services.
+ */
+ public void interrupt() {
+ }
+
+ /**
+ * Returns the {@link ServiceInfo}s of the installed accessibility services.
+ *
+ * @return An unmodifiable list with {@link ServiceInfo}s.
+ */
+ @Deprecated
+ public List<ServiceInfo> getAccessibilityServiceList() {
+ return Collections.emptyList();
+ }
+
+ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns the {@link AccessibilityServiceInfo}s of the enabled accessibility services
+ * for a given feedback type.
+ *
+ * @param feedbackTypeFlags The feedback type flags.
+ * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
+ *
+ * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE
+ * @see AccessibilityServiceInfo#FEEDBACK_GENERIC
+ * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
+ * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
+ * @see AccessibilityServiceInfo#FEEDBACK_VISUAL
+ */
+ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
+ int feedbackTypeFlags) {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Registers an {@link AccessibilityStateChangeListener} for changes in
+ * the global accessibility state of the system.
+ *
+ * @param listener The listener.
+ * @return True if successfully registered.
+ */
+ public boolean addAccessibilityStateChangeListener(
+ AccessibilityStateChangeListener listener) {
+ return true;
+ }
+
+ /**
+ * Registers an {@link AccessibilityStateChangeListener} for changes in
+ * the global accessibility state of the system. If the listener has already been registered,
+ * the handler used to call it back is updated.
+ *
+ * @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.
+ */
+ public void addAccessibilityStateChangeListener(
+ @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {}
+
+ public boolean removeAccessibilityStateChangeListener(
+ AccessibilityStateChangeListener listener) {
+ return true;
+ }
+
+ /**
+ * Registers a {@link TouchExplorationStateChangeListener} for changes in
+ * the global touch exploration state of the system.
+ *
+ * @param listener The listener.
+ * @return True if successfully registered.
+ */
+ public boolean addTouchExplorationStateChangeListener(
+ @NonNull TouchExplorationStateChangeListener listener) {
+ return true;
+ }
+
+ /**
+ * Registers an {@link TouchExplorationStateChangeListener} for changes in
+ * the global touch exploration state of the system. If the listener has already been
+ * registered, the handler used to call it back is updated.
+ *
+ * @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.
+ */
+ public void addTouchExplorationStateChangeListener(
+ @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {}
+
+ /**
+ * Unregisters a {@link TouchExplorationStateChangeListener}.
+ *
+ * @param listener The listener.
+ * @return True if successfully unregistered.
+ */
+ public boolean removeTouchExplorationStateChangeListener(
+ @NonNull TouchExplorationStateChangeListener listener) {
+ return true;
+ }
+
+ /**
+ * Registers a {@link HighTextContrastChangeListener} for changes in
+ * the global high text contrast state of the system.
+ *
+ * @param listener The listener.
+ *
+ * @hide
+ */
+ public void addHighTextContrastStateChangeListener(
+ @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {}
+
+ /**
+ * Unregisters a {@link HighTextContrastChangeListener}.
+ *
+ * @param listener The listener.
+ *
+ * @hide
+ */
+ public void removeHighTextContrastStateChangeListener(
+ @NonNull HighTextContrastChangeListener listener) {}
+
+ /**
+ * Sets the current state and notifies listeners, if necessary.
+ *
+ * @param stateFlags The state flags.
+ */
+ private void setStateLocked(int stateFlags) {
+ }
+
+ public int addAccessibilityInteractionConnection(IWindow windowToken,
+ IAccessibilityInteractionConnection connection) {
+ return View.NO_ID;
+ }
+
+ public void removeAccessibilityInteractionConnection(IWindow windowToken) {
+ }
+
+}
diff --git a/android/view/accessibility/AccessibilityNodeIdManager.java b/android/view/accessibility/AccessibilityNodeIdManager.java
new file mode 100644
index 0000000..e377117
--- /dev/null
+++ b/android/view/accessibility/AccessibilityNodeIdManager.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.view.View;
+
+/**
+ * Class that replaces the original AccessibilityNodeIdManager with a no-op version. This avoids
+ * the accidental leaking of views referenced by the originalin layoutlib.
+ */
+public final class AccessibilityNodeIdManager {
+ private static AccessibilityNodeIdManager sIdManager = new AccessibilityNodeIdManager();
+
+
+ public static synchronized AccessibilityNodeIdManager getInstance() {
+ return sIdManager;
+ }
+
+ private AccessibilityNodeIdManager() {
+ }
+
+ /**
+ * Register view to be kept track of by the accessibility system.
+ * Must be paired with unregisterView, otherwise this will leak.
+ * @param view The view to be registered.
+ * @param id The accessibilityViewId of the view.
+ */
+ public void registerViewWithId(View view, int id) {
+ }
+
+ /**
+ * Unregister view, accessibility won't keep track of this view after this call.
+ * @param id The id returned from registerView when the view as first associated.
+ */
+ public void unregisterViewWithId(int id) {
+ }
+
+ /**
+ * Accessibility uses this to find the view in the hierarchy.
+ * @param id The accessibility view id.
+ * @return The view.
+ */
+ public View findView(int id) {
+ return null;
+ }
+}
+
diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java
new file mode 100644
index 0000000..085eb81
--- /dev/null
+++ b/android/view/accessibility/AccessibilityNodeInfo.java
@@ -0,0 +1,5974 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import static com.android.internal.util.BitUtils.bitAt;
+import static com.android.internal.util.BitUtils.isBitSet;
+
+import static java.util.Collections.EMPTY_LIST;
+
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.InputType;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.AccessibilityClickableSpan;
+import android.text.style.AccessibilityReplacementSpan;
+import android.text.style.AccessibilityURLSpan;
+import android.text.style.ClickableSpan;
+import android.text.style.ReplacementSpan;
+import android.text.style.URLSpan;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.LongArray;
+import android.util.Pools.SynchronizedPool;
+import android.util.Size;
+import android.util.TypedValue;
+import android.view.SurfaceView;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.util.CollectionUtils;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This class represents a node of the window content as well as actions that
+ * can be requested from its source. From the point of view of an
+ * {@link android.accessibilityservice.AccessibilityService} a window's content is
+ * presented as a tree of accessibility node infos, which may or may not map one-to-one
+ * to the view hierarchy. In other words, a custom view is free to report itself as
+ * a tree of accessibility node info.
+ * </p>
+ * <p>
+ * Once an accessibility node info is delivered to an accessibility service it is
+ * made immutable and calling a state mutation method generates an error.
+ * </p>
+ * <p>
+ * Please refer to {@link android.accessibilityservice.AccessibilityService} for
+ * details about how to obtain a handle to window content as a tree of accessibility
+ * node info as well as details about the security model.
+ * </p>
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about making applications accessible, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * @see android.accessibilityservice.AccessibilityService
+ * @see AccessibilityEvent
+ * @see AccessibilityManager
+ */
+public class AccessibilityNodeInfo implements Parcelable {
+
+ private static final String TAG = "AccessibilityNodeInfo";
+
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) && Build.IS_DEBUGGABLE;
+
+ /** @hide */
+ public static final int UNDEFINED_CONNECTION_ID = -1;
+
+ /** @hide */
+ public static final int UNDEFINED_SELECTION_INDEX = -1;
+
+ /** @hide */
+ public static final int UNDEFINED_ITEM_ID = Integer.MAX_VALUE;
+
+ /** @hide */
+ public static final int ROOT_ITEM_ID = Integer.MAX_VALUE - 1;
+
+ /** @hide */
+ public static final int LEASHED_ITEM_ID = Integer.MAX_VALUE - 2;
+
+ /** @hide */
+ public static final long UNDEFINED_NODE_ID = makeNodeId(UNDEFINED_ITEM_ID, UNDEFINED_ITEM_ID);
+
+ /** @hide */
+ public static final long ROOT_NODE_ID = makeNodeId(ROOT_ITEM_ID,
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+
+ /** @hide */
+ public static final long LEASHED_NODE_ID = makeNodeId(LEASHED_ITEM_ID,
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+
+ /** @hide */
+ public static final int FLAG_PREFETCH_PREDECESSORS = 0x00000001;
+
+ /** @hide */
+ public static final int FLAG_PREFETCH_SIBLINGS = 0x00000002;
+
+ /** @hide */
+ public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004;
+
+ /** @hide */
+ public static final int FLAG_PREFETCH_MASK = 0x00000007;
+
+ /** @hide */
+ public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008;
+
+ /** @hide */
+ public static final int FLAG_REPORT_VIEW_IDS = 0x00000010;
+
+ // Actions.
+
+ /**
+ * Action that gives input focus to the node.
+ */
+ public static final int ACTION_FOCUS = 0x00000001;
+
+ /**
+ * Action that clears input focus of the node.
+ */
+ public static final int ACTION_CLEAR_FOCUS = 0x00000002;
+
+ /**
+ * Action that selects the node.
+ */
+ public static final int ACTION_SELECT = 0x00000004;
+
+ /**
+ * Action that deselects the node.
+ */
+ public static final int ACTION_CLEAR_SELECTION = 0x00000008;
+
+ /**
+ * Action that clicks on the node info.
+ *
+ * See {@link AccessibilityAction#ACTION_CLICK}
+ */
+ public static final int ACTION_CLICK = 0x00000010;
+
+ /**
+ * Action that long clicks on the node.
+ *
+ * <p>It does not support coordinate information for anchoring.</p>
+ */
+ public static final int ACTION_LONG_CLICK = 0x00000020;
+
+ /**
+ * Action that gives accessibility focus to the node.
+ */
+ public static final int ACTION_ACCESSIBILITY_FOCUS = 0x00000040;
+
+ /**
+ * Action that clears accessibility focus of the node.
+ */
+ public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080;
+
+ /**
+ * Action that requests to go to the next entity in this node's text
+ * at a given movement granularity. For example, move to the next character,
+ * word, etc.
+ * <p>
+ * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
+ * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
+ * <strong>Example:</strong> Move to the previous character and do not extend selection.
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
+ * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
+ * false);
+ * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, arguments);
+ * </code></pre></p>
+ * </p>
+ *
+ * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ *
+ * @see #setMovementGranularities(int)
+ * @see #getMovementGranularities()
+ *
+ * @see #MOVEMENT_GRANULARITY_CHARACTER
+ * @see #MOVEMENT_GRANULARITY_WORD
+ * @see #MOVEMENT_GRANULARITY_LINE
+ * @see #MOVEMENT_GRANULARITY_PARAGRAPH
+ * @see #MOVEMENT_GRANULARITY_PAGE
+ */
+ public static final int ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100;
+
+ /**
+ * Action that requests to go to the previous entity in this node's text
+ * at a given movement granularity. For example, move to the next character,
+ * word, etc.
+ * <p>
+ * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
+ * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
+ * <strong>Example:</strong> Move to the next character and do not extend selection.
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
+ * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
+ * false);
+ * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
+ * arguments);
+ * </code></pre></p>
+ * </p>
+ *
+ * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ *
+ * @see #setMovementGranularities(int)
+ * @see #getMovementGranularities()
+ *
+ * @see #MOVEMENT_GRANULARITY_CHARACTER
+ * @see #MOVEMENT_GRANULARITY_WORD
+ * @see #MOVEMENT_GRANULARITY_LINE
+ * @see #MOVEMENT_GRANULARITY_PARAGRAPH
+ * @see #MOVEMENT_GRANULARITY_PAGE
+ */
+ public static final int ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200;
+
+ /**
+ * Action to move to the next HTML element of a given type. For example, move
+ * to the BUTTON, INPUT, TABLE, etc.
+ * <p>
+ * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
+ * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, arguments);
+ * </code></pre></p>
+ * </p>
+ */
+ public static final int ACTION_NEXT_HTML_ELEMENT = 0x00000400;
+
+ /**
+ * Action to move to the previous HTML element of a given type. For example, move
+ * to the BUTTON, INPUT, TABLE, etc.
+ * <p>
+ * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
+ * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, arguments);
+ * </code></pre></p>
+ * </p>
+ */
+ public static final int ACTION_PREVIOUS_HTML_ELEMENT = 0x00000800;
+
+ /**
+ * Action to scroll the node content forward.
+ */
+ public static final int ACTION_SCROLL_FORWARD = 0x00001000;
+
+ /**
+ * Action to scroll the node content backward.
+ */
+ public static final int ACTION_SCROLL_BACKWARD = 0x00002000;
+
+ /**
+ * Action to copy the current selection to the clipboard.
+ */
+ public static final int ACTION_COPY = 0x00004000;
+
+ /**
+ * Action to paste the current clipboard content.
+ */
+ public static final int ACTION_PASTE = 0x00008000;
+
+ /**
+ * Action to cut the current selection and place it to the clipboard.
+ */
+ public static final int ACTION_CUT = 0x00010000;
+
+ /**
+ * Action to set the selection. Performing this action with no arguments
+ * clears the selection.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link #ACTION_ARGUMENT_SELECTION_START_INT},
+ * {@link #ACTION_ARGUMENT_SELECTION_END_INT}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1);
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2);
+ * info.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments);
+ * </code></pre></p>
+ * </p>
+ *
+ * @see #ACTION_ARGUMENT_SELECTION_START_INT
+ * @see #ACTION_ARGUMENT_SELECTION_END_INT
+ */
+ public static final int ACTION_SET_SELECTION = 0x00020000;
+
+ /**
+ * Action to expand an expandable node.
+ */
+ public static final int ACTION_EXPAND = 0x00040000;
+
+ /**
+ * Action to collapse an expandable node.
+ */
+ public static final int ACTION_COLLAPSE = 0x00080000;
+
+ /**
+ * Action to dismiss a dismissable node.
+ */
+ public static final int ACTION_DISMISS = 0x00100000;
+
+ /**
+ * Action that sets the text of the node. Performing the action without argument, using <code>
+ * null</code> or empty {@link CharSequence} will clear the text. This action will also put the
+ * cursor at the end of text.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link #ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
+ * "android");
+ * info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
+ * </code></pre></p>
+ */
+ public static final int ACTION_SET_TEXT = 0x00200000;
+
+ /** @hide */
+ public static final int LAST_LEGACY_STANDARD_ACTION = ACTION_SET_TEXT;
+
+ /**
+ * Mask to see if the value is larger than the largest ACTION_ constant
+ */
+ private static final int ACTION_TYPE_MASK = 0xFF000000;
+
+ // Action arguments
+
+ /**
+ * Argument for which movement granularity to be used when traversing the node text.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_NEXT_AT_MOVEMENT_GRANULARITY}</li>
+ * <li>{@link AccessibilityAction#ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY}</li>
+ * </ul>
+ * </p>
+ *
+ * @see AccessibilityAction#ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+ * @see AccessibilityAction#ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
+ */
+ public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT =
+ "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
+
+ /**
+ * Argument for which HTML element to get moving to the next/previous HTML element.
+ * <p>
+ * <strong>Type:</strong> String<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_NEXT_HTML_ELEMENT}</li>
+ * <li>{@link AccessibilityAction#ACTION_PREVIOUS_HTML_ELEMENT}</li>
+ * </ul>
+ * </p>
+ *
+ * @see AccessibilityAction#ACTION_NEXT_HTML_ELEMENT
+ * @see AccessibilityAction#ACTION_PREVIOUS_HTML_ELEMENT
+ */
+ public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING =
+ "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
+
+ /**
+ * Argument for whether when moving at granularity to extend the selection
+ * or to move it otherwise.
+ * <p>
+ * <strong>Type:</strong> boolean<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_NEXT_AT_MOVEMENT_GRANULARITY}</li>
+ * <li>{@link AccessibilityAction#ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+ * @see AccessibilityAction#ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
+ */
+ public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN =
+ "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN";
+
+ /**
+ * Argument for specifying the selection start.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_SET_SELECTION}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_SET_SELECTION
+ */
+ public static final String ACTION_ARGUMENT_SELECTION_START_INT =
+ "ACTION_ARGUMENT_SELECTION_START_INT";
+
+ /**
+ * Argument for specifying the selection end.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_SET_SELECTION}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_SET_SELECTION
+ */
+ public static final String ACTION_ARGUMENT_SELECTION_END_INT =
+ "ACTION_ARGUMENT_SELECTION_END_INT";
+
+ /**
+ * Argument for specifying the text content to set.
+ * <p>
+ * <strong>Type:</strong> CharSequence<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_SET_TEXT}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_SET_TEXT
+ */
+ public static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE =
+ "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE";
+
+ /**
+ * Argument for specifying the collection row to make visible on screen.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_SCROLL_TO_POSITION}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_SCROLL_TO_POSITION
+ */
+ public static final String ACTION_ARGUMENT_ROW_INT =
+ "android.view.accessibility.action.ARGUMENT_ROW_INT";
+
+ /**
+ * Argument for specifying the collection column to make visible on screen.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_SCROLL_TO_POSITION}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_SCROLL_TO_POSITION
+ */
+ public static final String ACTION_ARGUMENT_COLUMN_INT =
+ "android.view.accessibility.action.ARGUMENT_COLUMN_INT";
+
+ /**
+ * Argument for specifying the progress value to set.
+ * <p>
+ * <strong>Type:</strong> float<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_SET_PROGRESS}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_SET_PROGRESS
+ */
+ public static final String ACTION_ARGUMENT_PROGRESS_VALUE =
+ "android.view.accessibility.action.ARGUMENT_PROGRESS_VALUE";
+
+ /**
+ * Argument for specifying the x coordinate to which to move a window.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_MOVE_WINDOW}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_MOVE_WINDOW
+ */
+ public static final String ACTION_ARGUMENT_MOVE_WINDOW_X =
+ "ACTION_ARGUMENT_MOVE_WINDOW_X";
+
+ /**
+ * Argument for specifying the y coordinate to which to move a window.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_MOVE_WINDOW}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_MOVE_WINDOW
+ */
+ public static final String ACTION_ARGUMENT_MOVE_WINDOW_Y =
+ "ACTION_ARGUMENT_MOVE_WINDOW_Y";
+
+ /**
+ * Argument to pass the {@link AccessibilityClickableSpan}.
+ * For use with R.id.accessibilityActionClickOnClickableSpan
+ * @hide
+ */
+ public static final String ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN =
+ "android.view.accessibility.action.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN";
+
+ /**
+ * Argument to represent the duration in milliseconds to press and hold a node.
+ * <p>
+ * <strong>Type:</strong> int<br>
+ * <strong>Actions:</strong>
+ * <ul>
+ * <li>{@link AccessibilityAction#ACTION_PRESS_AND_HOLD}</li>
+ * </ul>
+ *
+ * @see AccessibilityAction#ACTION_PRESS_AND_HOLD
+ */
+ public static final String ACTION_ARGUMENT_PRESS_AND_HOLD_DURATION_MILLIS_INT =
+ "android.view.accessibility.action.ARGUMENT_PRESS_AND_HOLD_DURATION_MILLIS_INT";
+
+ // Focus types
+
+ /**
+ * The input focus.
+ */
+ public static final int FOCUS_INPUT = 1;
+
+ /**
+ * The accessibility focus.
+ */
+ public static final int FOCUS_ACCESSIBILITY = 2;
+
+ // Movement granularities
+
+ /**
+ * Movement granularity bit for traversing the text of a node by character.
+ */
+ public static final int MOVEMENT_GRANULARITY_CHARACTER = 0x00000001;
+
+ /**
+ * Movement granularity bit for traversing the text of a node by word.
+ */
+ public static final int MOVEMENT_GRANULARITY_WORD = 0x00000002;
+
+ /**
+ * Movement granularity bit for traversing the text of a node by line.
+ */
+ public static final int MOVEMENT_GRANULARITY_LINE = 0x00000004;
+
+ /**
+ * Movement granularity bit for traversing the text of a node by paragraph.
+ */
+ public static final int MOVEMENT_GRANULARITY_PARAGRAPH = 0x00000008;
+
+ /**
+ * Movement granularity bit for traversing the text of a node by page.
+ */
+ public static final int MOVEMENT_GRANULARITY_PAGE = 0x00000010;
+
+ /**
+ * Key used to request and locate extra data for text character location. This key requests that
+ * an array of {@link android.graphics.RectF}s be added to the extras. This request is made with
+ * {@link #refreshWithExtraData(String, Bundle)}. The arguments taken by this request are two
+ * integers: {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX} and
+ * {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH}. The starting index must be valid
+ * inside the CharSequence returned by {@link #getText()}, and the length must be positive.
+ * <p>
+ * The data can be retrieved from the {@code Bundle} returned by {@link #getExtras()} using this
+ * string as a key for {@link Bundle#getParcelableArray(String)}. The
+ * {@link android.graphics.RectF} will be null for characters that either do not exist or are
+ * off the screen.
+ *
+ * {@see #refreshWithExtraData(String, Bundle)}
+ */
+ public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY =
+ "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_KEY";
+
+ /**
+ * Integer argument specifying the start index of the requested text location data. Must be
+ * valid inside the CharSequence returned by {@link #getText()}.
+ *
+ * @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
+ */
+ public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX =
+ "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX";
+
+ /**
+ * Integer argument specifying the end index of the requested text location data. Must be
+ * positive and no larger than {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH}.
+ *
+ * @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
+ */
+ public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH =
+ "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
+
+ /**
+ * The maximum allowed length of the requested text location data.
+ */
+ public static final int EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH = 20000;
+
+ /**
+ * Key used to request extra data for the rendering information.
+ * The key requests that a {@link AccessibilityNodeInfo.ExtraRenderingInfo} be added to this
+ * info. This request is made with {@link #refreshWithExtraData(String, Bundle)} without
+ * argument.
+ * <p>
+ * The data can be retrieved from the {@link ExtraRenderingInfo} returned by
+ * {@link #getExtraRenderingInfo()} using {@link ExtraRenderingInfo#getLayoutSize},
+ * {@link ExtraRenderingInfo#getTextSizeInPx()} and
+ * {@link ExtraRenderingInfo#getTextSizeUnit()}. For layout params, it is supported by both
+ * {@link TextView} and {@link ViewGroup}. For text size and unit, it is only supported by
+ * {@link TextView}.
+ *
+ * @see #refreshWithExtraData(String, Bundle)
+ */
+ public static final String EXTRA_DATA_RENDERING_INFO_KEY =
+ "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
+
+ /** @hide */
+ public static final String EXTRA_DATA_REQUESTED_KEY =
+ "android.view.accessibility.AccessibilityNodeInfo.extra_data_requested";
+
+ // Boolean attributes.
+
+ private static final int BOOLEAN_PROPERTY_CHECKABLE = 0x00000001;
+
+ private static final int BOOLEAN_PROPERTY_CHECKED = 0x00000002;
+
+ private static final int BOOLEAN_PROPERTY_FOCUSABLE = 0x00000004;
+
+ private static final int BOOLEAN_PROPERTY_FOCUSED = 0x00000008;
+
+ private static final int BOOLEAN_PROPERTY_SELECTED = 0x00000010;
+
+ private static final int BOOLEAN_PROPERTY_CLICKABLE = 0x00000020;
+
+ private static final int BOOLEAN_PROPERTY_LONG_CLICKABLE = 0x00000040;
+
+ private static final int BOOLEAN_PROPERTY_ENABLED = 0x00000080;
+
+ private static final int BOOLEAN_PROPERTY_PASSWORD = 0x00000100;
+
+ private static final int BOOLEAN_PROPERTY_SCROLLABLE = 0x00000200;
+
+ private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 0x00000400;
+
+ private static final int BOOLEAN_PROPERTY_VISIBLE_TO_USER = 0x00000800;
+
+ private static final int BOOLEAN_PROPERTY_EDITABLE = 0x00001000;
+
+ private static final int BOOLEAN_PROPERTY_OPENS_POPUP = 0x00002000;
+
+ private static final int BOOLEAN_PROPERTY_DISMISSABLE = 0x00004000;
+
+ private static final int BOOLEAN_PROPERTY_MULTI_LINE = 0x00008000;
+
+ private static final int BOOLEAN_PROPERTY_CONTENT_INVALID = 0x00010000;
+
+ private static final int BOOLEAN_PROPERTY_CONTEXT_CLICKABLE = 0x00020000;
+
+ private static final int BOOLEAN_PROPERTY_IMPORTANCE = 0x0040000;
+
+ private static final int BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE = 0x0080000;
+
+ private static final int BOOLEAN_PROPERTY_IS_SHOWING_HINT = 0x0100000;
+
+ private static final int BOOLEAN_PROPERTY_IS_HEADING = 0x0200000;
+
+ private static final int BOOLEAN_PROPERTY_IS_TEXT_ENTRY_KEY = 0x0400000;
+
+ /**
+ * Bits that provide the id of a virtual descendant of a view.
+ */
+ private static final long VIRTUAL_DESCENDANT_ID_MASK = 0xffffffff00000000L;
+ /**
+ * Bit shift of {@link #VIRTUAL_DESCENDANT_ID_MASK} to get to the id for a
+ * virtual descendant of a view. Such a descendant does not exist in the view
+ * hierarchy and is only reported via the accessibility APIs.
+ */
+ private static final int VIRTUAL_DESCENDANT_ID_SHIFT = 32;
+
+ // TODO(b/129300068): Remove sNumInstancesInUse.
+ private static AtomicInteger sNumInstancesInUse;
+
+ /**
+ * Gets the accessibility view id which identifies a View in the view three.
+ *
+ * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}.
+ * @return The accessibility view id part of the node id.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static int getAccessibilityViewId(long accessibilityNodeId) {
+ return (int) accessibilityNodeId;
+ }
+
+ /**
+ * Gets the virtual descendant id which identifies an imaginary view in a
+ * containing View.
+ *
+ * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}.
+ * @return The virtual view id part of the node id.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static int getVirtualDescendantId(long accessibilityNodeId) {
+ return (int) ((accessibilityNodeId & VIRTUAL_DESCENDANT_ID_MASK)
+ >> VIRTUAL_DESCENDANT_ID_SHIFT);
+ }
+
+ /**
+ * Makes a node id by shifting the <code>virtualDescendantId</code>
+ * by {@link #VIRTUAL_DESCENDANT_ID_SHIFT} and taking
+ * the bitwise or with the <code>accessibilityViewId</code>.
+ *
+ * @param accessibilityViewId A View accessibility id.
+ * @param virtualDescendantId A virtual descendant id.
+ * @return The node id.
+ *
+ * @hide
+ */
+ public static long makeNodeId(int accessibilityViewId, int virtualDescendantId) {
+ return (((long) virtualDescendantId) << VIRTUAL_DESCENDANT_ID_SHIFT) | accessibilityViewId;
+ }
+
+ // Housekeeping.
+ private static final int MAX_POOL_SIZE = 50;
+ private static final SynchronizedPool<AccessibilityNodeInfo> sPool =
+ new SynchronizedPool<>(MAX_POOL_SIZE);
+
+ private static final AccessibilityNodeInfo DEFAULT = new AccessibilityNodeInfo();
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private boolean mSealed;
+
+ // Data.
+ private int mWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ @UnsupportedAppUsage
+ private long mSourceNodeId = UNDEFINED_NODE_ID;
+ private long mParentNodeId = UNDEFINED_NODE_ID;
+ private long mLabelForId = UNDEFINED_NODE_ID;
+ private long mLabeledById = UNDEFINED_NODE_ID;
+ private long mTraversalBefore = UNDEFINED_NODE_ID;
+ private long mTraversalAfter = UNDEFINED_NODE_ID;
+
+ private int mBooleanProperties;
+ private final Rect mBoundsInParent = new Rect();
+ private final Rect mBoundsInScreen = new Rect();
+ private int mDrawingOrderInParent;
+
+ private CharSequence mPackageName;
+ private CharSequence mClassName;
+ // Hidden, unparceled value used to hold the original value passed to setText
+ private CharSequence mOriginalText;
+ private CharSequence mText;
+ private CharSequence mHintText;
+ private CharSequence mError;
+ private CharSequence mPaneTitle;
+ private CharSequence mStateDescription;
+ private CharSequence mContentDescription;
+ private CharSequence mTooltipText;
+ private String mViewIdResourceName;
+ private ArrayList<String> mExtraDataKeys;
+
+ @UnsupportedAppUsage
+ private LongArray mChildNodeIds;
+ private ArrayList<AccessibilityAction> mActions;
+
+ private int mMaxTextLength = -1;
+ private int mMovementGranularities;
+
+ private int mTextSelectionStart = UNDEFINED_SELECTION_INDEX;
+ private int mTextSelectionEnd = UNDEFINED_SELECTION_INDEX;
+ private int mInputType = InputType.TYPE_NULL;
+ private int mLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE;
+
+ private Bundle mExtras;
+
+ private int mConnectionId = UNDEFINED_CONNECTION_ID;
+
+ private RangeInfo mRangeInfo;
+ private CollectionInfo mCollectionInfo;
+ private CollectionItemInfo mCollectionItemInfo;
+
+ private TouchDelegateInfo mTouchDelegateInfo;
+
+ private ExtraRenderingInfo mExtraRenderingInfo;
+
+ private IBinder mLeashedChild;
+ private IBinder mLeashedParent;
+ private long mLeashedParentNodeId = UNDEFINED_NODE_ID;
+
+ /**
+ * Creates a new {@link AccessibilityNodeInfo}.
+ */
+ public AccessibilityNodeInfo() {
+ }
+
+ /**
+ * Creates a new {@link AccessibilityNodeInfo} with the given <code>source</code>.
+ *
+ * @param source The source view.
+ */
+ public AccessibilityNodeInfo(@NonNull View source) {
+ setSource(source);
+ }
+
+ /**
+ * Creates a new {@link AccessibilityNodeInfo} with the given <code>source</code>.
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public AccessibilityNodeInfo(@NonNull View root, int virtualDescendantId) {
+ setSource(root, virtualDescendantId);
+ }
+
+ /**
+ * Copy constructor. Creates a new {@link AccessibilityNodeInfo}, and this new instance is
+ * initialized from the given <code>info</code>.
+ *
+ * @param info The other info.
+ */
+ public AccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
+ init(info, false /* usePoolingInfo */);
+ }
+
+ /**
+ * Sets the source.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param source The info source.
+ */
+ public void setSource(View source) {
+ setSource(source, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the source to be a virtual descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root
+ * is set as the source.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report themselves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setSource(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ mWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED_ITEM_ID;
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ }
+
+ /**
+ * Find the view that has the specified focus type. The search starts from
+ * the view represented by this node info.
+ *
+ * <p>
+ * <strong>Note:</strong> If this view hierarchy has a {@link SurfaceView} embedding another
+ * view hierarchy via {@link SurfaceView#setChildSurfacePackage}, there is a limitation that
+ * this API won't be able to find the node for the view on the embedded view hierarchy. It's
+ * because views don't know about the embedded hierarchies. Instead, you could traverse all
+ * the children to find the node. Or, use {@link AccessibilityService#findFocus(int)} for
+ * {@link #FOCUS_ACCESSIBILITY} only since it has no such limitation.
+ * </p>
+ *
+ * @param focus The focus to find. One of {@link #FOCUS_INPUT} or
+ * {@link #FOCUS_ACCESSIBILITY}.
+ * @return The node info of the focused view or null.
+ *
+ * @see #FOCUS_INPUT
+ * @see #FOCUS_ACCESSIBILITY
+ */
+ public AccessibilityNodeInfo findFocus(int focus) {
+ enforceSealed();
+ enforceValidFocusType(focus);
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return null;
+ }
+ return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, mWindowId,
+ mSourceNodeId, focus);
+ }
+
+ /**
+ * Searches for the nearest view in the specified direction that can take
+ * the input focus.
+ *
+ * <p>
+ * <strong>Note:</strong> If this view hierarchy has a {@link SurfaceView} embedding another
+ * view hierarchy via {@link SurfaceView#setChildSurfacePackage}, there is a limitation that
+ * this API won't be able to find the node for the view in the specified direction on the
+ * embedded view hierarchy. It's because views don't know about the embedded hierarchies.
+ * Instead, you could traverse all the children to find the node.
+ * </p>
+ *
+ * @param direction The direction. Can be one of:
+ * {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_UP},
+ * {@link View#FOCUS_LEFT},
+ * {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_FORWARD},
+ * {@link View#FOCUS_BACKWARD}.
+ *
+ * @return The node info for the view that can take accessibility focus.
+ */
+ public AccessibilityNodeInfo focusSearch(int direction) {
+ enforceSealed();
+ enforceValidFocusDirection(direction);
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return null;
+ }
+ return AccessibilityInteractionClient.getInstance().focusSearch(mConnectionId, mWindowId,
+ mSourceNodeId, direction);
+ }
+
+ /**
+ * Gets the id of the window from which the info comes from.
+ *
+ * @return The window id.
+ */
+ public int getWindowId() {
+ return mWindowId;
+ }
+
+ /**
+ * Refreshes this info with the latest state of the view it represents.
+ * <p>
+ * <strong>Note:</strong> If this method returns false this info is obsolete
+ * since it represents a view that is no longer in the view tree and should
+ * be recycled.
+ * </p>
+ *
+ * @param bypassCache Whether to bypass the cache.
+ * @return Whether the refresh succeeded.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean refresh(Bundle arguments, boolean bypassCache) {
+ enforceSealed();
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return false;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ AccessibilityNodeInfo refreshedInfo = client.findAccessibilityNodeInfoByAccessibilityId(
+ mConnectionId, mWindowId, mSourceNodeId, bypassCache, 0, arguments);
+ if (refreshedInfo == null) {
+ return false;
+ }
+ // Hard-to-reproduce bugs seem to be due to some tools recycling a node on another
+ // thread. If that happens, the init will re-seal the node, which then is in a bad state
+ // when it is obtained. Enforce sealing again before we init to fail when a node has been
+ // recycled during a refresh to catch such errors earlier.
+ enforceSealed();
+ init(refreshedInfo, true /* usePoolingInfo */);
+ refreshedInfo.recycle();
+ return true;
+ }
+
+ /**
+ * Refreshes this info with the latest state of the view it represents.
+ *
+ * @return {@code true} if the refresh succeeded. {@code false} if the {@link View} represented
+ * by this node is no longer in the view tree (and thus this node is obsolete and should be
+ * recycled).
+ */
+ public boolean refresh() {
+ return refresh(null, true);
+ }
+
+ /**
+ * Refreshes this info with the latest state of the view it represents, and request new
+ * data be added by the View.
+ *
+ * @param extraDataKey The extra data requested. Data that must be requested
+ * with this mechanism is generally expensive to retrieve, so should only be
+ * requested when needed. See
+ * {@link #EXTRA_DATA_RENDERING_INFO_KEY},
+ * {@link #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY},
+ * {@link #getAvailableExtraData()} and {@link #getExtraRenderingInfo()}.
+ * @param args A bundle of arguments for the request. These depend on the particular request.
+ *
+ * @return {@code true} if the refresh succeeded. {@code false} if the {@link View} represented
+ * by this node is no longer in the view tree (and thus this node is obsolete and should be
+ * recycled).
+ */
+ public boolean refreshWithExtraData(String extraDataKey, Bundle args) {
+ // limits the text location length to make sure the rectangle array allocation avoids
+ // the binder transaction failure and OOM crash.
+ if (args.getInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, -1)
+ > EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH) {
+ args.putInt(EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH,
+ EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH);
+ }
+
+ args.putString(EXTRA_DATA_REQUESTED_KEY, extraDataKey);
+ return refresh(args, true);
+ }
+
+ /**
+ * Returns the array containing the IDs of this node's children.
+ *
+ * @hide
+ */
+ public LongArray getChildNodeIds() {
+ return mChildNodeIds;
+ }
+
+ /**
+ * Returns the id of the child at the specified index.
+ *
+ * @throws IndexOutOfBoundsException when index < 0 || index >=
+ * getChildCount()
+ * @hide
+ */
+ public long getChildId(int index) {
+ if (mChildNodeIds == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return mChildNodeIds.get(index);
+ }
+
+ /**
+ * Gets the number of children.
+ *
+ * @return The child count.
+ */
+ public int getChildCount() {
+ return mChildNodeIds == null ? 0 : mChildNodeIds.size();
+ }
+
+ /**
+ * Get the child at given index.
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ *
+ * @param index The child index.
+ * @return The child node.
+ *
+ * @throws IllegalStateException If called outside of an AccessibilityService.
+ *
+ */
+ public AccessibilityNodeInfo getChild(int index) {
+ enforceSealed();
+ if (mChildNodeIds == null) {
+ return null;
+ }
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return null;
+ }
+ final long childId = mChildNodeIds.get(index);
+ final AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ if (mLeashedChild != null && childId == LEASHED_NODE_ID) {
+ return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mLeashedChild,
+ ROOT_NODE_ID, false, FLAG_PREFETCH_DESCENDANTS, null);
+ }
+
+ return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mWindowId,
+ childId, false, FLAG_PREFETCH_DESCENDANTS, null);
+ }
+
+ /**
+ * Adds a child.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * Note that a view cannot be made its own child.
+ * </p>
+ *
+ * @param child The child.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void addChild(View child) {
+ addChildInternal(child, AccessibilityNodeProvider.HOST_VIEW_ID, true);
+ }
+
+ /**
+ * Adds a view root from leashed content as a child. This method is used to embedded another
+ * view hierarchy.
+ * <p>
+ * <strong>Note:</strong> Only one leashed child is permitted.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * Note that a view cannot be made its own child.
+ * </p>
+ *
+ * @param token The token to which a view root is added.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @hide
+ */
+ @TestApi
+ public void addChild(@NonNull IBinder token) {
+ enforceNotSealed();
+ if (token == null) {
+ return;
+ }
+ if (mChildNodeIds == null) {
+ mChildNodeIds = new LongArray();
+ }
+
+ mLeashedChild = token;
+ // Checking uniqueness.
+ // Since only one leashed child is permitted, skip adding ID if the ID already exists.
+ if (mChildNodeIds.indexOf(LEASHED_NODE_ID) >= 0) {
+ return;
+ }
+ mChildNodeIds.add(LEASHED_NODE_ID);
+ }
+
+ /**
+ * Unchecked version of {@link #addChild(View)} that does not verify
+ * uniqueness. For framework use only.
+ *
+ * @hide
+ */
+ public void addChildUnchecked(View child) {
+ addChildInternal(child, AccessibilityNodeProvider.HOST_VIEW_ID, false);
+ }
+
+ /**
+ * Removes a child. If the child was not previously added to the node,
+ * calling this method has no effect.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param child The child.
+ * @return true if the child was present
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public boolean removeChild(View child) {
+ return removeChild(child, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Removes a leashed child. If the child was not previously added to the node,
+ * calling this method has no effect.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param token The token of the leashed child
+ * @return true if the child was present
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @hide
+ */
+ public boolean removeChild(IBinder token) {
+ enforceNotSealed();
+ if (mChildNodeIds == null || mLeashedChild == null) {
+ return false;
+ }
+ if (!mLeashedChild.equals(token)) {
+ return false;
+ }
+ final int index = mChildNodeIds.indexOf(LEASHED_NODE_ID);
+ mLeashedChild = null;
+ if (index < 0) {
+ return false;
+ }
+ mChildNodeIds.remove(index);
+ return true;
+ }
+
+ /**
+ * Adds a virtual child which is a descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root
+ * is added as a child.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * Note that a view cannot be made its own child.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual child.
+ */
+ public void addChild(View root, int virtualDescendantId) {
+ addChildInternal(root, virtualDescendantId, true);
+ }
+
+ private void addChildInternal(View root, int virtualDescendantId, boolean checked) {
+ enforceNotSealed();
+ if (mChildNodeIds == null) {
+ mChildNodeIds = new LongArray();
+ }
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ if (childNodeId == mSourceNodeId) {
+ Log.e(TAG, "Rejecting attempt to make a View its own child");
+ return;
+ }
+
+ // If we're checking uniqueness and the ID already exists, abort.
+ if (checked && mChildNodeIds.indexOf(childNodeId) >= 0) {
+ return;
+ }
+ mChildNodeIds.add(childNodeId);
+ }
+
+ /**
+ * Removes a virtual child which is a descendant of the given
+ * <code>root</code>. If the child was not previously added to the node,
+ * calling this method has no effect.
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual child.
+ * @return true if the child was present
+ * @see #addChild(View, int)
+ */
+ public boolean removeChild(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ final LongArray childIds = mChildNodeIds;
+ if (childIds == null) {
+ return false;
+ }
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ final int index = childIds.indexOf(childNodeId);
+ if (index < 0) {
+ return false;
+ }
+ childIds.remove(index);
+ return true;
+ }
+
+ /**
+ * Gets the actions that can be performed on the node.
+ */
+ public List<AccessibilityAction> getActionList() {
+ return CollectionUtils.emptyIfNull(mActions);
+ }
+
+ /**
+ * Gets the actions that can be performed on the node.
+ *
+ * @return The bit mask of with actions.
+ *
+ * @see AccessibilityNodeInfo#ACTION_FOCUS
+ * @see AccessibilityNodeInfo#ACTION_CLEAR_FOCUS
+ * @see AccessibilityNodeInfo#ACTION_SELECT
+ * @see AccessibilityNodeInfo#ACTION_CLEAR_SELECTION
+ * @see AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS
+ * @see AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS
+ * @see AccessibilityNodeInfo#ACTION_CLICK
+ * @see AccessibilityNodeInfo#ACTION_LONG_CLICK
+ * @see AccessibilityNodeInfo#ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+ * @see AccessibilityNodeInfo#ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
+ * @see AccessibilityNodeInfo#ACTION_NEXT_HTML_ELEMENT
+ * @see AccessibilityNodeInfo#ACTION_PREVIOUS_HTML_ELEMENT
+ * @see AccessibilityNodeInfo#ACTION_SCROLL_FORWARD
+ * @see AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD
+ *
+ * @deprecated Use {@link #getActionList()}.
+ */
+ @Deprecated
+ public int getActions() {
+ int returnValue = 0;
+
+ if (mActions == null) {
+ return returnValue;
+ }
+
+ final int actionSize = mActions.size();
+ for (int i = 0; i < actionSize; i++) {
+ int actionId = mActions.get(i).getId();
+ if (actionId <= LAST_LEGACY_STANDARD_ACTION) {
+ returnValue |= actionId;
+ }
+ }
+
+ return returnValue;
+ }
+
+ /**
+ * Adds an action that can be performed on the node.
+ * <p>
+ * To add a standard action use the static constants on {@link AccessibilityAction}.
+ * To add a custom action create a new {@link AccessibilityAction} by passing in a
+ * resource id from your application as the action id and an optional label that
+ * describes the action. To override one of the standard actions use as the action
+ * id of a standard action id such as {@link #ACTION_CLICK} and an optional label that
+ * describes the action.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param action The action.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void addAction(AccessibilityAction action) {
+ enforceNotSealed();
+
+ addActionUnchecked(action);
+ }
+
+ private void addActionUnchecked(AccessibilityAction action) {
+ if (action == null) {
+ return;
+ }
+
+ if (mActions == null) {
+ mActions = new ArrayList<>();
+ }
+
+ mActions.remove(action);
+ mActions.add(action);
+ }
+
+ /**
+ * Adds an action that can be performed on the node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param action The action.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @throws IllegalArgumentException If the argument is not one of the standard actions.
+ *
+ * @deprecated This has been deprecated for {@link #addAction(AccessibilityAction)}
+ */
+ @Deprecated
+ public void addAction(int action) {
+ enforceNotSealed();
+
+ if ((action & ACTION_TYPE_MASK) != 0) {
+ throw new IllegalArgumentException("Action is not a combination of the standard " +
+ "actions: " + action);
+ }
+
+ addStandardActions(action);
+ }
+
+ /**
+ * Removes an action that can be performed on the node. If the action was
+ * not already added to the node, calling this method has no effect.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param action The action to be removed.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @deprecated Use {@link #removeAction(AccessibilityAction)}
+ */
+ @Deprecated
+ public void removeAction(int action) {
+ enforceNotSealed();
+
+ removeAction(getActionSingleton(action));
+ }
+
+ /**
+ * Removes an action that can be performed on the node. If the action was
+ * not already added to the node, calling this method has no effect.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param action The action to be removed.
+ * @return The action removed from the list of actions.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public boolean removeAction(AccessibilityAction action) {
+ enforceNotSealed();
+
+ if (mActions == null || action == null) {
+ return false;
+ }
+
+ return mActions.remove(action);
+ }
+
+ /**
+ * Removes all actions.
+ *
+ * @hide
+ */
+ public void removeAllActions() {
+ if (mActions != null) {
+ mActions.clear();
+ }
+ }
+
+ /**
+ * Gets the node before which this one is visited during traversal. A screen-reader
+ * must visit the content of this node before the content of the one it precedes.
+ *
+ * @return The succeeding node if such or <code>null</code>.
+ *
+ * @see #setTraversalBefore(android.view.View)
+ * @see #setTraversalBefore(android.view.View, int)
+ */
+ public AccessibilityNodeInfo getTraversalBefore() {
+ enforceSealed();
+ return getNodeForAccessibilityId(mConnectionId, mWindowId, mTraversalBefore);
+ }
+
+ /**
+ * Sets the view before whose node this one should be visited during traversal. A
+ * screen-reader must visit the content of this node before the content of the one
+ * it precedes.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param view The view providing the preceding node.
+ *
+ * @see #getTraversalBefore()
+ */
+ public void setTraversalBefore(View view) {
+ setTraversalBefore(view, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the node before which this one is visited during traversal. A screen-reader
+ * must visit the content of this node before the content of the one it precedes.
+ * The successor is a virtual descendant of the given <code>root</code>. If
+ * <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root is set
+ * as the successor.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setTraversalBefore(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ final int rootAccessibilityViewId = (root != null)
+ ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ mTraversalBefore = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ }
+
+ /**
+ * Gets the node after which this one is visited in accessibility traversal.
+ * A screen-reader must visit the content of the other node before the content
+ * of this one.
+ *
+ * @return The succeeding node if such or <code>null</code>.
+ *
+ * @see #setTraversalAfter(android.view.View)
+ * @see #setTraversalAfter(android.view.View, int)
+ */
+ public AccessibilityNodeInfo getTraversalAfter() {
+ enforceSealed();
+ return getNodeForAccessibilityId(mConnectionId, mWindowId, mTraversalAfter);
+ }
+
+ /**
+ * Sets the view whose node is visited after this one in accessibility traversal.
+ * A screen-reader must visit the content of the other node before the content
+ * of this one.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param view The previous view.
+ *
+ * @see #getTraversalAfter()
+ */
+ public void setTraversalAfter(View view) {
+ setTraversalAfter(view, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the node after which this one is visited in accessibility traversal.
+ * A screen-reader must visit the content of the other node before the content
+ * of this one. If <code>virtualDescendantId</code> equals to {@link View#NO_ID}
+ * the root is set as the predecessor.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setTraversalAfter(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ final int rootAccessibilityViewId = (root != null)
+ ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ mTraversalAfter = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ }
+
+ /**
+ * Get the extra data available for this node.
+ * <p>
+ * Some data that is useful for some accessibility services is expensive to compute, and would
+ * place undue overhead on apps to compute all the time. That data can be requested with
+ * {@link #refreshWithExtraData(String, Bundle)}.
+ *
+ * @return An unmodifiable list of keys corresponding to extra data that can be requested.
+ * @see #EXTRA_DATA_RENDERING_INFO_KEY
+ * @see #EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
+ */
+ public List<String> getAvailableExtraData() {
+ if (mExtraDataKeys != null) {
+ return Collections.unmodifiableList(mExtraDataKeys);
+ } else {
+ return EMPTY_LIST;
+ }
+ }
+
+ /**
+ * Set the extra data available for this node.
+ * <p>
+ * <strong>Note:</strong> When a {@code View} passes in a non-empty list, it promises that
+ * it will populate the node's extras with corresponding pieces of information in
+ * {@link View#addExtraDataToAccessibilityNodeInfo(AccessibilityNodeInfo, String, Bundle)}.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ *
+ * @param extraDataKeys A list of types of extra data that are available.
+ * @see #getAvailableExtraData()
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setAvailableExtraData(List<String> extraDataKeys) {
+ enforceNotSealed();
+ mExtraDataKeys = new ArrayList<>(extraDataKeys);
+ }
+
+ /**
+ * Sets the maximum text length, or -1 for no limit.
+ * <p>
+ * Typically used to indicate that an editable text field has a limit on
+ * the number of characters entered.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ *
+ * @param max The maximum text length.
+ * @see #getMaxTextLength()
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setMaxTextLength(int max) {
+ enforceNotSealed();
+ mMaxTextLength = max;
+ }
+
+ /**
+ * Returns the maximum text length for this node.
+ *
+ * @return The maximum text length, or -1 for no limit.
+ * @see #setMaxTextLength(int)
+ */
+ public int getMaxTextLength() {
+ return mMaxTextLength;
+ }
+
+ /**
+ * Sets the movement granularities for traversing the text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param granularities The bit mask with granularities.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setMovementGranularities(int granularities) {
+ enforceNotSealed();
+ mMovementGranularities = granularities;
+ }
+
+ /**
+ * Gets the movement granularities for traversing the text of this node.
+ *
+ * @return The bit mask with granularities.
+ */
+ public int getMovementGranularities() {
+ return mMovementGranularities;
+ }
+
+ /**
+ * Performs an action on the node.
+ * <p>
+ * <strong>Note:</strong> An action can be performed only if the request is made
+ * from an {@link android.accessibilityservice.AccessibilityService}.
+ * </p>
+ *
+ * @param action The action to perform.
+ * @return True if the action was performed.
+ *
+ * @throws IllegalStateException If called outside of an AccessibilityService.
+ */
+ public boolean performAction(int action) {
+ enforceSealed();
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return false;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ Bundle arguments = null;
+ if (mExtras != null) {
+ arguments = mExtras;
+ }
+ return client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId,
+ action, arguments);
+ }
+
+ /**
+ * Performs an action on the node.
+ * <p>
+ * <strong>Note:</strong> An action can be performed only if the request is made
+ * from an {@link android.accessibilityservice.AccessibilityService}.
+ * </p>
+ *
+ * @param action The action to perform.
+ * @param arguments A bundle with additional arguments.
+ * @return True if the action was performed.
+ *
+ * @throws IllegalStateException If called outside of an AccessibilityService.
+ */
+ public boolean performAction(int action, Bundle arguments) {
+ enforceSealed();
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return false;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId,
+ action, arguments);
+ }
+
+ /**
+ * Finds {@link AccessibilityNodeInfo}s by text. The match is case
+ * insensitive containment. The search is relative to this info i.e.
+ * this info is the root of the traversed tree.
+ *
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> If this view hierarchy has a {@link SurfaceView} embedding another
+ * view hierarchy via {@link SurfaceView#setChildSurfacePackage}, there is a limitation that
+ * this API won't be able to find the node for the view on the embedded view hierarchy. It's
+ * because views don't know about the embedded hierarchies. Instead, you could traverse all
+ * the children to find the node.
+ * </p>
+ *
+ * @param text The searched text.
+ * @return A list of node info.
+ */
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) {
+ enforceSealed();
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return Collections.emptyList();
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfosByText(mConnectionId, mWindowId, mSourceNodeId,
+ text);
+ }
+
+ /**
+ * Finds {@link AccessibilityNodeInfo}s by the fully qualified view id's resource
+ * name where a fully qualified id is of the from "package:id/id_resource_name".
+ * For example, if the target application's package is "foo.bar" and the id
+ * resource name is "baz", the fully qualified resource id is "foo.bar:id/baz".
+ *
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> The primary usage of this API is for UI test automation
+ * and in order to report the fully qualified view id if an {@link AccessibilityNodeInfo}
+ * the client has to set the {@link AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS}
+ * flag when configuring the {@link android.accessibilityservice.AccessibilityService}.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> If this view hierarchy has a {@link SurfaceView} embedding another
+ * view hierarchy via {@link SurfaceView#setChildSurfacePackage}, there is a limitation that
+ * this API won't be able to find the node for the view on the embedded view hierarchy. It's
+ * because views don't know about the embedded hierarchies. Instead, you could traverse all
+ * the children to find the node.
+ * </p>
+ *
+ * @param viewId The fully qualified resource name of the view id to find.
+ * @return A list of node info.
+ */
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(@NonNull String viewId) {
+ enforceSealed();
+ if (viewId == null) {
+ Log.e(TAG, "returns empty list due to null viewId.");
+ return Collections.emptyList();
+ }
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return Collections.emptyList();
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfosByViewId(mConnectionId, mWindowId, mSourceNodeId,
+ viewId);
+ }
+
+ /**
+ * Gets the window to which this node belongs.
+ *
+ * @return The window.
+ *
+ * @see android.accessibilityservice.AccessibilityService#getWindows()
+ */
+ public AccessibilityWindowInfo getWindow() {
+ enforceSealed();
+ if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
+ return null;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.getWindow(mConnectionId, mWindowId);
+ }
+
+ /**
+ * Gets the parent.
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ *
+ * @return The parent.
+ */
+ public AccessibilityNodeInfo getParent() {
+ enforceSealed();
+ if (mLeashedParent != null && mLeashedParentNodeId != UNDEFINED_NODE_ID) {
+ return getNodeForAccessibilityId(mConnectionId, mLeashedParent, mLeashedParentNodeId);
+ }
+ return getNodeForAccessibilityId(mConnectionId, mWindowId, mParentNodeId);
+ }
+
+ /**
+ * @return The parent node id.
+ *
+ * @hide
+ */
+ public long getParentNodeId() {
+ return mParentNodeId;
+ }
+
+ /**
+ * Sets the parent.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param parent The parent.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setParent(View parent) {
+ setParent(parent, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the parent to be a virtual descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root
+ * is set as the parent.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setParent(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ mParentNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ }
+
+ /**
+ * Gets the node bounds in the viewParent's coordinates.
+ * {@link #getParent()} does not represent the source's viewParent.
+ * Instead it represents the result of {@link View#getParentForAccessibility()},
+ * which returns the closest ancestor where {@link View#isImportantForAccessibility()} is true.
+ * So this method is not reliable.
+ * <p>
+ * When magnification is enabled, the bounds in parent are also scaled up by magnification
+ * scale. For example, it returns Rect(20, 20, 200, 200) for original bounds
+ * Rect(10, 10, 100, 100), when the magnification scale is 2.
+ * <p/>
+ *
+ * @param outBounds The output node bounds.
+ * @deprecated Use {@link #getBoundsInScreen(Rect)} instead.
+ *
+ */
+ @Deprecated
+ public void getBoundsInParent(Rect outBounds) {
+ outBounds.set(mBoundsInParent.left, mBoundsInParent.top,
+ mBoundsInParent.right, mBoundsInParent.bottom);
+ }
+
+ /**
+ * Sets the node bounds in the viewParent's coordinates.
+ * {@link #getParent()} does not represent the source's viewParent.
+ * Instead it represents the result of {@link View#getParentForAccessibility()},
+ * which returns the closest ancestor where {@link View#isImportantForAccessibility()} is true.
+ * So this method is not reliable.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param bounds The node bounds.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @deprecated Accessibility services should not care about these bounds.
+ */
+ @Deprecated
+ public void setBoundsInParent(Rect bounds) {
+ enforceNotSealed();
+ mBoundsInParent.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ /**
+ * Gets the node bounds in screen coordinates.
+ * <p>
+ * When magnification is enabled, the bounds in screen are scaled up by magnification scale
+ * and the positions are also adjusted according to the offset of magnification viewport.
+ * For example, it returns Rect(-180, -180, 0, 0) for original bounds Rect(10, 10, 100, 100),
+ * when the magnification scale is 2 and offsets for X and Y are both 200.
+ * <p/>
+ *
+ * @param outBounds The output node bounds.
+ */
+ public void getBoundsInScreen(Rect outBounds) {
+ outBounds.set(mBoundsInScreen.left, mBoundsInScreen.top,
+ mBoundsInScreen.right, mBoundsInScreen.bottom);
+ }
+
+ /**
+ * Returns the actual rect containing the node bounds in screen coordinates.
+ *
+ * @hide Not safe to expose outside the framework.
+ */
+ public Rect getBoundsInScreen() {
+ return mBoundsInScreen;
+ }
+
+ /**
+ * Sets the node bounds in screen coordinates.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param bounds The node bounds.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setBoundsInScreen(Rect bounds) {
+ enforceNotSealed();
+ mBoundsInScreen.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ /**
+ * Gets whether this node is checkable.
+ *
+ * @return True if the node is checkable.
+ */
+ public boolean isCheckable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_CHECKABLE);
+ }
+
+ /**
+ * Sets whether this node is checkable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param checkable True if the node is checkable.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setCheckable(boolean checkable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_CHECKABLE, checkable);
+ }
+
+ /**
+ * Gets whether this node is checked.
+ *
+ * @return True if the node is checked.
+ */
+ public boolean isChecked() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_CHECKED);
+ }
+
+ /**
+ * Sets whether this node is checked.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param checked True if the node is checked.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setChecked(boolean checked) {
+ setBooleanProperty(BOOLEAN_PROPERTY_CHECKED, checked);
+ }
+
+ /**
+ * Gets whether this node is focusable.
+ *
+ * @return True if the node is focusable.
+ */
+ public boolean isFocusable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSABLE);
+ }
+
+ /**
+ * Sets whether this node is focusable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param focusable True if the node is focusable.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setFocusable(boolean focusable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_FOCUSABLE, focusable);
+ }
+
+ /**
+ * Gets whether this node is focused.
+ *
+ * @return True if the node is focused.
+ */
+ public boolean isFocused() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED);
+ }
+
+ /**
+ * Sets whether this node is focused.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param focused True if the node is focused.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setFocused(boolean focused) {
+ setBooleanProperty(BOOLEAN_PROPERTY_FOCUSED, focused);
+ }
+
+ /**
+ * Gets whether this node is visible to the user.
+ * <p>
+ * Between {@link Build.VERSION_CODES#JELLY_BEAN API 16} and
+ * {@link Build.VERSION_CODES#Q API 29}, this method may incorrectly return false when
+ * magnification is enabled. On other versions, a node is considered visible even if it is not
+ * on the screen because magnification is active.
+ * </p>
+ *
+ * @return Whether the node is visible to the user.
+ */
+ public boolean isVisibleToUser() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_VISIBLE_TO_USER);
+ }
+
+ /**
+ * Sets whether this node is visible to the user.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param visibleToUser Whether the node is visible to the user.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setVisibleToUser(boolean visibleToUser) {
+ setBooleanProperty(BOOLEAN_PROPERTY_VISIBLE_TO_USER, visibleToUser);
+ }
+
+ /**
+ * Gets whether this node is accessibility focused.
+ *
+ * @return True if the node is accessibility focused.
+ */
+ public boolean isAccessibilityFocused() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED);
+ }
+
+ /**
+ * Sets whether this node is accessibility focused.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param focused True if the node is accessibility focused.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setAccessibilityFocused(boolean focused) {
+ setBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED, focused);
+ }
+
+ /**
+ * Gets whether this node is selected.
+ *
+ * @return True if the node is selected.
+ */
+ public boolean isSelected() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_SELECTED);
+ }
+
+ /**
+ * Sets whether this node is selected.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param selected True if the node is selected.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setSelected(boolean selected) {
+ setBooleanProperty(BOOLEAN_PROPERTY_SELECTED, selected);
+ }
+
+ /**
+ * Gets whether this node is clickable.
+ *
+ * @return True if the node is clickable.
+ */
+ public boolean isClickable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_CLICKABLE);
+ }
+
+ /**
+ * Sets whether this node is clickable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param clickable True if the node is clickable.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setClickable(boolean clickable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_CLICKABLE, clickable);
+ }
+
+ /**
+ * Gets whether this node is long clickable.
+ *
+ * @return True if the node is long clickable.
+ */
+ public boolean isLongClickable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_LONG_CLICKABLE);
+ }
+
+ /**
+ * Sets whether this node is long clickable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param longClickable True if the node is long clickable.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setLongClickable(boolean longClickable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_LONG_CLICKABLE, longClickable);
+ }
+
+ /**
+ * Gets whether this node is enabled.
+ *
+ * @return True if the node is enabled.
+ */
+ public boolean isEnabled() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_ENABLED);
+ }
+
+ /**
+ * Sets whether this node is enabled.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param enabled True if the node is enabled.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setEnabled(boolean enabled) {
+ setBooleanProperty(BOOLEAN_PROPERTY_ENABLED, enabled);
+ }
+
+ /**
+ * Gets whether this node is a password.
+ *
+ * @return True if the node is a password.
+ */
+ public boolean isPassword() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_PASSWORD);
+ }
+
+ /**
+ * Sets whether this node is a password.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param password True if the node is a password.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setPassword(boolean password) {
+ setBooleanProperty(BOOLEAN_PROPERTY_PASSWORD, password);
+ }
+
+ /**
+ * Gets if the node is scrollable.
+ *
+ * @return True if the node is scrollable, false otherwise.
+ */
+ public boolean isScrollable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_SCROLLABLE);
+ }
+
+ /**
+ * Sets if the node is scrollable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param scrollable True if the node is scrollable, false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setScrollable(boolean scrollable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_SCROLLABLE, scrollable);
+ }
+
+ /**
+ * Gets if the node is editable.
+ *
+ * @return True if the node is editable, false otherwise.
+ */
+ public boolean isEditable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_EDITABLE);
+ }
+
+ /**
+ * Sets whether this node is editable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param editable True if the node is editable.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setEditable(boolean editable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_EDITABLE, editable);
+ }
+
+ /**
+ * If this node represents a visually distinct region of the screen that may update separately
+ * from the rest of the window, it is considered a pane. Set the pane title to indicate that
+ * the node is a pane, and to provide a title for it.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ * @param paneTitle The title of the pane represented by this node.
+ */
+ public void setPaneTitle(@Nullable CharSequence paneTitle) {
+ enforceNotSealed();
+ mPaneTitle = (paneTitle == null)
+ ? null : paneTitle.subSequence(0, paneTitle.length());
+ }
+
+ /**
+ * Get the title of the pane represented by this node.
+ *
+ * @return The title of the pane represented by this node, or {@code null} if this node does
+ * not represent a pane.
+ */
+ public @Nullable CharSequence getPaneTitle() {
+ return mPaneTitle;
+ }
+
+ /**
+ * Get the drawing order of the view corresponding it this node.
+ * <p>
+ * Drawing order is determined only within the node's parent, so this index is only relative
+ * to its siblings.
+ * <p>
+ * In some cases, the drawing order is essentially simultaneous, so it is possible for two
+ * siblings to return the same value. It is also possible that values will be skipped.
+ *
+ * @return The drawing position of the view corresponding to this node relative to its siblings.
+ */
+ public int getDrawingOrder() {
+ return mDrawingOrderInParent;
+ }
+
+ /**
+ * Set the drawing order of the view corresponding it this node.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ * @param drawingOrderInParent
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setDrawingOrder(int drawingOrderInParent) {
+ enforceNotSealed();
+ mDrawingOrderInParent = drawingOrderInParent;
+ }
+
+ /**
+ * Gets the collection info if the node is a collection. A collection
+ * child is always a collection item.
+ *
+ * @return The collection info.
+ */
+ public CollectionInfo getCollectionInfo() {
+ return mCollectionInfo;
+ }
+
+ /**
+ * Sets the collection info if the node is a collection. A collection
+ * child is always a collection item.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param collectionInfo The collection info.
+ */
+ public void setCollectionInfo(CollectionInfo collectionInfo) {
+ enforceNotSealed();
+ mCollectionInfo = collectionInfo;
+ }
+
+ /**
+ * Gets the collection item info if the node is a collection item. A collection
+ * item is always a child of a collection.
+ *
+ * @return The collection item info.
+ */
+ public CollectionItemInfo getCollectionItemInfo() {
+ return mCollectionItemInfo;
+ }
+
+ /**
+ * Sets the collection item info if the node is a collection item. A collection
+ * item is always a child of a collection.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ */
+ public void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) {
+ enforceNotSealed();
+ mCollectionItemInfo = collectionItemInfo;
+ }
+
+ /**
+ * Gets the range info if this node is a range.
+ *
+ * @return The range.
+ */
+ public RangeInfo getRangeInfo() {
+ return mRangeInfo;
+ }
+
+ /**
+ * Sets the range info if this node is a range.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param rangeInfo The range info.
+ */
+ public void setRangeInfo(RangeInfo rangeInfo) {
+ enforceNotSealed();
+ mRangeInfo = rangeInfo;
+ }
+
+ /**
+ * Gets the {@link ExtraRenderingInfo extra rendering info} if the node is meant to be
+ * refreshed with extra data to examine rendering related accessibility issues.
+ *
+ * @return The {@link ExtraRenderingInfo extra rendering info}.
+ *
+ * @see #EXTRA_DATA_RENDERING_INFO_KEY
+ * @see #refreshWithExtraData(String, Bundle)
+ */
+ @Nullable
+ public ExtraRenderingInfo getExtraRenderingInfo() {
+ return mExtraRenderingInfo;
+ }
+
+ /**
+ * Sets the extra rendering info, <code>extraRenderingInfo<code/>, if the node is meant to be
+ * refreshed with extra data.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param extraRenderingInfo The {@link ExtraRenderingInfo extra rendering info}.
+ * @hide
+ */
+ public void setExtraRenderingInfo(@NonNull ExtraRenderingInfo extraRenderingInfo) {
+ enforceNotSealed();
+ mExtraRenderingInfo = extraRenderingInfo;
+ }
+
+ /**
+ * Gets if the content of this node is invalid. For example,
+ * a date is not well-formed.
+ *
+ * @return If the node content is invalid.
+ */
+ public boolean isContentInvalid() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_CONTENT_INVALID);
+ }
+
+ /**
+ * Sets if the content of this node is invalid. For example,
+ * a date is not well-formed.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param contentInvalid If the node content is invalid.
+ */
+ public void setContentInvalid(boolean contentInvalid) {
+ setBooleanProperty(BOOLEAN_PROPERTY_CONTENT_INVALID, contentInvalid);
+ }
+
+ /**
+ * Gets whether this node is context clickable.
+ *
+ * @return True if the node is context clickable.
+ */
+ public boolean isContextClickable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_CONTEXT_CLICKABLE);
+ }
+
+ /**
+ * Sets whether this node is context clickable.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}. This class is made immutable
+ * before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param contextClickable True if the node is context clickable.
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setContextClickable(boolean contextClickable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_CONTEXT_CLICKABLE, contextClickable);
+ }
+
+ /**
+ * Gets the node's live region mode.
+ * <p>
+ * A live region is a node that contains information that is important for
+ * the user and when it changes the user should be notified. For example,
+ * in a login screen with a TextView that displays an "incorrect password"
+ * notification, that view should be marked as a live region with mode
+ * {@link View#ACCESSIBILITY_LIVE_REGION_POLITE}.
+ * <p>
+ * It is the responsibility of the accessibility service to monitor
+ * {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} events indicating
+ * changes to live region nodes and their children.
+ *
+ * @return The live region mode, or
+ * {@link View#ACCESSIBILITY_LIVE_REGION_NONE} if the view is not a
+ * live region.
+ * @see android.view.View#getAccessibilityLiveRegion()
+ */
+ public int getLiveRegion() {
+ return mLiveRegion;
+ }
+
+ /**
+ * Sets the node's live region mode.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}. This class is
+ * made immutable before being delivered to an AccessibilityService.
+ *
+ * @param mode The live region mode, or
+ * {@link View#ACCESSIBILITY_LIVE_REGION_NONE} if the view is not a
+ * live region.
+ * @see android.view.View#setAccessibilityLiveRegion(int)
+ */
+ public void setLiveRegion(int mode) {
+ enforceNotSealed();
+ mLiveRegion = mode;
+ }
+
+ /**
+ * Gets if the node is a multi line editable text.
+ *
+ * @return True if the node is multi line.
+ */
+ public boolean isMultiLine() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_MULTI_LINE);
+ }
+
+ /**
+ * Sets if the node is a multi line editable text.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param multiLine True if the node is multi line.
+ */
+ public void setMultiLine(boolean multiLine) {
+ setBooleanProperty(BOOLEAN_PROPERTY_MULTI_LINE, multiLine);
+ }
+
+ /**
+ * Gets if this node opens a popup or a dialog.
+ *
+ * @return If the the node opens a popup.
+ */
+ public boolean canOpenPopup() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_OPENS_POPUP);
+ }
+
+ /**
+ * Sets if this node opens a popup or a dialog.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param opensPopup If the the node opens a popup.
+ */
+ public void setCanOpenPopup(boolean opensPopup) {
+ enforceNotSealed();
+ setBooleanProperty(BOOLEAN_PROPERTY_OPENS_POPUP, opensPopup);
+ }
+
+ /**
+ * Gets if the node can be dismissed.
+ *
+ * @return If the node can be dismissed.
+ */
+ public boolean isDismissable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_DISMISSABLE);
+ }
+
+ /**
+ * Sets if the node can be dismissed.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param dismissable If the node can be dismissed.
+ */
+ public void setDismissable(boolean dismissable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_DISMISSABLE, dismissable);
+ }
+
+ /**
+ * Returns whether the node originates from a view considered important for accessibility.
+ *
+ * @return {@code true} if the node originates from a view considered important for
+ * accessibility, {@code false} otherwise
+ *
+ * @see View#isImportantForAccessibility()
+ */
+ public boolean isImportantForAccessibility() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_IMPORTANCE);
+ }
+
+ /**
+ * Sets whether the node is considered important for accessibility.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param important {@code true} if the node is considered important for accessibility,
+ * {@code false} otherwise
+ */
+ public void setImportantForAccessibility(boolean important) {
+ setBooleanProperty(BOOLEAN_PROPERTY_IMPORTANCE, important);
+ }
+
+ /**
+ * Returns whether the node is explicitly marked as a focusable unit by a screen reader. Note
+ * that {@code false} indicates that it is not explicitly marked, not that the node is not
+ * a focusable unit. Screen readers should generally use other signals, such as
+ * {@link #isFocusable()}, or the presence of text in a node, to determine what should receive
+ * focus.
+ *
+ * @return {@code true} if the node is specifically marked as a focusable unit for screen
+ * readers, {@code false} otherwise.
+ *
+ * @see View#isScreenReaderFocusable()
+ */
+ public boolean isScreenReaderFocusable() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE);
+ }
+
+ /**
+ * Sets whether the node should be considered a focusable unit by a screen reader.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param screenReaderFocusable {@code true} if the node is a focusable unit for screen readers,
+ * {@code false} otherwise.
+ */
+ public void setScreenReaderFocusable(boolean screenReaderFocusable) {
+ setBooleanProperty(BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE, screenReaderFocusable);
+ }
+
+ /**
+ * Returns whether the node's text represents a hint for the user to enter text. It should only
+ * be {@code true} if the node has editable text.
+ *
+ * @return {@code true} if the text in the node represents a hint to the user, {@code false}
+ * otherwise.
+ */
+ public boolean isShowingHintText() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_IS_SHOWING_HINT);
+ }
+
+ /**
+ * Sets whether the node's text represents a hint for the user to enter text. It should only
+ * be {@code true} if the node has editable text.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param showingHintText {@code true} if the text in the node represents a hint to the user,
+ * {@code false} otherwise.
+ */
+ public void setShowingHintText(boolean showingHintText) {
+ setBooleanProperty(BOOLEAN_PROPERTY_IS_SHOWING_HINT, showingHintText);
+ }
+
+ /**
+ * Returns whether node represents a heading.
+ * <p><strong>Note:</strong> Returns {@code true} if either {@link #setHeading(boolean)}
+ * marks this node as a heading or if the node has a {@link CollectionItemInfo} that marks
+ * it as such, to accomodate apps that use the now-deprecated API.</p>
+ *
+ * @return {@code true} if the node is a heading, {@code false} otherwise.
+ */
+ public boolean isHeading() {
+ if (getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING)) return true;
+ CollectionItemInfo itemInfo = getCollectionItemInfo();
+ return ((itemInfo != null) && itemInfo.mHeading);
+ }
+
+ /**
+ * Sets whether the node represents a heading.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param isHeading {@code true} if the node is a heading, {@code false} otherwise.
+ */
+ public void setHeading(boolean isHeading) {
+ setBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING, isHeading);
+ }
+
+ /**
+ * Returns whether node represents a text entry key that is part of a keyboard or keypad.
+ *
+ * @return {@code true} if the node is a text entry key., {@code false} otherwise.
+ */
+ public boolean isTextEntryKey() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_IS_TEXT_ENTRY_KEY);
+ }
+
+ /**
+ * Sets whether the node represents a text entry key that is part of a keyboard or keypad.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param isTextEntryKey {@code true} if the node is a text entry key, {@code false} otherwise.
+ */
+ public void setTextEntryKey(boolean isTextEntryKey) {
+ setBooleanProperty(BOOLEAN_PROPERTY_IS_TEXT_ENTRY_KEY, isTextEntryKey);
+ }
+
+ /**
+ * Gets the package this node comes from.
+ *
+ * @return The package name.
+ */
+ public CharSequence getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Sets the package this node comes from.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param packageName The package name.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setPackageName(CharSequence packageName) {
+ enforceNotSealed();
+ mPackageName = packageName;
+ }
+
+ /**
+ * Gets the class this node comes from.
+ *
+ * @return The class name.
+ */
+ public CharSequence getClassName() {
+ return mClassName;
+ }
+
+ /**
+ * Sets the class this node comes from.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param className The class name.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setClassName(CharSequence className) {
+ enforceNotSealed();
+ mClassName = className;
+ }
+
+ /**
+ * Gets the text of this node.
+ * <p>
+ * <strong>Note:</strong> If the text contains {@link ClickableSpan}s or {@link URLSpan}s,
+ * these spans will have been replaced with ones whose {@link ClickableSpan#onClick(View)}
+ * can be called from an {@link AccessibilityService}. When called from a service, the
+ * {@link View} argument is ignored and the corresponding span will be found on the view that
+ * this {@code AccessibilityNodeInfo} represents and called with that view as its argument.
+ * <p>
+ * This treatment of {@link ClickableSpan}s means that the text returned from this method may
+ * different slightly one passed to {@link #setText(CharSequence)}, although they will be
+ * equivalent according to {@link TextUtils#equals(CharSequence, CharSequence)}. The
+ * {@link ClickableSpan#onClick(View)} of any spans, however, will generally not work outside
+ * of an accessibility service.
+ * </p>
+ *
+ * @return The text.
+ */
+ public CharSequence getText() {
+ // Attach this node to any spans that need it
+ if (mText instanceof Spanned) {
+ Spanned spanned = (Spanned) mText;
+ AccessibilityClickableSpan[] clickableSpans =
+ spanned.getSpans(0, mText.length(), AccessibilityClickableSpan.class);
+ for (int i = 0; i < clickableSpans.length; i++) {
+ clickableSpans[i].copyConnectionDataFrom(this);
+ }
+ AccessibilityURLSpan[] urlSpans =
+ spanned.getSpans(0, mText.length(), AccessibilityURLSpan.class);
+ for (int i = 0; i < urlSpans.length; i++) {
+ urlSpans[i].copyConnectionDataFrom(this);
+ }
+ }
+ return mText;
+ }
+
+ /**
+ * Get the text passed to setText before any changes to the spans.
+ * @hide
+ */
+ public CharSequence getOriginalText() {
+ return mOriginalText;
+ }
+
+ /**
+ * Sets the text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param text The text.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setText(CharSequence text) {
+ enforceNotSealed();
+ mOriginalText = text;
+ if (text instanceof Spanned) {
+ CharSequence tmpText = text;
+ tmpText = replaceClickableSpan(tmpText);
+ tmpText = replaceReplacementSpan(tmpText);
+ mText = tmpText;
+ return;
+ }
+ mText = (text == null) ? null : text.subSequence(0, text.length());
+ }
+
+ /**
+ * Replaces any ClickableSpan in the given {@code text} with placeholders.
+ *
+ * @param text The text.
+ *
+ * @return The spannable with ClickableSpan replacement.
+ */
+ private CharSequence replaceClickableSpan(CharSequence text) {
+ ClickableSpan[] clickableSpans =
+ ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class);
+ Spannable spannable = new SpannableStringBuilder(text);
+ if (clickableSpans.length == 0) {
+ return text;
+ }
+ for (int i = 0; i < clickableSpans.length; i++) {
+ ClickableSpan span = clickableSpans[i];
+ if ((span instanceof AccessibilityClickableSpan)
+ || (span instanceof AccessibilityURLSpan)) {
+ // We've already done enough
+ break;
+ }
+ int spanToReplaceStart = spannable.getSpanStart(span);
+ int spanToReplaceEnd = spannable.getSpanEnd(span);
+ int spanToReplaceFlags = spannable.getSpanFlags(span);
+ spannable.removeSpan(span);
+ ClickableSpan replacementSpan = (span instanceof URLSpan)
+ ? new AccessibilityURLSpan((URLSpan) span)
+ : new AccessibilityClickableSpan(span.getId());
+ spannable.setSpan(replacementSpan, spanToReplaceStart, spanToReplaceEnd,
+ spanToReplaceFlags);
+ }
+ return spannable;
+ }
+
+ /**
+ * Replaces any ReplacementSpan in the given {@code text} if the object has content description.
+ *
+ * @param text The text.
+ *
+ * @return The spannable with ReplacementSpan replacement.
+ */
+ private CharSequence replaceReplacementSpan(CharSequence text) {
+ ReplacementSpan[] replacementSpans =
+ ((Spanned) text).getSpans(0, text.length(), ReplacementSpan.class);
+ SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+ if (replacementSpans.length == 0) {
+ return text;
+ }
+ for (int i = 0; i < replacementSpans.length; i++) {
+ ReplacementSpan span = replacementSpans[i];
+ CharSequence replacementText = span.getContentDescription();
+ if (span instanceof AccessibilityReplacementSpan) {
+ // We've already done enough
+ break;
+ }
+ if (replacementText == null) {
+ continue;
+ }
+ int spanToReplaceStart = spannable.getSpanStart(span);
+ int spanToReplaceEnd = spannable.getSpanEnd(span);
+ int spanToReplaceFlags = spannable.getSpanFlags(span);
+ spannable.removeSpan(span);
+ ReplacementSpan replacementSpan = new AccessibilityReplacementSpan(replacementText);
+ spannable.setSpan(replacementSpan, spanToReplaceStart, spanToReplaceEnd,
+ spanToReplaceFlags);
+ }
+ return spannable;
+ }
+
+ /**
+ * Gets the hint text of this node. Only applies to nodes where text can be entered.
+ *
+ * @return The hint text.
+ */
+ public CharSequence getHintText() {
+ return mHintText;
+ }
+
+ /**
+ * Sets the hint text of this node. Only applies to nodes where text can be entered.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param hintText The hint text for this mode.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setHintText(CharSequence hintText) {
+ enforceNotSealed();
+ mHintText = (hintText == null) ? null : hintText.subSequence(0, hintText.length());
+ }
+
+ /**
+ * Sets the error text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param error The error text.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setError(CharSequence error) {
+ enforceNotSealed();
+ mError = (error == null) ? null : error.subSequence(0, error.length());
+ }
+
+ /**
+ * Gets the error text of this node.
+ *
+ * @return The error text.
+ */
+ public CharSequence getError() {
+ return mError;
+ }
+
+ /**
+ * Get the state description of this node.
+ *
+ * @return the state description
+ */
+ public @Nullable CharSequence getStateDescription() {
+ return mStateDescription;
+ }
+
+ /**
+ * Gets the content description of this node.
+ *
+ * @return The content description.
+ */
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+
+ /**
+ * Sets the state description of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param stateDescription the state description of this node.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setStateDescription(@Nullable CharSequence stateDescription) {
+ enforceNotSealed();
+ mStateDescription = (stateDescription == null) ? null
+ : stateDescription.subSequence(0, stateDescription.length());
+ }
+
+ /**
+ * Sets the content description of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param contentDescription The content description.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setContentDescription(CharSequence contentDescription) {
+ enforceNotSealed();
+ mContentDescription = (contentDescription == null) ? null
+ : contentDescription.subSequence(0, contentDescription.length());
+ }
+
+ /**
+ * Gets the tooltip text of this node.
+ *
+ * @return The tooltip text.
+ */
+ @Nullable
+ public CharSequence getTooltipText() {
+ return mTooltipText;
+ }
+
+ /**
+ * Sets the tooltip text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param tooltipText The tooltip text.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setTooltipText(@Nullable CharSequence tooltipText) {
+ enforceNotSealed();
+ mTooltipText = (tooltipText == null) ? null
+ : tooltipText.subSequence(0, tooltipText.length());
+ }
+
+ /**
+ * Sets the view for which the view represented by this info serves as a
+ * label for accessibility purposes.
+ *
+ * @param labeled The view for which this info serves as a label.
+ */
+ public void setLabelFor(View labeled) {
+ setLabelFor(labeled, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the view for which the view represented by this info serves as a
+ * label for accessibility purposes. If <code>virtualDescendantId</code>
+ * is {@link View#NO_ID} the root is set as the labeled.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report themselves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root whose virtual descendant serves as a label.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setLabelFor(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ final int rootAccessibilityViewId = (root != null)
+ ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ mLabelForId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ }
+
+ /**
+ * Gets the node info for which the view represented by this info serves as
+ * a label for accessibility purposes.
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ *
+ * @return The labeled info.
+ */
+ public AccessibilityNodeInfo getLabelFor() {
+ enforceSealed();
+ return getNodeForAccessibilityId(mConnectionId, mWindowId, mLabelForId);
+ }
+
+ /**
+ * Sets the view which serves as the label of the view represented by
+ * this info for accessibility purposes.
+ *
+ * @param label The view that labels this node's source.
+ */
+ public void setLabeledBy(View label) {
+ setLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the view which serves as the label of the view represented by
+ * this info for accessibility purposes. If <code>virtualDescendantId</code>
+ * is {@link View#NO_ID} the root is set as the label.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report themselves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root whose virtual descendant labels this node's source.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setLabeledBy(View root, int virtualDescendantId) {
+ enforceNotSealed();
+ final int rootAccessibilityViewId = (root != null)
+ ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+ mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ }
+
+ /**
+ * Gets the node info which serves as the label of the view represented by
+ * this info for accessibility purposes.
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ *
+ * @return The label.
+ */
+ public AccessibilityNodeInfo getLabeledBy() {
+ enforceSealed();
+ return getNodeForAccessibilityId(mConnectionId, mWindowId, mLabeledById);
+ }
+
+ /**
+ * Sets the fully qualified resource name of the source view's id.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param viewIdResName The id resource name.
+ */
+ public void setViewIdResourceName(String viewIdResName) {
+ enforceNotSealed();
+ mViewIdResourceName = viewIdResName;
+ }
+
+ /**
+ * Gets the fully qualified resource name of the source view's id.
+ *
+ * <p>
+ * <strong>Note:</strong> The primary usage of this API is for UI test automation
+ * and in order to report the source view id of an {@link AccessibilityNodeInfo} the
+ * client has to set the {@link AccessibilityServiceInfo#FLAG_REPORT_VIEW_IDS}
+ * flag when configuring the {@link android.accessibilityservice.AccessibilityService}.
+ * </p>
+
+ * @return The id resource name.
+ */
+ public String getViewIdResourceName() {
+ return mViewIdResourceName;
+ }
+
+ /**
+ * Gets the text selection start or the cursor position.
+ * <p>
+ * If no text is selected, both this method and
+ * {@link AccessibilityNodeInfo#getTextSelectionEnd()} return the same value:
+ * the current location of the cursor.
+ * </p>
+ *
+ * @return The text selection start, the cursor location if there is no selection, or -1 if
+ * there is no text selection and no cursor.
+ */
+ public int getTextSelectionStart() {
+ return mTextSelectionStart;
+ }
+
+ /**
+ * Gets the text selection end if text is selected.
+ * <p>
+ * If no text is selected, both this method and
+ * {@link AccessibilityNodeInfo#getTextSelectionStart()} return the same value:
+ * the current location of the cursor.
+ * </p>
+ *
+ * @return The text selection end, the cursor location if there is no selection, or -1 if
+ * there is no text selection and no cursor.
+ */
+ public int getTextSelectionEnd() {
+ return mTextSelectionEnd;
+ }
+
+ /**
+ * Sets the text selection start and end.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param start The text selection start.
+ * @param end The text selection end.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setTextSelection(int start, int end) {
+ enforceNotSealed();
+ mTextSelectionStart = start;
+ mTextSelectionEnd = end;
+ }
+
+ /**
+ * Gets the input type of the source as defined by {@link InputType}.
+ *
+ * @return The input type.
+ */
+ public int getInputType() {
+ return mInputType;
+ }
+
+ /**
+ * Sets the input type of the source as defined by {@link InputType}.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an
+ * AccessibilityService.
+ * </p>
+ *
+ * @param inputType The input type.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setInputType(int inputType) {
+ enforceNotSealed();
+ mInputType = inputType;
+ }
+
+ /**
+ * Gets an optional bundle with extra data. The bundle
+ * is lazily created and never <code>null</code>.
+ * <p>
+ * <strong>Note:</strong> It is recommended to use the package
+ * name of your application as a prefix for the keys to avoid
+ * collisions which may confuse an accessibility service if the
+ * same key has different meaning when emitted from different
+ * applications.
+ * </p>
+ *
+ * @return The bundle.
+ */
+ public Bundle getExtras() {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ return mExtras;
+ }
+
+ /**
+ * Check if a node has an extras bundle
+ * @hide
+ */
+ public boolean hasExtras() {
+ return mExtras != null;
+ }
+
+ /**
+ * Get the {@link TouchDelegateInfo} for touch delegate behavior with the represented view.
+ * It is possible for the same node to be pointed to by several regions. Use
+ * {@link TouchDelegateInfo#getRegionAt(int)} to get touch delegate target {@link Region}, and
+ * {@link TouchDelegateInfo#getTargetForRegion(Region)} for {@link AccessibilityNodeInfo} from
+ * the given region.
+ *
+ * @return {@link TouchDelegateInfo} or {@code null} if there are no touch delegates.
+ */
+ @Nullable
+ public TouchDelegateInfo getTouchDelegateInfo() {
+ if (mTouchDelegateInfo != null) {
+ mTouchDelegateInfo.setConnectionId(mConnectionId);
+ mTouchDelegateInfo.setWindowId(mWindowId);
+ }
+ return mTouchDelegateInfo;
+ }
+
+ /**
+ * Set touch delegate info if the represented view has a {@link TouchDelegate}.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an
+ * AccessibilityService.
+ * </p>
+ *
+ * @param delegatedInfo {@link TouchDelegateInfo} returned from
+ * {@link TouchDelegate#getTouchDelegateInfo()}.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setTouchDelegateInfo(@NonNull TouchDelegateInfo delegatedInfo) {
+ enforceNotSealed();
+ mTouchDelegateInfo = delegatedInfo;
+ }
+
+ /**
+ * Gets the value of a boolean property.
+ *
+ * @param property The property.
+ * @return The value.
+ */
+ private boolean getBooleanProperty(int property) {
+ return (mBooleanProperties & property) != 0;
+ }
+
+ /**
+ * Sets a boolean property.
+ *
+ * @param property The property.
+ * @param value The value.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ private void setBooleanProperty(int property, boolean value) {
+ enforceNotSealed();
+ if (value) {
+ mBooleanProperties |= property;
+ } else {
+ mBooleanProperties &= ~property;
+ }
+ }
+
+ /**
+ * Sets the unique id of the IAccessibilityServiceConnection over which
+ * this instance can send requests to the system.
+ *
+ * @param connectionId The connection id.
+ *
+ * @hide
+ */
+ public void setConnectionId(int connectionId) {
+ enforceNotSealed();
+ mConnectionId = connectionId;
+ }
+
+ /**
+ * Get the connection ID.
+ *
+ * @return The connection id
+ *
+ * @hide
+ */
+ public int getConnectionId() {
+ return mConnectionId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Sets the id of the source node.
+ *
+ * @param sourceId The id.
+ * @param windowId The window id.
+ *
+ * @hide
+ */
+ public void setSourceNodeId(long sourceId, int windowId) {
+ enforceNotSealed();
+ mSourceNodeId = sourceId;
+ mWindowId = windowId;
+ }
+
+ /**
+ * Gets the id of the source node.
+ *
+ * @return The id.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @TestApi
+ public long getSourceNodeId() {
+ return mSourceNodeId;
+ }
+
+ /**
+ * Sets the token and node id of the leashed parent.
+ *
+ * @param token The token.
+ * @param viewId The accessibility view id.
+ * @hide
+ */
+ @TestApi
+ public void setLeashedParent(@Nullable IBinder token, int viewId) {
+ enforceNotSealed();
+ mLeashedParent = token;
+ mLeashedParentNodeId = makeNodeId(viewId, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Gets the token of the leashed parent.
+ *
+ * @return The token.
+ * @hide
+ */
+ public @Nullable IBinder getLeashedParent() {
+ return mLeashedParent;
+ }
+
+ /**
+ * Gets the node id of the leashed parent.
+ *
+ * @return The accessibility node id.
+ * @hide
+ */
+ public long getLeashedParentNodeId() {
+ return mLeashedParentNodeId;
+ }
+
+ /**
+ * Sets if this instance is sealed.
+ *
+ * @param sealed Whether is sealed.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setSealed(boolean sealed) {
+ mSealed = sealed;
+ }
+
+ /**
+ * Gets if this instance is sealed.
+ *
+ * @return Whether is sealed.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean isSealed() {
+ return mSealed;
+ }
+
+ /**
+ * Enforces that this instance is sealed.
+ *
+ * @throws IllegalStateException If this instance is not sealed.
+ *
+ * @hide
+ */
+ protected void enforceSealed() {
+ if (!isSealed()) {
+ throw new IllegalStateException("Cannot perform this "
+ + "action on a not sealed instance.");
+ }
+ }
+
+ private void enforceValidFocusDirection(int direction) {
+ switch (direction) {
+ case View.FOCUS_DOWN:
+ case View.FOCUS_UP:
+ case View.FOCUS_LEFT:
+ case View.FOCUS_RIGHT:
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_BACKWARD:
+ return;
+ default:
+ throw new IllegalArgumentException("Unknown direction: " + direction);
+ }
+ }
+
+ private void enforceValidFocusType(int focusType) {
+ switch (focusType) {
+ case FOCUS_INPUT:
+ case FOCUS_ACCESSIBILITY:
+ return;
+ default:
+ throw new IllegalArgumentException("Unknown focus type: " + focusType);
+ }
+ }
+
+ /**
+ * Enforces that this instance is not sealed.
+ *
+ * @throws IllegalStateException If this instance is sealed.
+ *
+ * @hide
+ */
+ protected void enforceNotSealed() {
+ if (isSealed()) {
+ throw new IllegalStateException("Cannot perform this "
+ + "action on a sealed instance.");
+ }
+ }
+
+ /**
+ * Returns a cached instance if such is available otherwise a new one
+ * and sets the source.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityNodeInfo(View)} instead.
+ *
+ * @param source The source view.
+ * @return An instance.
+ *
+ * @see #setSource(View)
+ */
+ public static AccessibilityNodeInfo obtain(View source) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setSource(source);
+ return info;
+ }
+
+ /**
+ * Returns a cached instance if such is available otherwise a new one
+ * and sets the source.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityNodeInfo(View, int)} instead.
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ * @return An instance.
+ *
+ * @see #setSource(View, int)
+ */
+ public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setSource(root, virtualDescendantId);
+ return info;
+ }
+
+ /**
+ * Returns a cached instance if such is available otherwise a new one.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityNodeInfo()} instead.
+ *
+ * @return An instance.
+ */
+ public static AccessibilityNodeInfo obtain() {
+ AccessibilityNodeInfo info = sPool.acquire();
+ if (sNumInstancesInUse != null) {
+ sNumInstancesInUse.incrementAndGet();
+ }
+ return (info != null) ? info : new AccessibilityNodeInfo();
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * create. The returned instance is initialized from the given
+ * <code>info</code>.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityNodeInfo(AccessibilityNodeInfo)} instead.
+ *
+ * @param info The other info.
+ * @return An instance.
+ */
+ public static AccessibilityNodeInfo obtain(AccessibilityNodeInfo info) {
+ AccessibilityNodeInfo infoClone = AccessibilityNodeInfo.obtain();
+ infoClone.init(info, true /* usePoolingInfo */);
+ return infoClone;
+ }
+
+ /**
+ * Return an instance back to be reused.
+ * <p>
+ * <strong>Note:</strong> You must not touch the object after calling this function.
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ *
+ * @throws IllegalStateException If the info is already recycled.
+ */
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ if (sNumInstancesInUse != null) {
+ sNumInstancesInUse.decrementAndGet();
+ }
+ }
+
+ /**
+ * Specify a counter that will be incremented on obtain() and decremented on recycle()
+ *
+ * @hide
+ */
+ @TestApi
+ public static void setNumInstancesInUseCounter(AtomicInteger counter) {
+ sNumInstancesInUse = counter;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <strong>Note:</strong> After the instance is written to a parcel it
+ * is recycled. You must not touch the object after calling this function.
+ * </p>
+ */
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ writeToParcelNoRecycle(parcel, flags);
+ // Since instances of this class are fetched via synchronous i.e. blocking
+ // calls in IPCs we always recycle as soon as the instance is marshaled.
+ recycle();
+ }
+
+ /** @hide */
+ @TestApi
+ public void writeToParcelNoRecycle(Parcel parcel, int flags) {
+ // Write bit set of indices of fields with values differing from default
+ long nonDefaultFields = 0;
+ int fieldIndex = 0; // index of the current field
+ if (isSealed() != DEFAULT.isSealed()) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mSourceNodeId != DEFAULT.mSourceNodeId) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mWindowId != DEFAULT.mWindowId) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mParentNodeId != DEFAULT.mParentNodeId) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mLabelForId != DEFAULT.mLabelForId) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mLabeledById != DEFAULT.mLabeledById) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mTraversalBefore != DEFAULT.mTraversalBefore) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mConnectionId != DEFAULT.mConnectionId) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!LongArray.elementsEqual(mChildNodeIds, DEFAULT.mChildNodeIds)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mBoundsInParent, DEFAULT.mBoundsInParent)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mBoundsInScreen, DEFAULT.mBoundsInScreen)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mActions, DEFAULT.mActions)) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mMaxTextLength != DEFAULT.mMaxTextLength) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mMovementGranularities != DEFAULT.mMovementGranularities) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mBooleanProperties != DEFAULT.mBooleanProperties) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!Objects.equals(mPackageName, DEFAULT.mPackageName)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mClassName, DEFAULT.mClassName)) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!Objects.equals(mText, DEFAULT.mText)) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!Objects.equals(mHintText, DEFAULT.mHintText)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mError, DEFAULT.mError)) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!Objects.equals(mStateDescription, DEFAULT.mStateDescription)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mContentDescription, DEFAULT.mContentDescription)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mPaneTitle, DEFAULT.mPaneTitle)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mTooltipText, DEFAULT.mTooltipText)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mViewIdResourceName, DEFAULT.mViewIdResourceName)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mTextSelectionStart != DEFAULT.mTextSelectionStart) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mTextSelectionEnd != DEFAULT.mTextSelectionEnd) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mInputType != DEFAULT.mInputType) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mLiveRegion != DEFAULT.mLiveRegion) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (mDrawingOrderInParent != DEFAULT.mDrawingOrderInParent) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mExtraDataKeys, DEFAULT.mExtraDataKeys)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mExtras, DEFAULT.mExtras)) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!Objects.equals(mRangeInfo, DEFAULT.mRangeInfo)) nonDefaultFields |= bitAt(fieldIndex);
+ fieldIndex++;
+ if (!Objects.equals(mCollectionInfo, DEFAULT.mCollectionInfo)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mCollectionItemInfo, DEFAULT.mCollectionItemInfo)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mTouchDelegateInfo, DEFAULT.mTouchDelegateInfo)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mExtraRenderingInfo, DEFAULT.mExtraRenderingInfo)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mLeashedChild != DEFAULT.mLeashedChild) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mLeashedParent != DEFAULT.mLeashedParent) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (mLeashedParentNodeId != DEFAULT.mLeashedParentNodeId) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ int totalFields = fieldIndex;
+ parcel.writeLong(nonDefaultFields);
+
+ fieldIndex = 0;
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(isSealed() ? 1 : 0);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mSourceNodeId);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mWindowId);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mParentNodeId);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabelForId);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mConnectionId);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ final LongArray childIds = mChildNodeIds;
+ if (childIds == null) {
+ parcel.writeInt(0);
+ } else {
+ final int childIdsSize = childIds.size();
+ parcel.writeInt(childIdsSize);
+ for (int i = 0; i < childIdsSize; i++) {
+ parcel.writeLong(childIds.get(i));
+ }
+ }
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mBoundsInParent.top);
+ parcel.writeInt(mBoundsInParent.bottom);
+ parcel.writeInt(mBoundsInParent.left);
+ parcel.writeInt(mBoundsInParent.right);
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mBoundsInScreen.top);
+ parcel.writeInt(mBoundsInScreen.bottom);
+ parcel.writeInt(mBoundsInScreen.left);
+ parcel.writeInt(mBoundsInScreen.right);
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ if (mActions != null && !mActions.isEmpty()) {
+ final int actionCount = mActions.size();
+
+ int nonStandardActionCount = 0;
+ long defaultStandardActions = 0;
+ for (int i = 0; i < actionCount; i++) {
+ AccessibilityAction action = mActions.get(i);
+ if (isDefaultStandardAction(action)) {
+ defaultStandardActions |= action.mSerializationFlag;
+ } else {
+ nonStandardActionCount++;
+ }
+ }
+ parcel.writeLong(defaultStandardActions);
+
+ parcel.writeInt(nonStandardActionCount);
+ for (int i = 0; i < actionCount; i++) {
+ AccessibilityAction action = mActions.get(i);
+ if (!isDefaultStandardAction(action)) {
+ action.writeToParcel(parcel, flags);
+ }
+ }
+ } else {
+ parcel.writeLong(0);
+ parcel.writeInt(0);
+ }
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mMaxTextLength);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mMovementGranularities);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mBooleanProperties);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mPackageName);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mClassName);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mText);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mHintText);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mError);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mStateDescription);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeCharSequence(mContentDescription);
+ }
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mPaneTitle);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mTooltipText);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeString(mViewIdResourceName);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mTextSelectionStart);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mTextSelectionEnd);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mInputType);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mLiveRegion);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mDrawingOrderInParent);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeStringList(mExtraDataKeys);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeBundle(mExtras);
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mRangeInfo.getType());
+ parcel.writeFloat(mRangeInfo.getMin());
+ parcel.writeFloat(mRangeInfo.getMax());
+ parcel.writeFloat(mRangeInfo.getCurrent());
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mCollectionInfo.getRowCount());
+ parcel.writeInt(mCollectionInfo.getColumnCount());
+ parcel.writeInt(mCollectionInfo.isHierarchical() ? 1 : 0);
+ parcel.writeInt(mCollectionInfo.getSelectionMode());
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mCollectionItemInfo.getRowIndex());
+ parcel.writeInt(mCollectionItemInfo.getRowSpan());
+ parcel.writeInt(mCollectionItemInfo.getColumnIndex());
+ parcel.writeInt(mCollectionItemInfo.getColumnSpan());
+ parcel.writeInt(mCollectionItemInfo.isHeading() ? 1 : 0);
+ parcel.writeInt(mCollectionItemInfo.isSelected() ? 1 : 0);
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mTouchDelegateInfo.writeToParcel(parcel, flags);
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeValue(mExtraRenderingInfo.getLayoutSize());
+ parcel.writeFloat(mExtraRenderingInfo.getTextSizeInPx());
+ parcel.writeInt(mExtraRenderingInfo.getTextSizeUnit());
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeStrongBinder(mLeashedChild);
+ }
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeStrongBinder(mLeashedParent);
+ }
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeLong(mLeashedParentNodeId);
+ }
+
+ if (DEBUG) {
+ fieldIndex--;
+ if (totalFields != fieldIndex) {
+ throw new IllegalStateException("Number of fields mismatch: " + totalFields
+ + " vs " + fieldIndex);
+ }
+ }
+ }
+
+ /**
+ * Initializes this instance from another one.
+ *
+ * @param other The other instance.
+ * @param usePoolingInfos whether using pooled object internally or not
+ */
+ private void init(AccessibilityNodeInfo other, boolean usePoolingInfos) {
+ mSealed = other.mSealed;
+ mSourceNodeId = other.mSourceNodeId;
+ mParentNodeId = other.mParentNodeId;
+ mLabelForId = other.mLabelForId;
+ mLabeledById = other.mLabeledById;
+ mTraversalBefore = other.mTraversalBefore;
+ mTraversalAfter = other.mTraversalAfter;
+ mWindowId = other.mWindowId;
+ mConnectionId = other.mConnectionId;
+ mBoundsInParent.set(other.mBoundsInParent);
+ mBoundsInScreen.set(other.mBoundsInScreen);
+ mPackageName = other.mPackageName;
+ mClassName = other.mClassName;
+ mText = other.mText;
+ mOriginalText = other.mOriginalText;
+ mHintText = other.mHintText;
+ mError = other.mError;
+ mStateDescription = other.mStateDescription;
+ mContentDescription = other.mContentDescription;
+ mPaneTitle = other.mPaneTitle;
+ mTooltipText = other.mTooltipText;
+ mViewIdResourceName = other.mViewIdResourceName;
+
+ if (mActions != null) mActions.clear();
+ final ArrayList<AccessibilityAction> otherActions = other.mActions;
+ if (otherActions != null && otherActions.size() > 0) {
+ if (mActions == null) {
+ mActions = new ArrayList(otherActions);
+ } else {
+ mActions.addAll(other.mActions);
+ }
+ }
+
+ mBooleanProperties = other.mBooleanProperties;
+ mMaxTextLength = other.mMaxTextLength;
+ mMovementGranularities = other.mMovementGranularities;
+
+
+ if (mChildNodeIds != null) mChildNodeIds.clear();
+ final LongArray otherChildNodeIds = other.mChildNodeIds;
+ if (otherChildNodeIds != null && otherChildNodeIds.size() > 0) {
+ if (mChildNodeIds == null) {
+ mChildNodeIds = otherChildNodeIds.clone();
+ } else {
+ mChildNodeIds.addAll(otherChildNodeIds);
+ }
+ }
+
+ mTextSelectionStart = other.mTextSelectionStart;
+ mTextSelectionEnd = other.mTextSelectionEnd;
+ mInputType = other.mInputType;
+ mLiveRegion = other.mLiveRegion;
+ mDrawingOrderInParent = other.mDrawingOrderInParent;
+
+ mExtraDataKeys = other.mExtraDataKeys;
+
+ mExtras = other.mExtras != null ? new Bundle(other.mExtras) : null;
+
+ if (usePoolingInfos) {
+ initPoolingInfos(other);
+ } else {
+ initCopyInfos(other);
+ }
+
+ final TouchDelegateInfo otherInfo = other.mTouchDelegateInfo;
+ mTouchDelegateInfo = (otherInfo != null)
+ ? new TouchDelegateInfo(otherInfo.mTargetMap, true) : null;
+
+ mLeashedChild = other.mLeashedChild;
+ mLeashedParent = other.mLeashedParent;
+ mLeashedParentNodeId = other.mLeashedParentNodeId;
+ }
+
+ private void initPoolingInfos(AccessibilityNodeInfo other) {
+ if (mRangeInfo != null) mRangeInfo.recycle();
+ mRangeInfo = (other.mRangeInfo != null)
+ ? RangeInfo.obtain(other.mRangeInfo) : null;
+ if (mCollectionInfo != null) mCollectionInfo.recycle();
+ mCollectionInfo = (other.mCollectionInfo != null)
+ ? CollectionInfo.obtain(other.mCollectionInfo) : null;
+ if (mCollectionItemInfo != null) mCollectionItemInfo.recycle();
+ mCollectionItemInfo = (other.mCollectionItemInfo != null)
+ ? CollectionItemInfo.obtain(other.mCollectionItemInfo) : null;
+ if (mExtraRenderingInfo != null) mExtraRenderingInfo.recycle();
+ mExtraRenderingInfo = (other.mExtraRenderingInfo != null)
+ ? ExtraRenderingInfo.obtain(other.mExtraRenderingInfo) : null;
+ }
+
+ private void initCopyInfos(AccessibilityNodeInfo other) {
+ RangeInfo ri = other.mRangeInfo;
+ mRangeInfo = (ri == null) ? null
+ : new RangeInfo(ri.mType, ri.mMin, ri.mMax, ri.mCurrent);
+ CollectionInfo ci = other.mCollectionInfo;
+ mCollectionInfo = (ci == null) ? null
+ : new CollectionInfo(ci.mRowCount, ci.mColumnCount,
+ ci.mHierarchical, ci.mSelectionMode);
+ CollectionItemInfo cii = other.mCollectionItemInfo;
+ mCollectionItemInfo = (cii == null) ? null
+ : new CollectionItemInfo(cii.mRowIndex, cii.mRowSpan, cii.mColumnIndex,
+ cii.mColumnSpan, cii.mHeading, cii.mSelected);
+ ExtraRenderingInfo ti = other.mExtraRenderingInfo;
+ mExtraRenderingInfo = (ti == null) ? null
+ : new ExtraRenderingInfo(ti);
+ }
+
+ /**
+ * Creates a new instance from a {@link Parcel}.
+ *
+ * @param parcel A parcel containing the state of a {@link AccessibilityNodeInfo}.
+ */
+ private void initFromParcel(Parcel parcel) {
+ // Bit mask of non-default-valued field indices
+ long nonDefaultFields = parcel.readLong();
+ int fieldIndex = 0;
+ final boolean sealed = isBitSet(nonDefaultFields, fieldIndex++)
+ ? (parcel.readInt() == 1)
+ : DEFAULT.mSealed;
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mSourceNodeId = parcel.readLong();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mWindowId = parcel.readInt();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mParentNodeId = parcel.readLong();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mLabelForId = parcel.readLong();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong();
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mConnectionId = parcel.readInt();
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ final int childrenSize = parcel.readInt();
+ if (childrenSize <= 0) {
+ mChildNodeIds = null;
+ } else {
+ mChildNodeIds = new LongArray(childrenSize);
+ for (int i = 0; i < childrenSize; i++) {
+ final long childId = parcel.readLong();
+ mChildNodeIds.add(childId);
+ }
+ }
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mBoundsInParent.top = parcel.readInt();
+ mBoundsInParent.bottom = parcel.readInt();
+ mBoundsInParent.left = parcel.readInt();
+ mBoundsInParent.right = parcel.readInt();
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mBoundsInScreen.top = parcel.readInt();
+ mBoundsInScreen.bottom = parcel.readInt();
+ mBoundsInScreen.left = parcel.readInt();
+ mBoundsInScreen.right = parcel.readInt();
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ final long standardActions = parcel.readLong();
+ addStandardActions(standardActions);
+ final int nonStandardActionCount = parcel.readInt();
+ for (int i = 0; i < nonStandardActionCount; i++) {
+ final AccessibilityAction action =
+ AccessibilityAction.CREATOR.createFromParcel(parcel);
+ addActionUnchecked(action);
+ }
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mMaxTextLength = parcel.readInt();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mMovementGranularities = parcel.readInt();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mBooleanProperties = parcel.readInt();
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mPackageName = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mClassName = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mText = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mHintText = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mError = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mStateDescription = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mContentDescription = parcel.readCharSequence();
+ }
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mPaneTitle = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTooltipText = parcel.readCharSequence();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mViewIdResourceName = parcel.readString();
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTextSelectionStart = parcel.readInt();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTextSelectionEnd = parcel.readInt();
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mInputType = parcel.readInt();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mLiveRegion = parcel.readInt();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mDrawingOrderInParent = parcel.readInt();
+
+ mExtraDataKeys = isBitSet(nonDefaultFields, fieldIndex++)
+ ? parcel.createStringArrayList()
+ : null;
+
+ mExtras = isBitSet(nonDefaultFields, fieldIndex++)
+ ? parcel.readBundle()
+ : null;
+
+ if (mRangeInfo != null) mRangeInfo.recycle();
+ mRangeInfo = isBitSet(nonDefaultFields, fieldIndex++)
+ ? RangeInfo.obtain(
+ parcel.readInt(),
+ parcel.readFloat(),
+ parcel.readFloat(),
+ parcel.readFloat())
+ : null;
+
+ if (mCollectionInfo != null) mCollectionInfo.recycle();
+ mCollectionInfo = isBitSet(nonDefaultFields, fieldIndex++)
+ ? CollectionInfo.obtain(
+ parcel.readInt(),
+ parcel.readInt(),
+ parcel.readInt() == 1,
+ parcel.readInt())
+ : null;
+
+ if (mCollectionItemInfo != null) mCollectionItemInfo.recycle();
+ mCollectionItemInfo = isBitSet(nonDefaultFields, fieldIndex++)
+ ? CollectionItemInfo.obtain(
+ parcel.readInt(),
+ parcel.readInt(),
+ parcel.readInt(),
+ parcel.readInt(),
+ parcel.readInt() == 1,
+ parcel.readInt() == 1)
+ : null;
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mTouchDelegateInfo = TouchDelegateInfo.CREATOR.createFromParcel(parcel);
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ if (mExtraRenderingInfo != null) mExtraRenderingInfo.recycle();
+ mExtraRenderingInfo = ExtraRenderingInfo.obtain();
+ mExtraRenderingInfo.mLayoutSize = (Size) parcel.readValue(null);
+ mExtraRenderingInfo.mTextSizeInPx = parcel.readFloat();
+ mExtraRenderingInfo.mTextSizeUnit = parcel.readInt();
+ }
+
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mLeashedChild = parcel.readStrongBinder();
+ }
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mLeashedParent = parcel.readStrongBinder();
+ }
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mLeashedParentNodeId = parcel.readLong();
+ }
+
+ mSealed = sealed;
+ }
+
+ /**
+ * Clears the state of this instance.
+ */
+ private void clear() {
+ init(DEFAULT, true /* usePoolingInfo */);
+ }
+
+ private static boolean isDefaultStandardAction(AccessibilityAction action) {
+ return (action.mSerializationFlag != -1L) && TextUtils.isEmpty(action.getLabel());
+ }
+
+ private static AccessibilityAction getActionSingleton(int actionId) {
+ final int actions = AccessibilityAction.sStandardActions.size();
+ for (int i = 0; i < actions; i++) {
+ AccessibilityAction currentAction = AccessibilityAction.sStandardActions.valueAt(i);
+ if (actionId == currentAction.getId()) {
+ return currentAction;
+ }
+ }
+
+ return null;
+ }
+
+ private static AccessibilityAction getActionSingletonBySerializationFlag(long flag) {
+ final int actions = AccessibilityAction.sStandardActions.size();
+ for (int i = 0; i < actions; i++) {
+ AccessibilityAction currentAction = AccessibilityAction.sStandardActions.valueAt(i);
+ if (flag == currentAction.mSerializationFlag) {
+ return currentAction;
+ }
+ }
+
+ return null;
+ }
+
+ private void addStandardActions(long serializationIdMask) {
+ long remainingIds = serializationIdMask;
+ while (remainingIds > 0) {
+ final long id = 1L << Long.numberOfTrailingZeros(remainingIds);
+ remainingIds &= ~id;
+ AccessibilityAction action = getActionSingletonBySerializationFlag(id);
+ addAction(action);
+ }
+ }
+
+ /**
+ * Gets the human readable action symbolic name.
+ *
+ * @param action The action.
+ * @return The symbolic name.
+ */
+ private static String getActionSymbolicName(int action) {
+ switch (action) {
+ case ACTION_FOCUS:
+ return "ACTION_FOCUS";
+ case ACTION_CLEAR_FOCUS:
+ return "ACTION_CLEAR_FOCUS";
+ case ACTION_SELECT:
+ return "ACTION_SELECT";
+ case ACTION_CLEAR_SELECTION:
+ return "ACTION_CLEAR_SELECTION";
+ case ACTION_CLICK:
+ return "ACTION_CLICK";
+ case ACTION_LONG_CLICK:
+ return "ACTION_LONG_CLICK";
+ case ACTION_ACCESSIBILITY_FOCUS:
+ return "ACTION_ACCESSIBILITY_FOCUS";
+ case ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+ return "ACTION_CLEAR_ACCESSIBILITY_FOCUS";
+ case ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
+ return "ACTION_NEXT_AT_MOVEMENT_GRANULARITY";
+ case ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
+ return "ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY";
+ case ACTION_NEXT_HTML_ELEMENT:
+ return "ACTION_NEXT_HTML_ELEMENT";
+ case ACTION_PREVIOUS_HTML_ELEMENT:
+ return "ACTION_PREVIOUS_HTML_ELEMENT";
+ case ACTION_SCROLL_FORWARD:
+ return "ACTION_SCROLL_FORWARD";
+ case ACTION_SCROLL_BACKWARD:
+ return "ACTION_SCROLL_BACKWARD";
+ case ACTION_CUT:
+ return "ACTION_CUT";
+ case ACTION_COPY:
+ return "ACTION_COPY";
+ case ACTION_PASTE:
+ return "ACTION_PASTE";
+ case ACTION_SET_SELECTION:
+ return "ACTION_SET_SELECTION";
+ case ACTION_EXPAND:
+ return "ACTION_EXPAND";
+ case ACTION_COLLAPSE:
+ return "ACTION_COLLAPSE";
+ case ACTION_DISMISS:
+ return "ACTION_DISMISS";
+ case ACTION_SET_TEXT:
+ return "ACTION_SET_TEXT";
+ case R.id.accessibilityActionShowOnScreen:
+ return "ACTION_SHOW_ON_SCREEN";
+ case R.id.accessibilityActionScrollToPosition:
+ return "ACTION_SCROLL_TO_POSITION";
+ case R.id.accessibilityActionScrollUp:
+ return "ACTION_SCROLL_UP";
+ case R.id.accessibilityActionScrollLeft:
+ return "ACTION_SCROLL_LEFT";
+ case R.id.accessibilityActionScrollDown:
+ return "ACTION_SCROLL_DOWN";
+ case R.id.accessibilityActionScrollRight:
+ return "ACTION_SCROLL_RIGHT";
+ case R.id.accessibilityActionPageDown:
+ return "ACTION_PAGE_DOWN";
+ case R.id.accessibilityActionPageUp:
+ return "ACTION_PAGE_UP";
+ case R.id.accessibilityActionPageLeft:
+ return "ACTION_PAGE_LEFT";
+ case R.id.accessibilityActionPageRight:
+ return "ACTION_PAGE_RIGHT";
+ case R.id.accessibilityActionSetProgress:
+ return "ACTION_SET_PROGRESS";
+ case R.id.accessibilityActionContextClick:
+ return "ACTION_CONTEXT_CLICK";
+ case R.id.accessibilityActionShowTooltip:
+ return "ACTION_SHOW_TOOLTIP";
+ case R.id.accessibilityActionHideTooltip:
+ return "ACTION_HIDE_TOOLTIP";
+ case R.id.accessibilityActionPressAndHold:
+ return "ACTION_PRESS_AND_HOLD";
+ case R.id.accessibilityActionImeEnter:
+ return "ACTION_IME_ENTER";
+ default:
+ return "ACTION_UNKNOWN";
+ }
+ }
+
+ /**
+ * Gets the human readable movement granularity symbolic name.
+ *
+ * @param granularity The granularity.
+ * @return The symbolic name.
+ */
+ private static String getMovementGranularitySymbolicName(int granularity) {
+ switch (granularity) {
+ case MOVEMENT_GRANULARITY_CHARACTER:
+ return "MOVEMENT_GRANULARITY_CHARACTER";
+ case MOVEMENT_GRANULARITY_WORD:
+ return "MOVEMENT_GRANULARITY_WORD";
+ case MOVEMENT_GRANULARITY_LINE:
+ return "MOVEMENT_GRANULARITY_LINE";
+ case MOVEMENT_GRANULARITY_PARAGRAPH:
+ return "MOVEMENT_GRANULARITY_PARAGRAPH";
+ case MOVEMENT_GRANULARITY_PAGE:
+ return "MOVEMENT_GRANULARITY_PAGE";
+ default:
+ throw new IllegalArgumentException("Unknown movement granularity: " + granularity);
+ }
+ }
+
+ private static boolean canPerformRequestOverConnection(int connectionId,
+ int windowId, long accessibilityNodeId) {
+ return ((windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)
+ && (getAccessibilityViewId(accessibilityNodeId) != UNDEFINED_ITEM_ID)
+ && (connectionId != UNDEFINED_CONNECTION_ID));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null) {
+ return false;
+ }
+ if (getClass() != object.getClass()) {
+ return false;
+ }
+ AccessibilityNodeInfo other = (AccessibilityNodeInfo) object;
+ if (mSourceNodeId != other.mSourceNodeId) {
+ return false;
+ }
+ if (mWindowId != other.mWindowId) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + getAccessibilityViewId(mSourceNodeId);
+ result = prime * result + getVirtualDescendantId(mSourceNodeId);
+ result = prime * result + mWindowId;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(super.toString());
+
+ if (DEBUG) {
+ builder.append("; sourceNodeId: 0x").append(Long.toHexString(mSourceNodeId));
+ builder.append("; windowId: 0x").append(Long.toHexString(mWindowId));
+ builder.append("; accessibilityViewId: 0x")
+ .append(Long.toHexString(getAccessibilityViewId(mSourceNodeId)));
+ builder.append("; virtualDescendantId: 0x")
+ .append(Long.toHexString(getVirtualDescendantId(mSourceNodeId)));
+ builder.append("; mParentNodeId: 0x").append(Long.toHexString(mParentNodeId));
+ builder.append("; traversalBefore: 0x").append(Long.toHexString(mTraversalBefore));
+ builder.append("; traversalAfter: 0x").append(Long.toHexString(mTraversalAfter));
+
+ int granularities = mMovementGranularities;
+ builder.append("; MovementGranularities: [");
+ while (granularities != 0) {
+ final int granularity = 1 << Integer.numberOfTrailingZeros(granularities);
+ granularities &= ~granularity;
+ builder.append(getMovementGranularitySymbolicName(granularity));
+ if (granularities != 0) {
+ builder.append(", ");
+ }
+ }
+ builder.append("]");
+
+ builder.append("; childAccessibilityIds: [");
+ final LongArray childIds = mChildNodeIds;
+ if (childIds != null) {
+ for (int i = 0, count = childIds.size(); i < count; i++) {
+ builder.append("0x").append(Long.toHexString(childIds.get(i)));
+ if (i < count - 1) {
+ builder.append(", ");
+ }
+ }
+ }
+ builder.append("]");
+ }
+
+ builder.append("; boundsInParent: ").append(mBoundsInParent);
+ builder.append("; boundsInScreen: ").append(mBoundsInScreen);
+
+ builder.append("; packageName: ").append(mPackageName);
+ builder.append("; className: ").append(mClassName);
+ builder.append("; text: ").append(mText);
+ builder.append("; error: ").append(mError);
+ builder.append("; maxTextLength: ").append(mMaxTextLength);
+ builder.append("; stateDescription: ").append(mStateDescription);
+ builder.append("; contentDescription: ").append(mContentDescription);
+ builder.append("; tooltipText: ").append(mTooltipText);
+ builder.append("; viewIdResName: ").append(mViewIdResourceName);
+
+ builder.append("; checkable: ").append(isCheckable());
+ builder.append("; checked: ").append(isChecked());
+ builder.append("; focusable: ").append(isFocusable());
+ builder.append("; focused: ").append(isFocused());
+ builder.append("; selected: ").append(isSelected());
+ builder.append("; clickable: ").append(isClickable());
+ builder.append("; longClickable: ").append(isLongClickable());
+ builder.append("; contextClickable: ").append(isContextClickable());
+ builder.append("; enabled: ").append(isEnabled());
+ builder.append("; password: ").append(isPassword());
+ builder.append("; scrollable: ").append(isScrollable());
+ builder.append("; importantForAccessibility: ").append(isImportantForAccessibility());
+ builder.append("; visible: ").append(isVisibleToUser());
+ builder.append("; actions: ").append(mActions);
+
+ return builder.toString();
+ }
+
+ private static AccessibilityNodeInfo getNodeForAccessibilityId(int connectionId,
+ int windowId, long accessibilityId) {
+ if (!canPerformRequestOverConnection(connectionId, windowId, accessibilityId)) {
+ return null;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfoByAccessibilityId(connectionId,
+ windowId, accessibilityId, false, FLAG_PREFETCH_PREDECESSORS
+ | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null);
+ }
+
+ private static AccessibilityNodeInfo getNodeForAccessibilityId(int connectionId,
+ IBinder leashToken, long accessibilityId) {
+ if (!((leashToken != null)
+ && (getAccessibilityViewId(accessibilityId) != UNDEFINED_ITEM_ID)
+ && (connectionId != UNDEFINED_CONNECTION_ID))) {
+ return null;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfoByAccessibilityId(connectionId,
+ leashToken, accessibilityId, false, FLAG_PREFETCH_PREDECESSORS
+ | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null);
+ }
+
+ /** @hide */
+ public static String idToString(long accessibilityId) {
+ int accessibilityViewId = getAccessibilityViewId(accessibilityId);
+ int virtualDescendantId = getVirtualDescendantId(accessibilityId);
+ return virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID
+ ? idItemToString(accessibilityViewId)
+ : idItemToString(accessibilityViewId) + ":" + idItemToString(virtualDescendantId);
+ }
+
+ private static String idItemToString(int item) {
+ switch (item) {
+ case ROOT_ITEM_ID: return "ROOT";
+ case UNDEFINED_ITEM_ID: return "UNDEFINED";
+ case AccessibilityNodeProvider.HOST_VIEW_ID: return "HOST";
+ default: return "" + item;
+ }
+ }
+
+ /**
+ * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}.
+ * Each action has a unique id that is mandatory and optional data.
+ * <p>
+ * There are three categories of actions:
+ * <ul>
+ * <li><strong>Standard actions</strong> - These are actions that are reported and
+ * handled by the standard UI widgets in the platform. For each standard action
+ * there is a static constant defined in this class, e.g. {@link #ACTION_FOCUS}.
+ * These actions will have {@code null} labels.
+ * </li>
+ * <li><strong>Custom actions action</strong> - These are actions that are reported
+ * and handled by custom widgets. i.e. ones that are not part of the UI toolkit. For
+ * example, an application may define a custom action for clearing the user history.
+ * </li>
+ * <li><strong>Overriden standard actions</strong> - These are actions that override
+ * standard actions to customize them. For example, an app may add a label to the
+ * standard {@link #ACTION_CLICK} action to indicate to the user that this action clears
+ * browsing history.
+ * </ul>
+ * </p>
+ * <p>
+ * Actions are typically added to an {@link AccessibilityNodeInfo} by using
+ * {@link AccessibilityNodeInfo#addAction(AccessibilityAction)} within
+ * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} and are performed
+ * within {@link View#performAccessibilityAction(int, Bundle)}.
+ * </p>
+ * <p class="note">
+ * <strong>Note:</strong> Views which support these actions should invoke
+ * {@link View#setImportantForAccessibility(int)} with
+ * {@link View#IMPORTANT_FOR_ACCESSIBILITY_YES} to ensure an {@link AccessibilityService}
+ * can discover the set of supported actions.
+ * </p>
+ */
+ public static final class AccessibilityAction implements Parcelable {
+
+ /** @hide */
+ public static final ArraySet<AccessibilityAction> sStandardActions = new ArraySet<>();
+
+ /**
+ * Action that gives input focus to the node.
+ */
+ public static final AccessibilityAction ACTION_FOCUS =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_FOCUS);
+
+ /**
+ * Action that clears input focus of the node.
+ */
+ public static final AccessibilityAction ACTION_CLEAR_FOCUS =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
+
+ /**
+ * Action that selects the node.
+ */
+ public static final AccessibilityAction ACTION_SELECT =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_SELECT);
+
+ /**
+ * Action that deselects the node.
+ */
+ public static final AccessibilityAction ACTION_CLEAR_SELECTION =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
+
+ /**
+ * Action that clicks on the node info.
+ */
+ public static final AccessibilityAction ACTION_CLICK =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK);
+
+ /**
+ * Action that long clicks on the node.
+ */
+ public static final AccessibilityAction ACTION_LONG_CLICK =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+
+ /**
+ * Action that gives accessibility focus to the node.
+ */
+ public static final AccessibilityAction ACTION_ACCESSIBILITY_FOCUS =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+
+ /**
+ * Action that clears accessibility focus of the node.
+ */
+ public static final AccessibilityAction ACTION_CLEAR_ACCESSIBILITY_FOCUS =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+
+ /**
+ * Action that requests to go to the next entity in this node's text
+ * at a given movement granularity. For example, move to the next character,
+ * word, etc.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT},
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
+ * <strong>Example:</strong> Move to the previous character and do not extend selection.
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
+ * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
+ * false);
+ * info.performAction(AccessibilityAction.ACTION_NEXT_AT_MOVEMENT_GRANULARITY.getId(),
+ * arguments);
+ * </code></pre></p>
+ * </p>
+ *
+ * @see AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * @see AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ *
+ * @see AccessibilityNodeInfo#setMovementGranularities(int)
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ * @see AccessibilityNodeInfo#getMovementGranularities()
+ * AccessibilityNodeInfo.getMovementGranularities()
+ *
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_CHARACTER
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_WORD
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_LINE
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PARAGRAPH
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PAGE
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE
+ */
+ public static final AccessibilityAction ACTION_NEXT_AT_MOVEMENT_GRANULARITY =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+
+ /**
+ * Action that requests to go to the previous entity in this node's text
+ * at a given movement granularity. For example, move to the next character,
+ * word, etc.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT},
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
+ * <strong>Example:</strong> Move to the next character and do not extend selection.
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
+ * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
+ * false);
+ * info.performAction(AccessibilityAction.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY.getId(),
+ * arguments);
+ * </code></pre></p>
+ * </p>
+ *
+ * @see AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * @see AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ *
+ * @see AccessibilityNodeInfo#setMovementGranularities(int)
+ * AccessibilityNodeInfo.setMovementGranularities(int)
+ * @see AccessibilityNodeInfo#getMovementGranularities()
+ * AccessibilityNodeInfo.getMovementGranularities()
+ *
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_CHARACTER
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_WORD
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_LINE
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PARAGRAPH
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
+ * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PAGE
+ * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE
+ */
+ public static final AccessibilityAction ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY =
+ new AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+
+ /**
+ * Action to move to the next HTML element of a given type. For example, move
+ * to the BUTTON, INPUT, TABLE, etc.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_HTML_ELEMENT_STRING
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
+ * info.performAction(AccessibilityAction.ACTION_NEXT_HTML_ELEMENT.getId(), arguments);
+ * </code></pre></p>
+ * </p>
+ */
+ public static final AccessibilityAction ACTION_NEXT_HTML_ELEMENT =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+
+ /**
+ * Action to move to the previous HTML element of a given type. For example, move
+ * to the BUTTON, INPUT, TABLE, etc.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_HTML_ELEMENT_STRING
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
+ * info.performAction(AccessibilityAction.ACTION_PREVIOUS_HTML_ELEMENT.getId(), arguments);
+ * </code></pre></p>
+ * </p>
+ */
+ public static final AccessibilityAction ACTION_PREVIOUS_HTML_ELEMENT =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
+
+ /**
+ * Action to scroll the node content forward.
+ */
+ public static final AccessibilityAction ACTION_SCROLL_FORWARD =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+
+ /**
+ * Action to scroll the node content backward.
+ */
+ public static final AccessibilityAction ACTION_SCROLL_BACKWARD =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+
+ /**
+ * Action to copy the current selection to the clipboard.
+ */
+ public static final AccessibilityAction ACTION_COPY =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_COPY);
+
+ /**
+ * Action to paste the current clipboard content.
+ */
+ public static final AccessibilityAction ACTION_PASTE =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_PASTE);
+
+ /**
+ * Action to cut the current selection and place it to the clipboard.
+ */
+ public static final AccessibilityAction ACTION_CUT =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_CUT);
+
+ /**
+ * Action to set the selection. Performing this action with no arguments
+ * clears the selection.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_START_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT},
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_END_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1);
+ * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2);
+ * info.performAction(AccessibilityAction.ACTION_SET_SELECTION.getId(), arguments);
+ * </code></pre></p>
+ * </p>
+ *
+ * @see AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_START_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT
+ * @see AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_END_INT
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT
+ */
+ public static final AccessibilityAction ACTION_SET_SELECTION =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
+
+ /**
+ * Action to expand an expandable node.
+ */
+ public static final AccessibilityAction ACTION_EXPAND =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_EXPAND);
+
+ /**
+ * Action to collapse an expandable node.
+ */
+ public static final AccessibilityAction ACTION_COLLAPSE =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+
+ /**
+ * Action to dismiss a dismissable node.
+ */
+ public static final AccessibilityAction ACTION_DISMISS =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_DISMISS);
+
+ /**
+ * Action that sets the text of the node. Performing the action without argument,
+ * using <code> null</code> or empty {@link CharSequence} will clear the text. This
+ * action will also put the cursor at the end of text.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
+ * AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
+ * "android");
+ * info.performAction(AccessibilityAction.ACTION_SET_TEXT.getId(), arguments);
+ * </code></pre></p>
+ */
+ public static final AccessibilityAction ACTION_SET_TEXT =
+ new AccessibilityAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
+
+ /**
+ * Action that requests the node make its bounding rectangle visible
+ * on the screen, scrolling if necessary just enough.
+ *
+ * @see View#requestRectangleOnScreen(Rect)
+ */
+ public static final AccessibilityAction ACTION_SHOW_ON_SCREEN =
+ new AccessibilityAction(R.id.accessibilityActionShowOnScreen);
+
+ /**
+ * Action that scrolls the node to make the specified collection
+ * position visible on screen.
+ * <p>
+ * <strong>Arguments:</strong>
+ * <ul>
+ * <li>{@link AccessibilityNodeInfo#ACTION_ARGUMENT_ROW_INT}</li>
+ * <li>{@link AccessibilityNodeInfo#ACTION_ARGUMENT_COLUMN_INT}</li>
+ * <ul>
+ *
+ * @see AccessibilityNodeInfo#getCollectionInfo()
+ */
+ public static final AccessibilityAction ACTION_SCROLL_TO_POSITION =
+ new AccessibilityAction(R.id.accessibilityActionScrollToPosition);
+
+ /**
+ * Action to scroll the node content up.
+ */
+ public static final AccessibilityAction ACTION_SCROLL_UP =
+ new AccessibilityAction(R.id.accessibilityActionScrollUp);
+
+ /**
+ * Action to scroll the node content left.
+ */
+ public static final AccessibilityAction ACTION_SCROLL_LEFT =
+ new AccessibilityAction(R.id.accessibilityActionScrollLeft);
+
+ /**
+ * Action to scroll the node content down.
+ */
+ public static final AccessibilityAction ACTION_SCROLL_DOWN =
+ new AccessibilityAction(R.id.accessibilityActionScrollDown);
+
+ /**
+ * Action to scroll the node content right.
+ */
+ public static final AccessibilityAction ACTION_SCROLL_RIGHT =
+ new AccessibilityAction(R.id.accessibilityActionScrollRight);
+
+ /**
+ * Action to move to the page above.
+ */
+ public static final AccessibilityAction ACTION_PAGE_UP =
+ new AccessibilityAction(R.id.accessibilityActionPageUp);
+
+ /**
+ * Action to move to the page below.
+ */
+ public static final AccessibilityAction ACTION_PAGE_DOWN =
+ new AccessibilityAction(R.id.accessibilityActionPageDown);
+
+ /**
+ * Action to move to the page left.
+ */
+ public static final AccessibilityAction ACTION_PAGE_LEFT =
+ new AccessibilityAction(R.id.accessibilityActionPageLeft);
+
+ /**
+ * Action to move to the page right.
+ */
+ public static final AccessibilityAction ACTION_PAGE_RIGHT =
+ new AccessibilityAction(R.id.accessibilityActionPageRight);
+
+ /**
+ * Action that context clicks the node.
+ */
+ public static final AccessibilityAction ACTION_CONTEXT_CLICK =
+ new AccessibilityAction(R.id.accessibilityActionContextClick);
+
+ /**
+ * Action that sets progress between {@link RangeInfo#getMin() RangeInfo.getMin()} and
+ * {@link RangeInfo#getMax() RangeInfo.getMax()}. It should use the same value type as
+ * {@link RangeInfo#getType() RangeInfo.getType()}
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_PROGRESS_VALUE}
+ *
+ * @see RangeInfo
+ */
+ public static final AccessibilityAction ACTION_SET_PROGRESS =
+ new AccessibilityAction(R.id.accessibilityActionSetProgress);
+
+ /**
+ * Action to move a window to a new location.
+ * <p>
+ * <strong>Arguments:</strong>
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_MOVE_WINDOW_X}
+ * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_MOVE_WINDOW_Y}
+ */
+ public static final AccessibilityAction ACTION_MOVE_WINDOW =
+ new AccessibilityAction(R.id.accessibilityActionMoveWindow);
+
+ /**
+ * Action to show a tooltip. A node should expose this action only for views with tooltip
+ * text that but are not currently showing a tooltip.
+ */
+ public static final AccessibilityAction ACTION_SHOW_TOOLTIP =
+ new AccessibilityAction(R.id.accessibilityActionShowTooltip);
+
+ /**
+ * Action to hide a tooltip. A node should expose this action only for views that are
+ * currently showing a tooltip.
+ */
+ public static final AccessibilityAction ACTION_HIDE_TOOLTIP =
+ new AccessibilityAction(R.id.accessibilityActionHideTooltip);
+
+ /**
+ * Action that presses and holds a node.
+ * <p>
+ * This action is for nodes that have distinct behavior that depends on how long a press is
+ * held. Nodes having a single action for long press should use {@link #ACTION_LONG_CLICK}
+ * instead of this action, and nodes should not expose both actions.
+ * <p>
+ * When calling {@code performAction(ACTION_PRESS_AND_HOLD, bundle}, use
+ * {@link #ACTION_ARGUMENT_PRESS_AND_HOLD_DURATION_MILLIS_INT} to specify how long the
+ * node is pressed. The first time an accessibility service performs ACTION_PRES_AND_HOLD
+ * on a node, it must specify 0 as ACTION_ARGUMENT_PRESS_AND_HOLD, so the application is
+ * notified that the held state has started. To ensure reasonable behavior, the values
+ * must be increased incrementally and may not exceed 10,000. UIs requested
+ * to hold for times outside of this range should ignore the action.
+ * <p>
+ * The total time the element is held could be specified by an accessibility user up-front,
+ * or may depend on what happens on the UI as the user continues to request the hold.
+ * <p>
+ * <strong>Note:</strong> The time between dispatching the action and it arriving in the
+ * UI process is not guaranteed. It is possible on a busy system for the time to expire
+ * unexpectedly. For the case of holding down a key for a repeating action, a delayed
+ * arrival should be benign. Please do not use this sort of action in cases where such
+ * delays will lead to unexpected UI behavior.
+ * <p>
+ */
+ @NonNull public static final AccessibilityAction ACTION_PRESS_AND_HOLD =
+ new AccessibilityAction(R.id.accessibilityActionPressAndHold);
+
+ /**
+ * Action to send an ime actionId which is from
+ * {@link android.view.inputmethod.EditorInfo#actionId}. This ime actionId sets by
+ * {@link TextView#setImeActionLabel(CharSequence, int)}, or it would be
+ * {@link android.view.inputmethod.EditorInfo#IME_ACTION_UNSPECIFIED} if no specific
+ * actionId has set. A node should expose this action only for views that are currently
+ * with input focus and editable.
+ */
+ @NonNull public static final AccessibilityAction ACTION_IME_ENTER =
+ new AccessibilityAction(R.id.accessibilityActionImeEnter);
+
+ private final int mActionId;
+ private final CharSequence mLabel;
+
+ /** @hide */
+ public long mSerializationFlag = -1L;
+
+ /**
+ * Creates a new AccessibilityAction. For adding a standard action without a specific label,
+ * use the static constants.
+ *
+ * You can also override the description for one the standard actions. Below is an example
+ * how to override the standard click action by adding a custom label:
+ * <pre>
+ * AccessibilityAction action = new AccessibilityAction(
+ * AccessibilityAction.ACTION_CLICK.getId(), getLocalizedLabel());
+ * node.addAction(action);
+ * </pre>
+ *
+ * @param actionId The id for this action. This should either be one of the
+ * standard actions or a specific action for your app. In that case it is
+ * required to use a resource identifier.
+ * @param label The label for the new AccessibilityAction.
+ */
+ public AccessibilityAction(int actionId, @Nullable CharSequence label) {
+ mActionId = actionId;
+ mLabel = label;
+ }
+
+ /**
+ * Constructor for a {@link #sStandardActions standard} action
+ */
+ private AccessibilityAction(int standardActionId) {
+ this(standardActionId, null);
+
+ mSerializationFlag = bitAt(sStandardActions.size());
+ sStandardActions.add(this);
+ }
+
+ /**
+ * Gets the id for this action.
+ *
+ * @return The action id.
+ */
+ public int getId() {
+ return mActionId;
+ }
+
+ /**
+ * Gets the label for this action. Its purpose is to describe the
+ * action to user.
+ *
+ * @return The label.
+ */
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ @Override
+ public int hashCode() {
+ return mActionId;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (other == null) {
+ return false;
+ }
+
+ if (other == this) {
+ return true;
+ }
+
+ if (getClass() != other.getClass()) {
+ return false;
+ }
+
+ return mActionId == ((AccessibilityAction)other).mActionId;
+ }
+
+ @Override
+ public String toString() {
+ return "AccessibilityAction: " + getActionSymbolicName(mActionId) + " - " + mLabel;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write data into a parcel.
+ */
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mActionId);
+ out.writeCharSequence(mLabel);
+ }
+
+ public static final @NonNull Parcelable.Creator<AccessibilityAction> CREATOR =
+ new Parcelable.Creator<AccessibilityAction>() {
+ public AccessibilityAction createFromParcel(Parcel in) {
+ return new AccessibilityAction(in);
+ }
+
+ public AccessibilityAction[] newArray(int size) {
+ return new AccessibilityAction[size];
+ }
+ };
+
+ private AccessibilityAction(Parcel in) {
+ mActionId = in.readInt();
+ mLabel = in.readCharSequence();
+ }
+ }
+
+ /**
+ * Class with information if a node is a range. Use
+ * {@link RangeInfo#obtain(int, float, float, float)} to get an instance. Recycling is
+ * handled by the {@link AccessibilityNodeInfo} to which this object is attached.
+ */
+ public static final class RangeInfo {
+ private static final int MAX_POOL_SIZE = 10;
+
+ /** Range type: integer. */
+ public static final int RANGE_TYPE_INT = 0;
+ /** Range type: float. */
+ public static final int RANGE_TYPE_FLOAT = 1;
+ /** Range type: percent with values from zero to one hundred. */
+ public static final int RANGE_TYPE_PERCENT = 2;
+
+ private static final SynchronizedPool<RangeInfo> sPool =
+ new SynchronizedPool<AccessibilityNodeInfo.RangeInfo>(MAX_POOL_SIZE);
+
+ private int mType;
+ private float mMin;
+ private float mMax;
+ private float mCurrent;
+
+ /**
+ * Obtains a pooled instance that is a clone of another one.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link AccessibilityNodeInfo.RangeInfo#RangeInfo(int,
+ * float, float, float)} instead.
+ *
+ * @param other The instance to clone.
+ *
+ * @hide
+ */
+ public static RangeInfo obtain(RangeInfo other) {
+ return obtain(other.mType, other.mMin, other.mMax, other.mCurrent);
+ }
+
+ /**
+ * Obtains a pooled instance.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link AccessibilityNodeInfo.RangeInfo#RangeInfo(int,
+ * float, float, float)} instead.
+ *
+ * @param type The type of the range.
+ * @param min The minimum value. Use {@code Float.NEGATIVE_INFINITY} if the range has no
+ * minimum.
+ * @param max The maximum value. Use {@code Float.POSITIVE_INFINITY} if the range has no
+ * maximum.
+ * @param current The current value.
+ */
+ public static RangeInfo obtain(int type, float min, float max, float current) {
+ RangeInfo info = sPool.acquire();
+ if (info == null) {
+ return new RangeInfo(type, min, max, current);
+ }
+
+ info.mType = type;
+ info.mMin = min;
+ info.mMax = max;
+ info.mCurrent = current;
+ return info;
+ }
+
+ /**
+ * Creates a new range.
+ *
+ * @param type The type of the range.
+ * @param min The minimum value. Use {@code Float.NEGATIVE_INFINITY} if the range has no
+ * minimum.
+ * @param max The maximum value. Use {@code Float.POSITIVE_INFINITY} if the range has no
+ * maximum.
+ * @param current The current value.
+ */
+ public RangeInfo(int type, float min, float max, float current) {
+ mType = type;
+ mMin = min;
+ mMax = max;
+ mCurrent = current;
+ }
+
+ /**
+ * Gets the range type.
+ *
+ * @return The range type.
+ *
+ * @see #RANGE_TYPE_INT
+ * @see #RANGE_TYPE_FLOAT
+ * @see #RANGE_TYPE_PERCENT
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Gets the minimum value.
+ *
+ * @return The minimum value, or {@code Float.NEGATIVE_INFINITY} if no minimum exists.
+ */
+ public float getMin() {
+ return mMin;
+ }
+
+ /**
+ * Gets the maximum value.
+ *
+ * @return The maximum value, or {@code Float.POSITIVE_INFINITY} if no maximum exists.
+ */
+ public float getMax() {
+ return mMax;
+ }
+
+ /**
+ * Gets the current value.
+ *
+ * @return The current value.
+ */
+ public float getCurrent() {
+ return mCurrent;
+ }
+
+ /**
+ * Recycles this instance.
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ */
+ void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ private void clear() {
+ mType = 0;
+ mMin = 0;
+ mMax = 0;
+ mCurrent = 0;
+ }
+ }
+
+ /**
+ * Class with information if a node is a collection. Use
+ * {@link CollectionInfo#obtain(int, int, boolean)} to get an instance. Recycling is
+ * handled by the {@link AccessibilityNodeInfo} to which this object is attached.
+ * <p>
+ * A collection of items has rows and columns and may be hierarchical.
+ * For example, a horizontal list is a collection with one column, as
+ * many rows as the list items, and is not hierarchical; A table is a
+ * collection with several rows, several columns, and is not hierarchical;
+ * A vertical tree is a hierarchical collection with one column and
+ * as many rows as the first level children.
+ * </p>
+ */
+ public static final class CollectionInfo {
+ /** Selection mode where items are not selectable. */
+ public static final int SELECTION_MODE_NONE = 0;
+
+ /** Selection mode where a single item may be selected. */
+ public static final int SELECTION_MODE_SINGLE = 1;
+
+ /** Selection mode where multiple items may be selected. */
+ public static final int SELECTION_MODE_MULTIPLE = 2;
+
+ private static final int MAX_POOL_SIZE = 20;
+
+ private static final SynchronizedPool<CollectionInfo> sPool =
+ new SynchronizedPool<>(MAX_POOL_SIZE);
+
+ private int mRowCount;
+ private int mColumnCount;
+ private boolean mHierarchical;
+ private int mSelectionMode;
+
+ /**
+ * Obtains a pooled instance that is a clone of another one.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link
+ * AccessibilityNodeInfo.CollectionInfo#CollectionInfo} instead.
+ *
+ * @param other The instance to clone.
+ * @hide
+ */
+ public static CollectionInfo obtain(CollectionInfo other) {
+ return CollectionInfo.obtain(other.mRowCount, other.mColumnCount, other.mHierarchical,
+ other.mSelectionMode);
+ }
+
+ /**
+ * Obtains a pooled instance.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link
+ * AccessibilityNodeInfo.CollectionInfo#CollectionInfo(int, int,
+ * boolean)} instead.
+ *
+ * @param rowCount The number of rows, or -1 if count is unknown.
+ * @param columnCount The number of columns, or -1 if count is unknown.
+ * @param hierarchical Whether the collection is hierarchical.
+ */
+ public static CollectionInfo obtain(int rowCount, int columnCount,
+ boolean hierarchical) {
+ return obtain(rowCount, columnCount, hierarchical, SELECTION_MODE_NONE);
+ }
+
+ /**
+ * Obtains a pooled instance.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link
+ * AccessibilityNodeInfo.CollectionInfo#CollectionInfo(int, int,
+ * boolean, int)} instead.
+ *
+ * @param rowCount The number of rows.
+ * @param columnCount The number of columns.
+ * @param hierarchical Whether the collection is hierarchical.
+ * @param selectionMode The collection's selection mode, one of:
+ * <ul>
+ * <li>{@link #SELECTION_MODE_NONE}
+ * <li>{@link #SELECTION_MODE_SINGLE}
+ * <li>{@link #SELECTION_MODE_MULTIPLE}
+ * </ul>
+ */
+ public static CollectionInfo obtain(int rowCount, int columnCount,
+ boolean hierarchical, int selectionMode) {
+ final CollectionInfo info = sPool.acquire();
+ if (info == null) {
+ return new CollectionInfo(rowCount, columnCount, hierarchical, selectionMode);
+ }
+
+ info.mRowCount = rowCount;
+ info.mColumnCount = columnCount;
+ info.mHierarchical = hierarchical;
+ info.mSelectionMode = selectionMode;
+ return info;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param rowCount The number of rows.
+ * @param columnCount The number of columns.
+ * @param hierarchical Whether the collection is hierarchical.
+ */
+ public CollectionInfo(int rowCount, int columnCount, boolean hierarchical) {
+ this(rowCount, columnCount, hierarchical, SELECTION_MODE_NONE);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param rowCount The number of rows.
+ * @param columnCount The number of columns.
+ * @param hierarchical Whether the collection is hierarchical.
+ * @param selectionMode The collection's selection mode.
+ */
+ public CollectionInfo(int rowCount, int columnCount, boolean hierarchical,
+ int selectionMode) {
+ mRowCount = rowCount;
+ mColumnCount = columnCount;
+ mHierarchical = hierarchical;
+ mSelectionMode = selectionMode;
+ }
+
+ /**
+ * Gets the number of rows.
+ *
+ * @return The row count, or -1 if count is unknown.
+ */
+ public int getRowCount() {
+ return mRowCount;
+ }
+
+ /**
+ * Gets the number of columns.
+ *
+ * @return The column count, or -1 if count is unknown.
+ */
+ public int getColumnCount() {
+ return mColumnCount;
+ }
+
+ /**
+ * Gets if the collection is a hierarchically ordered.
+ *
+ * @return Whether the collection is hierarchical.
+ */
+ public boolean isHierarchical() {
+ return mHierarchical;
+ }
+
+ /**
+ * Gets the collection's selection mode.
+ *
+ * @return The collection's selection mode, one of:
+ * <ul>
+ * <li>{@link #SELECTION_MODE_NONE}
+ * <li>{@link #SELECTION_MODE_SINGLE}
+ * <li>{@link #SELECTION_MODE_MULTIPLE}
+ * </ul>
+ */
+ public int getSelectionMode() {
+ return mSelectionMode;
+ }
+
+ /**
+ * Recycles this instance.
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ */
+ void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ private void clear() {
+ mRowCount = 0;
+ mColumnCount = 0;
+ mHierarchical = false;
+ mSelectionMode = SELECTION_MODE_NONE;
+ }
+ }
+
+ /**
+ * Class with information if a node is a collection item. Use
+ * {@link CollectionItemInfo#obtain(int, int, int, int, boolean)}
+ * to get an instance. Recycling is handled by the {@link AccessibilityNodeInfo} to which this
+ * object is attached.
+ * <p>
+ * A collection item is contained in a collection, it starts at
+ * a given row and column in the collection, and spans one or
+ * more rows and columns. For example, a header of two related
+ * table columns starts at the first row and the first column,
+ * spans one row and two columns.
+ * </p>
+ */
+ public static final class CollectionItemInfo {
+ private static final int MAX_POOL_SIZE = 20;
+
+ private static final SynchronizedPool<CollectionItemInfo> sPool =
+ new SynchronizedPool<>(MAX_POOL_SIZE);
+
+ /**
+ * Obtains a pooled instance that is a clone of another one.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link
+ * AccessibilityNodeInfo.CollectionItemInfo#CollectionItemInfo}
+ * instead.
+ *
+ * @param other The instance to clone.
+ * @hide
+ */
+ public static CollectionItemInfo obtain(CollectionItemInfo other) {
+ return CollectionItemInfo.obtain(other.mRowIndex, other.mRowSpan, other.mColumnIndex,
+ other.mColumnSpan, other.mHeading, other.mSelected);
+ }
+
+ /**
+ * Obtains a pooled instance.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link
+ * AccessibilityNodeInfo.CollectionItemInfo#CollectionItemInfo(int,
+ * int, int, int, boolean)} instead.
+ *
+ * @param rowIndex The row index at which the item is located.
+ * @param rowSpan The number of rows the item spans.
+ * @param columnIndex The column index at which the item is located.
+ * @param columnSpan The number of columns the item spans.
+ * @param heading Whether the item is a heading. (Prefer
+ * {@link AccessibilityNodeInfo#setHeading(boolean)}).
+ */
+ public static CollectionItemInfo obtain(int rowIndex, int rowSpan,
+ int columnIndex, int columnSpan, boolean heading) {
+ return obtain(rowIndex, rowSpan, columnIndex, columnSpan, heading, false);
+ }
+
+ /**
+ * Obtains a pooled instance.
+ *
+ * <p>In most situations object pooling is not beneficial. Creates a new instance using the
+ * constructor {@link
+ * AccessibilityNodeInfo.CollectionItemInfo#CollectionItemInfo(int,
+ * int, int, int, boolean, boolean)} instead.
+ *
+ * @param rowIndex The row index at which the item is located.
+ * @param rowSpan The number of rows the item spans.
+ * @param columnIndex The column index at which the item is located.
+ * @param columnSpan The number of columns the item spans.
+ * @param heading Whether the item is a heading. (Prefer
+ * {@link AccessibilityNodeInfo#setHeading(boolean)})
+ * @param selected Whether the item is selected.
+ */
+ public static CollectionItemInfo obtain(int rowIndex, int rowSpan,
+ int columnIndex, int columnSpan, boolean heading, boolean selected) {
+ final CollectionItemInfo info = sPool.acquire();
+ if (info == null) {
+ return new CollectionItemInfo(
+ rowIndex, rowSpan, columnIndex, columnSpan, heading, selected);
+ }
+
+ info.mRowIndex = rowIndex;
+ info.mRowSpan = rowSpan;
+ info.mColumnIndex = columnIndex;
+ info.mColumnSpan = columnSpan;
+ info.mHeading = heading;
+ info.mSelected = selected;
+ return info;
+ }
+
+ private boolean mHeading;
+ private int mColumnIndex;
+ private int mRowIndex;
+ private int mColumnSpan;
+ private int mRowSpan;
+ private boolean mSelected;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param rowIndex The row index at which the item is located.
+ * @param rowSpan The number of rows the item spans.
+ * @param columnIndex The column index at which the item is located.
+ * @param columnSpan The number of columns the item spans.
+ * @param heading Whether the item is a heading.
+ */
+ public CollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, int columnSpan,
+ boolean heading) {
+ this(rowIndex, rowSpan, columnIndex, columnSpan, heading, false);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param rowIndex The row index at which the item is located.
+ * @param rowSpan The number of rows the item spans.
+ * @param columnIndex The column index at which the item is located.
+ * @param columnSpan The number of columns the item spans.
+ * @param heading Whether the item is a heading.
+ * @param selected Whether the item is selected.
+ */
+ public CollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, int columnSpan,
+ boolean heading, boolean selected) {
+ mRowIndex = rowIndex;
+ mRowSpan = rowSpan;
+ mColumnIndex = columnIndex;
+ mColumnSpan = columnSpan;
+ mHeading = heading;
+ mSelected = selected;
+ }
+
+ /**
+ * Gets the column index at which the item is located.
+ *
+ * @return The column index.
+ */
+ public int getColumnIndex() {
+ return mColumnIndex;
+ }
+
+ /**
+ * Gets the row index at which the item is located.
+ *
+ * @return The row index.
+ */
+ public int getRowIndex() {
+ return mRowIndex;
+ }
+
+ /**
+ * Gets the number of columns the item spans.
+ *
+ * @return The column span.
+ */
+ public int getColumnSpan() {
+ return mColumnSpan;
+ }
+
+ /**
+ * Gets the number of rows the item spans.
+ *
+ * @return The row span.
+ */
+ public int getRowSpan() {
+ return mRowSpan;
+ }
+
+ /**
+ * Gets if the collection item is a heading. For example, section
+ * heading, table header, etc.
+ *
+ * @return If the item is a heading.
+ * @deprecated Use {@link AccessibilityNodeInfo#isHeading()}
+ */
+ public boolean isHeading() {
+ return mHeading;
+ }
+
+ /**
+ * Gets if the collection item is selected.
+ *
+ * @return If the item is selected.
+ */
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ /**
+ * Recycles this instance.
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ */
+ void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ private void clear() {
+ mColumnIndex = 0;
+ mColumnSpan = 0;
+ mRowIndex = 0;
+ mRowSpan = 0;
+ mHeading = false;
+ mSelected = false;
+ }
+ }
+
+ /**
+ * Class with information of touch delegated views and regions from {@link TouchDelegate} for
+ * the {@link AccessibilityNodeInfo}.
+ *
+ * @see AccessibilityNodeInfo#setTouchDelegateInfo(TouchDelegateInfo)
+ */
+ public static final class TouchDelegateInfo implements Parcelable {
+ private ArrayMap<Region, Long> mTargetMap;
+ // Two ids are initialized lazily in AccessibilityNodeInfo#getTouchDelegateInfo
+ private int mConnectionId;
+ private int mWindowId;
+
+ /**
+ * Create a new instance of {@link TouchDelegateInfo}.
+ *
+ * @param targetMap A map from regions (in view coordinates) to delegated views.
+ * @throws IllegalArgumentException if targetMap is empty or {@code null} in
+ * Regions or Views.
+ */
+ public TouchDelegateInfo(@NonNull Map<Region, View> targetMap) {
+ Preconditions.checkArgument(!targetMap.isEmpty()
+ && !targetMap.containsKey(null) && !targetMap.containsValue(null));
+ mTargetMap = new ArrayMap<>(targetMap.size());
+ for (final Region region : targetMap.keySet()) {
+ final View view = targetMap.get(region);
+ mTargetMap.put(region, (long) view.getAccessibilityViewId());
+ }
+ }
+
+ /**
+ * Create a new instance from target map.
+ *
+ * @param targetMap A map from regions (in view coordinates) to delegated views'
+ * accessibility id.
+ * @param doCopy True if shallow copy targetMap.
+ * @throws IllegalArgumentException if targetMap is empty or {@code null} in
+ * Regions or Views.
+ */
+ TouchDelegateInfo(@NonNull ArrayMap<Region, Long> targetMap, boolean doCopy) {
+ Preconditions.checkArgument(!targetMap.isEmpty()
+ && !targetMap.containsKey(null) && !targetMap.containsValue(null));
+ if (doCopy) {
+ mTargetMap = new ArrayMap<>(targetMap.size());
+ mTargetMap.putAll(targetMap);
+ } else {
+ mTargetMap = targetMap;
+ }
+ }
+
+ /**
+ * Set the connection ID.
+ *
+ * @param connectionId The connection id.
+ */
+ private void setConnectionId(int connectionId) {
+ mConnectionId = connectionId;
+ }
+
+ /**
+ * Set the window ID.
+ *
+ * @param windowId The window id.
+ */
+ private void setWindowId(int windowId) {
+ mWindowId = windowId;
+ }
+
+ /**
+ * Returns the number of touch delegate target region.
+ *
+ * @return Number of touch delegate target region.
+ */
+ public int getRegionCount() {
+ return mTargetMap.size();
+ }
+
+ /**
+ * Return the {@link Region} at the given index in the {@link TouchDelegateInfo}.
+ *
+ * @param index The desired index, must be between 0 and {@link #getRegionCount()}-1.
+ * @return Returns the {@link Region} stored at the given index.
+ */
+ @NonNull
+ public Region getRegionAt(int index) {
+ return mTargetMap.keyAt(index);
+ }
+
+ /**
+ * Return the target {@link AccessibilityNodeInfo} for the given {@link Region}.
+ * <p>
+ * <strong>Note:</strong> This api can only be called from {@link AccessibilityService}.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the
+ * received info by calling {@link AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ *
+ * @param region The region retrieved from {@link #getRegionAt(int)}.
+ * @return The target node associates with the given region.
+ */
+ @Nullable
+ public AccessibilityNodeInfo getTargetForRegion(@NonNull Region region) {
+ return getNodeForAccessibilityId(mConnectionId, mWindowId, mTargetMap.get(region));
+ }
+
+ /**
+ * Return the accessibility id of target node.
+ *
+ * @param region The region retrieved from {@link #getRegionAt(int)}.
+ * @return The accessibility id of target node.
+ *
+ * @hide
+ */
+ @TestApi
+ public long getAccessibilityIdForRegion(@NonNull Region region) {
+ return mTargetMap.get(region);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTargetMap.size());
+ for (int i = 0; i < mTargetMap.size(); i++) {
+ final Region region = mTargetMap.keyAt(i);
+ final Long accessibilityId = mTargetMap.valueAt(i);
+ region.writeToParcel(dest, flags);
+ dest.writeLong(accessibilityId);
+ }
+ }
+
+ /**
+ * @see android.os.Parcelable.Creator
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<TouchDelegateInfo> CREATOR =
+ new Parcelable.Creator<TouchDelegateInfo>() {
+ @Override
+ public TouchDelegateInfo createFromParcel(Parcel parcel) {
+ final int size = parcel.readInt();
+ if (size == 0) {
+ return null;
+ }
+ final ArrayMap<Region, Long> targetMap = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final Region region = Region.CREATOR.createFromParcel(parcel);
+ final long accessibilityId = parcel.readLong();
+ targetMap.put(region, accessibilityId);
+ }
+ final TouchDelegateInfo touchDelegateInfo = new TouchDelegateInfo(
+ targetMap, false);
+ return touchDelegateInfo;
+ }
+
+ @Override
+ public TouchDelegateInfo[] newArray(int size) {
+ return new TouchDelegateInfo[size];
+ }
+ };
+ }
+
+ /**
+ * Class with information of a view useful to evaluate accessibility needs. Developers can
+ * refresh the node with the key {@link #EXTRA_DATA_RENDERING_INFO_KEY} to fetch the text size
+ * and unit if it is {@link TextView} and the height and the width of layout params from
+ * {@link ViewGroup} or {@link TextView}.
+ *
+ * @see #EXTRA_DATA_RENDERING_INFO_KEY
+ * @see #refreshWithExtraData(String, Bundle)
+ */
+ public static final class ExtraRenderingInfo {
+ private static final int UNDEFINED_VALUE = -1;
+ private static final int MAX_POOL_SIZE = 20;
+ private static final SynchronizedPool<ExtraRenderingInfo> sPool =
+ new SynchronizedPool<>(MAX_POOL_SIZE);
+
+ private Size mLayoutSize;
+ private float mTextSizeInPx = UNDEFINED_VALUE;
+ private int mTextSizeUnit = UNDEFINED_VALUE;
+
+ /**
+ * Obtains a pooled instance.
+ * @hide
+ */
+ @NonNull
+ public static ExtraRenderingInfo obtain() {
+ final ExtraRenderingInfo info = sPool.acquire();
+ if (info == null) {
+ return new ExtraRenderingInfo(null);
+ }
+ return info;
+ }
+
+ /** Obtains a pooled instance that is a clone of another one. */
+ private static ExtraRenderingInfo obtain(ExtraRenderingInfo other) {
+ ExtraRenderingInfo extraRenderingInfo = ExtraRenderingInfo.obtain();
+ extraRenderingInfo.mLayoutSize = other.mLayoutSize;
+ extraRenderingInfo.mTextSizeInPx = other.mTextSizeInPx;
+ extraRenderingInfo.mTextSizeUnit = other.mTextSizeUnit;
+ return extraRenderingInfo;
+ }
+
+ /**
+ * Creates a new rendering info of a view, and this new instance is initialized from
+ * the given <code>other</code>.
+ *
+ * @param other The instance to clone.
+ */
+ private ExtraRenderingInfo(@Nullable ExtraRenderingInfo other) {
+ if (other != null) {
+ mLayoutSize = other.mLayoutSize;
+ mTextSizeInPx = other.mTextSizeInPx;
+ mTextSizeUnit = other.mTextSizeUnit;
+ }
+ }
+
+ /**
+ * Gets the size object containing the height and the width of
+ * {@link android.view.ViewGroup.LayoutParams} if the node is a {@link ViewGroup} or
+ * a {@link TextView}, or null otherwise. Useful for some accessibility services to
+ * understand whether the text is scalable and fits the view or not.
+ *
+ * @return a {@link Size} stores layout height and layout width of the view, or null
+ * otherwise. And the size value may be in pixels,
+ * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
+ * or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+ */
+ public @Nullable Size getLayoutSize() {
+ return mLayoutSize;
+ }
+
+ /**
+ * Sets layout width and layout height of the view.
+ *
+ * @param width The layout width.
+ * @param height The layout height.
+ * @hide
+ */
+ public void setLayoutSize(int width, int height) {
+ mLayoutSize = new Size(width, height);
+ }
+
+ /**
+ * Gets the text size if the node is a {@link TextView}, or -1 otherwise. Useful for some
+ * accessibility services to understand whether the text is scalable and fits the view or
+ * not.
+ *
+ * @return the text size of a {@code TextView}, or -1 otherwise.
+ */
+ public float getTextSizeInPx() {
+ return mTextSizeInPx;
+ }
+
+ /**
+ * Sets text size of the view.
+ *
+ * @param textSizeInPx The text size in pixels.
+ * @hide
+ */
+ public void setTextSizeInPx(float textSizeInPx) {
+ mTextSizeInPx = textSizeInPx;
+ }
+
+ /**
+ * Gets the text size unit if the node is a {@link TextView}, or -1 otherwise.
+ * Text size returned from {@link #getTextSizeInPx} in raw pixels may scale by factors and
+ * convert from other units. Useful for some accessibility services to understand whether
+ * the text is scalable and fits the view or not.
+ *
+ * @return the text size unit which type is {@link TypedValue#TYPE_DIMENSION} of a
+ * {@code TextView}, or -1 otherwise.
+ *
+ * @see TypedValue#TYPE_DIMENSION
+ */
+ public int getTextSizeUnit() {
+ return mTextSizeUnit;
+ }
+
+ /**
+ * Sets text size unit of the view.
+ *
+ * @param textSizeUnit The text size unit.
+ * @hide
+ */
+ public void setTextSizeUnit(int textSizeUnit) {
+ mTextSizeUnit = textSizeUnit;
+ }
+
+ /**
+ * Recycles this instance.
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ */
+ void recycle() {
+ clear();
+ sPool.release(this);
+ }
+
+ private void clear() {
+ mLayoutSize = null;
+ mTextSizeInPx = UNDEFINED_VALUE;
+ mTextSizeUnit = UNDEFINED_VALUE;
+ }
+ }
+
+ /**
+ * @see android.os.Parcelable.Creator
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<AccessibilityNodeInfo> CREATOR =
+ new Parcelable.Creator<AccessibilityNodeInfo>() {
+ @Override
+ public AccessibilityNodeInfo createFromParcel(Parcel parcel) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.initFromParcel(parcel);
+ return info;
+ }
+
+ @Override
+ public AccessibilityNodeInfo[] newArray(int size) {
+ return new AccessibilityNodeInfo[size];
+ }
+ };
+}
diff --git a/android/view/accessibility/AccessibilityNodeProvider.java b/android/view/accessibility/AccessibilityNodeProvider.java
new file mode 100644
index 0000000..f4c7b96
--- /dev/null
+++ b/android/view/accessibility/AccessibilityNodeProvider.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.accessibilityservice.AccessibilityService;
+import android.os.Bundle;
+import android.view.View;
+
+import java.util.List;
+
+/**
+ * This class is the contract a client should implement to enable support of a
+ * virtual view hierarchy rooted at a given view for accessibility purposes. A virtual
+ * view hierarchy is a tree of imaginary Views that is reported as a part of the view
+ * hierarchy when an {@link AccessibilityService} explores the window content.
+ * Since the virtual View tree does not exist this class is responsible for
+ * managing the {@link AccessibilityNodeInfo}s describing that tree to accessibility
+ * services.
+ * </p>
+ * <p>
+ * The main use case of these APIs is to enable a custom view that draws complex content,
+ * for example a monthly calendar grid, to be presented as a tree of logical nodes,
+ * for example month days each containing events, thus conveying its logical structure.
+ * <p>
+ * <p>
+ * A typical use case is to override {@link View#getAccessibilityNodeProvider()} of the
+ * View that is a root of a virtual View hierarchy to return an instance of this class.
+ * In such a case this instance is responsible for managing {@link AccessibilityNodeInfo}s
+ * describing the virtual sub-tree rooted at the View including the one representing the
+ * View itself. Similarly the returned instance is responsible for performing accessibility
+ * actions on any virtual view or the root view itself. For example:
+ * </p>
+ * <div>
+ * <div class="ds-selector-tabs"><section><h3 id="kotlin">Kotlin</h3>
+ * <pre class="prettyprint lang-kotlin">
+ * // "view" is the View instance on which this class performs accessibility functions.
+ * class MyCalendarViewAccessibilityDelegate(
+ * private var view: MyCalendarView) : AccessibilityDelegate() {
+ * override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider {
+ * return object : AccessibilityNodeProvider() {
+ * override fun createAccessibilityNodeInfo(virtualViewId: Int):
+ * AccessibilityNodeInfo? {
+ * when (virtualViewId) {
+ * <var>host-view-id</var> -> {
+ * val node = AccessibilityNodeInfo.obtain(view)
+ * node.addChild(view, <var>child-view-id</var>)
+ * // Set other attributes like screenReaderFocusable
+ * // and contentDescription.
+ * return node
+ * }
+ * <var>child-view-id</var> -> {
+ * val node = AccessibilityNodeInfo
+ * .obtain(view, virtualViewId)
+ * node.setParent(view)
+ * node.addAction(ACTION_SCROLL_UP)
+ * node.addAction(ACTION_SCROLL_DOWN)
+ * // Set other attributes like focusable and visibleToUser.
+ * node.setBoundsInScreen(
+ * Rect(<var>coords-of-edges-relative-to-screen</var>))
+ * return node
+ * }
+ * else -> return null
+ * }
+ * }
+ *
+ * override fun performAction(
+ * virtualViewId: Int,
+ * action: Int,
+ * arguments: Bundle
+ * ): Boolean {
+ * if (virtualViewId == <var>host-view-id</var>) {
+ * return view.performAccessibilityAction(action, arguments)
+ * }
+ * when (action) {
+ * ACTION_SCROLL_UP.id -> {
+ * // Implement logic in a separate method.
+ * navigateToPreviousMonth()
+ *
+ * return true
+ * }
+ * ACTION_SCROLL_DOWN.id ->
+ * // Implement logic in a separate method.
+ * navigateToNextMonth()
+ *
+ * return true
+ * else -> return false
+ * }
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ * </section><section><h3 id="java">Java</h3>
+ * <pre class="prettyprint lang-java">
+ * final class MyCalendarViewAccessibilityDelegate extends AccessibilityDelegate {
+ * // The View instance on which this class performs accessibility functions.
+ * private final MyCalendarView view;
+ *
+ * MyCalendarViewAccessibilityDelegate(MyCalendarView view) {
+ * this.view = view;
+ * }
+ *
+ * @Override
+ * public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ * return new AccessibilityNodeProvider() {
+ * @Override
+ * @Nullable
+ * public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ * if (virtualViewId == <var>host-view-id</var>) {
+ * AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(view);
+ * node.addChild(view, <var>child-view-id</var>);
+ * // Set other attributes like screenReaderFocusable and contentDescription.
+ * return node;
+ * } else if (virtualViewId == <var>child-view-id</var>) {
+ * AccessibilityNodeInfo node =
+ * AccessibilityNodeInfo.obtain(view, virtualViewId);
+ * node.setParent(view);
+ * node.addAction(ACTION_SCROLL_UP);
+ * node.addAction(ACTION_SCROLL_DOWN);
+ * // Set other attributes like focusable and visibleToUser.
+ * node.setBoundsInScreen(
+ * new Rect(<var>coordinates-of-edges-relative-to-screen</var>));
+ * return node;
+ * } else {
+ * return null;
+ * }
+ * }
+ *
+ * @Override
+ * public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ * if (virtualViewId == <var>host-view-id</var>) {
+ * return view.performAccessibilityAction(action, arguments);
+ * }
+ *
+ * if (action == ACTION_SCROLL_UP.getId()) {
+ * // Implement logic in a separate method.
+ * navigateToPreviousMonth();
+ *
+ * return true;
+ * } else if (action == ACTION_SCROLL_DOWN.getId()) {
+ * // Implement logic in a separate method.
+ * navigateToNextMonth();
+ *
+ * return true;
+ * } else {
+ * return false;
+ * }
+ * }
+ * };
+ * }
+ * }
+ * </pre></section></div></div>
+ */
+public abstract class AccessibilityNodeProvider {
+
+ /**
+ * The virtual id for the hosting View.
+ */
+ public static final int HOST_VIEW_ID = -1;
+
+ /**
+ * Returns an {@link AccessibilityNodeInfo} representing a virtual view,
+ * such as a descendant of the host View, with the given <code>virtualViewId</code>
+ * or the host View itself if <code>virtualViewId</code> equals to {@link #HOST_VIEW_ID}.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * The implementer is responsible for obtaining an accessibility node info from the
+ * pool of reusable instances and setting the desired properties of the node info
+ * before returning it.
+ * </p>
+ *
+ * @param virtualViewId A client defined virtual view id.
+ * @return A populated {@link AccessibilityNodeInfo} for a virtual descendant or the
+ * host View.
+ *
+ * @see View#createAccessibilityNodeInfo()
+ * @see AccessibilityNodeInfo
+ */
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ return null;
+ }
+
+ /**
+ * Adds extra data to an {@link AccessibilityNodeInfo} based on an explicit request for the
+ * additional data.
+ * <p>
+ * This method only needs to be implemented if a virtual view offers to provide additional
+ * data.
+ * </p>
+ *
+ * @param virtualViewId The virtual view id used to create the node
+ * @param info The info to which to add the extra data
+ * @param extraDataKey A key specifying the type of extra data to add to the info. The
+ * extra data should be added to the {@link Bundle} returned by
+ * the info's {@link AccessibilityNodeInfo#getExtras} method.
+ * @param arguments A {@link Bundle} holding any arguments relevant for this request.
+ *
+ * @see AccessibilityNodeInfo#setAvailableExtraData(List)
+ */
+ public void addExtraDataToAccessibilityNodeInfo(
+ int virtualViewId, AccessibilityNodeInfo info, String extraDataKey, Bundle arguments) {
+ }
+
+ /**
+ * Performs an accessibility action on a virtual view, such as a descendant of the
+ * host View, with the given <code>virtualViewId</code> or the host View itself
+ * if <code>virtualViewId</code> equals to {@link #HOST_VIEW_ID}.
+ *
+ * @param virtualViewId A client defined virtual view id.
+ * @param action The action to perform.
+ * @param arguments Optional action arguments.
+ * @return True if the action was performed.
+ *
+ * @see View#performAccessibilityAction(int, Bundle)
+ * @see #createAccessibilityNodeInfo(int)
+ * @see AccessibilityNodeInfo
+ */
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ return false;
+ }
+
+ /**
+ * Finds {@link AccessibilityNodeInfo}s by text. The match is case insensitive
+ * containment. The search is relative to the virtual view, i.e. a descendant of the
+ * host View, with the given <code>virtualViewId</code> or the host View itself
+ * <code>virtualViewId</code> equals to {@link #HOST_VIEW_ID}.
+ *
+ * @param virtualViewId A client defined virtual view id which defined
+ * the root of the tree in which to perform the search.
+ * @param text The searched text.
+ * @return A list of node info.
+ *
+ * @see #createAccessibilityNodeInfo(int)
+ * @see AccessibilityNodeInfo
+ */
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
+ int virtualViewId) {
+ return null;
+ }
+
+ /**
+ * Find the virtual view, such as a descendant of the host View, that has the
+ * specified focus type.
+ *
+ * @param focus The focus to find. One of
+ * {@link AccessibilityNodeInfo#FOCUS_INPUT} or
+ * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}.
+ * @return The node info of the focused view or null.
+ * @see AccessibilityNodeInfo#FOCUS_INPUT
+ * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
+ */
+ public AccessibilityNodeInfo findFocus(int focus) {
+ return null;
+ }
+}
diff --git a/android/view/accessibility/AccessibilityRecord.java b/android/view/accessibility/AccessibilityRecord.java
new file mode 100644
index 0000000..c3a4d32
--- /dev/null
+++ b/android/view/accessibility/AccessibilityRecord.java
@@ -0,0 +1,989 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import static com.android.internal.util.CollectionUtils.isEmpty;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcelable;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a record in an {@link AccessibilityEvent} and contains information
+ * about state change of its source {@link android.view.View}. When a view fires
+ * an accessibility event it requests from its parent to dispatch the
+ * constructed event. The parent may optionally append a record for itself
+ * for providing more context to
+ * {@link android.accessibilityservice.AccessibilityService}s. Hence,
+ * accessibility services can facilitate additional accessibility records
+ * to enhance feedback.
+ * </p>
+ * <p>
+ * Once the accessibility event containing a record is dispatched the record is
+ * made immutable and calling a state mutation method generates an error.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Not all properties are applicable to all accessibility
+ * event types. For detailed information please refer to {@link AccessibilityEvent}.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about creating and processing AccessibilityRecords, read the
+ * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
+ * developer guide.</p>
+ * </div>
+ *
+ * @see AccessibilityEvent
+ * @see AccessibilityManager
+ * @see android.accessibilityservice.AccessibilityService
+ * @see AccessibilityNodeInfo
+ */
+public class AccessibilityRecord {
+ /** @hide */
+ protected static final boolean DEBUG_CONCISE_TOSTRING = false;
+
+ private static final int UNDEFINED = -1;
+
+ private static final int PROPERTY_CHECKED = 0x00000001;
+ private static final int PROPERTY_ENABLED = 0x00000002;
+ private static final int PROPERTY_PASSWORD = 0x00000004;
+ private static final int PROPERTY_FULL_SCREEN = 0x00000080;
+ private static final int PROPERTY_SCROLLABLE = 0x00000100;
+ private static final int PROPERTY_IMPORTANT_FOR_ACCESSIBILITY = 0x00000200;
+
+ private static final int GET_SOURCE_PREFETCH_FLAGS =
+ AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS
+ | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS
+ | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS;
+
+ // Housekeeping
+ private static final int MAX_POOL_SIZE = 10;
+ private static final Object sPoolLock = new Object();
+ private static AccessibilityRecord sPool;
+ private static int sPoolSize;
+ private AccessibilityRecord mNext;
+ private boolean mIsInPool;
+
+ @UnsupportedAppUsage
+ boolean mSealed;
+ int mBooleanProperties = 0;
+ int mCurrentItemIndex = UNDEFINED;
+ int mItemCount = UNDEFINED;
+ int mFromIndex = UNDEFINED;
+ int mToIndex = UNDEFINED;
+ int mScrollX = 0;
+ int mScrollY = 0;
+
+ int mScrollDeltaX = UNDEFINED;
+ int mScrollDeltaY = UNDEFINED;
+ int mMaxScrollX = 0;
+ int mMaxScrollY = 0;
+
+ int mAddedCount= UNDEFINED;
+ int mRemovedCount = UNDEFINED;
+ @UnsupportedAppUsage
+ long mSourceNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+ int mSourceWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+
+ CharSequence mClassName;
+ CharSequence mContentDescription;
+ CharSequence mBeforeText;
+ Parcelable mParcelableData;
+
+ final List<CharSequence> mText = new ArrayList<CharSequence>();
+
+ int mConnectionId = UNDEFINED;
+
+ /**
+ * Creates a new {@link AccessibilityRecord}.
+ */
+ public AccessibilityRecord() {
+ }
+
+ /**
+ * Copy constructor. Creates a new {@link AccessibilityRecord}, and this instance is initialized
+ * with data from the given <code>record</code>.
+ *
+ * @param record The other record.
+ */
+ public AccessibilityRecord(@NonNull AccessibilityRecord record) {
+ init(record);
+ }
+
+ /**
+ * Sets the event source.
+ *
+ * @param source The source.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setSource(View source) {
+ setSource(source, AccessibilityNodeProvider.HOST_VIEW_ID);
+ }
+
+ /**
+ * Sets the source to be a virtual descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root
+ * is set as the source.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setSource(@Nullable View root, int virtualDescendantId) {
+ enforceNotSealed();
+ boolean important = true;
+ int rootViewId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ mSourceWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ if (root != null) {
+ important = root.isImportantForAccessibility();
+ rootViewId = root.getAccessibilityViewId();
+ mSourceWindowId = root.getAccessibilityWindowId();
+ }
+ setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, important);
+ mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId);
+ }
+
+ /**
+ * Set the source node ID directly
+ *
+ * @param sourceNodeId The source node Id
+ * @hide
+ */
+ public void setSourceNodeId(long sourceNodeId) {
+ mSourceNodeId = sourceNodeId;
+ }
+
+ /**
+ * Gets the {@link AccessibilityNodeInfo} of the event source.
+ * <p>
+ * <strong>Note:</strong> It is a client responsibility to recycle the received info
+ * by calling {@link AccessibilityNodeInfo#recycle() AccessibilityNodeInfo#recycle()}
+ * to avoid creating of multiple instances.
+ * </p>
+ * @return The info of the source.
+ */
+ public AccessibilityNodeInfo getSource() {
+ enforceSealed();
+ if ((mConnectionId == UNDEFINED)
+ || (mSourceWindowId == AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)
+ || (AccessibilityNodeInfo.getAccessibilityViewId(mSourceNodeId)
+ == AccessibilityNodeInfo.UNDEFINED_ITEM_ID)) {
+ return null;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mSourceWindowId,
+ mSourceNodeId, false, GET_SOURCE_PREFETCH_FLAGS, null);
+ }
+
+ /**
+ * Sets the window id.
+ *
+ * @param windowId The window id.
+ *
+ * @hide
+ */
+ public void setWindowId(int windowId) {
+ mSourceWindowId = windowId;
+ }
+
+ /**
+ * Gets the id of the window from which the event comes from.
+ *
+ * @return The window id.
+ */
+ public int getWindowId() {
+ return mSourceWindowId;
+ }
+
+ /**
+ * Gets if the source is checked.
+ *
+ * @return True if the view is checked, false otherwise.
+ */
+ public boolean isChecked() {
+ return getBooleanProperty(PROPERTY_CHECKED);
+ }
+
+ /**
+ * Sets if the source is checked.
+ *
+ * @param isChecked True if the view is checked, false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setChecked(boolean isChecked) {
+ enforceNotSealed();
+ setBooleanProperty(PROPERTY_CHECKED, isChecked);
+ }
+
+ /**
+ * Gets if the source is enabled.
+ *
+ * @return True if the view is enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ return getBooleanProperty(PROPERTY_ENABLED);
+ }
+
+ /**
+ * Sets if the source is enabled.
+ *
+ * @param isEnabled True if the view is enabled, false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setEnabled(boolean isEnabled) {
+ enforceNotSealed();
+ setBooleanProperty(PROPERTY_ENABLED, isEnabled);
+ }
+
+ /**
+ * Gets if the source is a password field.
+ *
+ * @return True if the view is a password field, false otherwise.
+ */
+ public boolean isPassword() {
+ return getBooleanProperty(PROPERTY_PASSWORD);
+ }
+
+ /**
+ * Sets if the source is a password field.
+ *
+ * @param isPassword True if the view is a password field, false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setPassword(boolean isPassword) {
+ enforceNotSealed();
+ setBooleanProperty(PROPERTY_PASSWORD, isPassword);
+ }
+
+ /**
+ * Gets if the source is taking the entire screen.
+ *
+ * @return True if the source is full screen, false otherwise.
+ */
+ public boolean isFullScreen() {
+ return getBooleanProperty(PROPERTY_FULL_SCREEN);
+ }
+
+ /**
+ * Sets if the source is taking the entire screen.
+ *
+ * @param isFullScreen True if the source is full screen, false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setFullScreen(boolean isFullScreen) {
+ enforceNotSealed();
+ setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen);
+ }
+
+ /**
+ * Gets if the source is scrollable.
+ *
+ * @return True if the source is scrollable, false otherwise.
+ */
+ public boolean isScrollable() {
+ return getBooleanProperty(PROPERTY_SCROLLABLE);
+ }
+
+ /**
+ * Sets if the source is scrollable.
+ *
+ * @param scrollable True if the source is scrollable, false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setScrollable(boolean scrollable) {
+ enforceNotSealed();
+ setBooleanProperty(PROPERTY_SCROLLABLE, scrollable);
+ }
+
+ /**
+ * Gets if the source is important for accessibility.
+ *
+ * <strong>Note:</strong> Used only internally to determine whether
+ * to deliver the event to a given accessibility service since some
+ * services may want to regard all views for accessibility while others
+ * may want to regard only the important views for accessibility.
+ *
+ * @return True if the source is important for accessibility,
+ * false otherwise.
+ *
+ * @hide
+ */
+ public boolean isImportantForAccessibility() {
+ return getBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY);
+ }
+
+ /**
+ * Sets if the source is important for accessibility.
+ *
+ * @param importantForAccessibility True if the source is important for accessibility,
+ * false otherwise.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @hide
+ */
+ public void setImportantForAccessibility(boolean importantForAccessibility) {
+ enforceNotSealed();
+ setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, importantForAccessibility);
+ }
+
+ /**
+ * Gets the number of items that can be visited.
+ *
+ * @return The number of items.
+ */
+ public int getItemCount() {
+ return mItemCount;
+ }
+
+ /**
+ * Sets the number of items that can be visited.
+ *
+ * @param itemCount The number of items.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setItemCount(int itemCount) {
+ enforceNotSealed();
+ mItemCount = itemCount;
+ }
+
+ /**
+ * Gets the index of the source in the list of items the can be visited.
+ *
+ * @return The current item index.
+ */
+ public int getCurrentItemIndex() {
+ return mCurrentItemIndex;
+ }
+
+ /**
+ * Sets the index of the source in the list of items that can be visited.
+ *
+ * @param currentItemIndex The current item index.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setCurrentItemIndex(int currentItemIndex) {
+ enforceNotSealed();
+ mCurrentItemIndex = currentItemIndex;
+ }
+
+ /**
+ * Gets the index of the first character of the changed sequence,
+ * or the beginning of a text selection or the index of the first
+ * visible item when scrolling.
+ *
+ * @return The index of the first character or selection
+ * start or the first visible item.
+ */
+ public int getFromIndex() {
+ return mFromIndex;
+ }
+
+ /**
+ * Sets the index of the first character of the changed sequence
+ * or the beginning of a text selection or the index of the first
+ * visible item when scrolling.
+ *
+ * @param fromIndex The index of the first character or selection
+ * start or the first visible item.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setFromIndex(int fromIndex) {
+ enforceNotSealed();
+ mFromIndex = fromIndex;
+ }
+
+ /**
+ * Gets the index of text selection end or the index of the last
+ * visible item when scrolling.
+ *
+ * @return The index of selection end or last item index.
+ */
+ public int getToIndex() {
+ return mToIndex;
+ }
+
+ /**
+ * Sets the index of text selection end or the index of the last
+ * visible item when scrolling.
+ *
+ * @param toIndex The index of selection end or last item index.
+ */
+ public void setToIndex(int toIndex) {
+ enforceNotSealed();
+ mToIndex = toIndex;
+ }
+
+ /**
+ * Gets the scroll offset of the source left edge in pixels.
+ *
+ * @return The scroll.
+ */
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ /**
+ * Sets the scroll offset of the source left edge in pixels.
+ *
+ * @param scrollX The scroll.
+ */
+ public void setScrollX(int scrollX) {
+ enforceNotSealed();
+ mScrollX = scrollX;
+ }
+
+ /**
+ * Gets the scroll offset of the source top edge in pixels.
+ *
+ * @return The scroll.
+ */
+ public int getScrollY() {
+ return mScrollY;
+ }
+
+ /**
+ * Sets the scroll offset of the source top edge in pixels.
+ *
+ * @param scrollY The scroll.
+ */
+ public void setScrollY(int scrollY) {
+ enforceNotSealed();
+ mScrollY = scrollY;
+ }
+
+ /**
+ * Gets the difference in pixels between the horizontal position before the scroll and the
+ * current horizontal position
+ *
+ * @return the scroll delta x
+ */
+ public int getScrollDeltaX() {
+ return mScrollDeltaX;
+ }
+
+ /**
+ * Sets the difference in pixels between the horizontal position before the scroll and the
+ * current horizontal position
+ *
+ * @param scrollDeltaX the scroll delta x
+ */
+ public void setScrollDeltaX(int scrollDeltaX) {
+ enforceNotSealed();
+ mScrollDeltaX = scrollDeltaX;
+ }
+
+ /**
+ * Gets the difference in pixels between the vertical position before the scroll and the
+ * current vertical position
+ *
+ * @return the scroll delta y
+ */
+ public int getScrollDeltaY() {
+ return mScrollDeltaY;
+ }
+
+ /**
+ * Sets the difference in pixels between the vertical position before the scroll and the
+ * current vertical position
+ *
+ * @param scrollDeltaY the scroll delta y
+ */
+ public void setScrollDeltaY(int scrollDeltaY) {
+ enforceNotSealed();
+ mScrollDeltaY = scrollDeltaY;
+ }
+
+ /**
+ * Gets the max scroll offset of the source left edge in pixels.
+ *
+ * @return The max scroll.
+ */
+ public int getMaxScrollX() {
+ return mMaxScrollX;
+ }
+
+ /**
+ * Sets the max scroll offset of the source left edge in pixels.
+ *
+ * @param maxScrollX The max scroll.
+ */
+ public void setMaxScrollX(int maxScrollX) {
+ enforceNotSealed();
+ mMaxScrollX = maxScrollX;
+ }
+
+ /**
+ * Gets the max scroll offset of the source top edge in pixels.
+ *
+ * @return The max scroll.
+ */
+ public int getMaxScrollY() {
+ return mMaxScrollY;
+ }
+
+ /**
+ * Sets the max scroll offset of the source top edge in pixels.
+ *
+ * @param maxScrollY The max scroll.
+ */
+ public void setMaxScrollY(int maxScrollY) {
+ enforceNotSealed();
+ mMaxScrollY = maxScrollY;
+ }
+
+ /**
+ * Gets the number of added characters.
+ *
+ * @return The number of added characters.
+ */
+ public int getAddedCount() {
+ return mAddedCount;
+ }
+
+ /**
+ * Sets the number of added characters.
+ *
+ * @param addedCount The number of added characters.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setAddedCount(int addedCount) {
+ enforceNotSealed();
+ mAddedCount = addedCount;
+ }
+
+ /**
+ * Gets the number of removed characters.
+ *
+ * @return The number of removed characters.
+ */
+ public int getRemovedCount() {
+ return mRemovedCount;
+ }
+
+ /**
+ * Sets the number of removed characters.
+ *
+ * @param removedCount The number of removed characters.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setRemovedCount(int removedCount) {
+ enforceNotSealed();
+ mRemovedCount = removedCount;
+ }
+
+ /**
+ * Gets the class name of the source.
+ *
+ * @return The class name.
+ */
+ public CharSequence getClassName() {
+ return mClassName;
+ }
+
+ /**
+ * Sets the class name of the source.
+ *
+ * @param className The lass name.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setClassName(CharSequence className) {
+ enforceNotSealed();
+ mClassName = className;
+ }
+
+ /**
+ * Gets the text of the event. The index in the list represents the priority
+ * of the text. Specifically, the lower the index the higher the priority.
+ *
+ * @return The text.
+ */
+ public List<CharSequence> getText() {
+ return mText;
+ }
+
+ /**
+ * Gets the text before a change.
+ *
+ * @return The text before the change.
+ */
+ public CharSequence getBeforeText() {
+ return mBeforeText;
+ }
+
+ /**
+ * Sets the text before a change.
+ *
+ * @param beforeText The text before the change.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setBeforeText(CharSequence beforeText) {
+ enforceNotSealed();
+ mBeforeText = (beforeText == null) ? null
+ : beforeText.subSequence(0, beforeText.length());
+ }
+
+ /**
+ * Gets the description of the source.
+ *
+ * @return The description.
+ */
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * Sets the description of the source.
+ *
+ * @param contentDescription The description.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setContentDescription(CharSequence contentDescription) {
+ enforceNotSealed();
+ mContentDescription = (contentDescription == null) ? null
+ : contentDescription.subSequence(0, contentDescription.length());
+ }
+
+ /**
+ * Gets the {@link Parcelable} data.
+ *
+ * @return The parcelable data.
+ */
+ public Parcelable getParcelableData() {
+ return mParcelableData;
+ }
+
+ /**
+ * Sets the {@link Parcelable} data of the event.
+ *
+ * @param parcelableData The parcelable data.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setParcelableData(Parcelable parcelableData) {
+ enforceNotSealed();
+ mParcelableData = parcelableData;
+ }
+
+ /**
+ * Gets the id of the source node.
+ *
+ * @return The id.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public long getSourceNodeId() {
+ return mSourceNodeId;
+ }
+
+ /**
+ * Sets the unique id of the IAccessibilityServiceConnection over which
+ * this instance can send requests to the system.
+ *
+ * @param connectionId The connection id.
+ *
+ * @hide
+ */
+ public void setConnectionId(int connectionId) {
+ enforceNotSealed();
+ mConnectionId = connectionId;
+ }
+
+ /**
+ * Sets if this instance is sealed.
+ *
+ * @param sealed Whether is sealed.
+ *
+ * @hide
+ */
+ public void setSealed(boolean sealed) {
+ mSealed = sealed;
+ }
+
+ /**
+ * Gets if this instance is sealed.
+ *
+ * @return Whether is sealed.
+ */
+ boolean isSealed() {
+ return mSealed;
+ }
+
+ /**
+ * Enforces that this instance is sealed.
+ *
+ * @throws IllegalStateException If this instance is not sealed.
+ */
+ void enforceSealed() {
+ if (!isSealed()) {
+ throw new IllegalStateException("Cannot perform this "
+ + "action on a not sealed instance.");
+ }
+ }
+
+ /**
+ * Enforces that this instance is not sealed.
+ *
+ * @throws IllegalStateException If this instance is sealed.
+ */
+ void enforceNotSealed() {
+ if (isSealed()) {
+ throw new IllegalStateException("Cannot perform this "
+ + "action on a sealed instance.");
+ }
+ }
+
+ /**
+ * Gets the value of a boolean property.
+ *
+ * @param property The property.
+ * @return The value.
+ */
+ private boolean getBooleanProperty(int property) {
+ return (mBooleanProperties & property) == property;
+ }
+
+ /**
+ * Sets a boolean property.
+ *
+ * @param property The property.
+ * @param value The value.
+ */
+ private void setBooleanProperty(int property, boolean value) {
+ if (value) {
+ mBooleanProperties |= property;
+ } else {
+ mBooleanProperties &= ~property;
+ }
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * instantiated. The instance is initialized with data from the
+ * given record.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityRecord(AccessibilityRecord)} instead.
+ *
+ * @return An instance.
+ */
+ public static AccessibilityRecord obtain(AccessibilityRecord record) {
+ AccessibilityRecord clone = AccessibilityRecord.obtain();
+ clone.init(record);
+ return clone;
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * instantiated.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityRecord()} instead.
+ *
+ * @return An instance.
+ */
+ public static AccessibilityRecord obtain() {
+ synchronized (sPoolLock) {
+ if (sPool != null) {
+ AccessibilityRecord record = sPool;
+ sPool = sPool.mNext;
+ sPoolSize--;
+ record.mNext = null;
+ record.mIsInPool = false;
+ return record;
+ }
+ return new AccessibilityRecord();
+ }
+ }
+
+ /**
+ * Return an instance back to be reused.
+ * <p>
+ * <strong>Note:</strong> You must not touch the object after calling this function.
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ *
+ * @throws IllegalStateException If the record is already recycled.
+ */
+ public void recycle() {
+ if (mIsInPool) {
+ throw new IllegalStateException("Record already recycled!");
+ }
+ clear();
+ synchronized (sPoolLock) {
+ if (sPoolSize <= MAX_POOL_SIZE) {
+ mNext = sPool;
+ sPool = this;
+ mIsInPool = true;
+ sPoolSize++;
+ }
+ }
+ }
+
+ /**
+ * Initialize this record from another one.
+ *
+ * @param record The to initialize from.
+ */
+ void init(AccessibilityRecord record) {
+ mSealed = record.mSealed;
+ mBooleanProperties = record.mBooleanProperties;
+ mCurrentItemIndex = record.mCurrentItemIndex;
+ mItemCount = record.mItemCount;
+ mFromIndex = record.mFromIndex;
+ mToIndex = record.mToIndex;
+ mScrollX = record.mScrollX;
+ mScrollY = record.mScrollY;
+ mMaxScrollX = record.mMaxScrollX;
+ mMaxScrollY = record.mMaxScrollY;
+ mScrollDeltaX = record.mScrollDeltaX;
+ mScrollDeltaY = record.mScrollDeltaY;
+ mAddedCount = record.mAddedCount;
+ mRemovedCount = record.mRemovedCount;
+ mClassName = record.mClassName;
+ mContentDescription = record.mContentDescription;
+ mBeforeText = record.mBeforeText;
+ mParcelableData = record.mParcelableData;
+ mText.addAll(record.mText);
+ mSourceWindowId = record.mSourceWindowId;
+ mSourceNodeId = record.mSourceNodeId;
+ mConnectionId = record.mConnectionId;
+ }
+
+ /**
+ * Clears the state of this instance.
+ */
+ void clear() {
+ mSealed = false;
+ mBooleanProperties = 0;
+ mCurrentItemIndex = UNDEFINED;
+ mItemCount = UNDEFINED;
+ mFromIndex = UNDEFINED;
+ mToIndex = UNDEFINED;
+ mScrollX = 0;
+ mScrollY = 0;
+ mMaxScrollX = 0;
+ mMaxScrollY = 0;
+ mScrollDeltaX = UNDEFINED;
+ mScrollDeltaY = UNDEFINED;
+ mAddedCount = UNDEFINED;
+ mRemovedCount = UNDEFINED;
+ mClassName = null;
+ mContentDescription = null;
+ mBeforeText = null;
+ mParcelableData = null;
+ mText.clear();
+ mSourceNodeId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ mSourceWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ mConnectionId = UNDEFINED;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ StringBuilder appendTo(StringBuilder builder) {
+ builder.append(" [ ClassName: ").append(mClassName);
+ if (!DEBUG_CONCISE_TOSTRING || !isEmpty(mText)) {
+ appendPropName(builder, "Text").append(mText);
+ }
+ append(builder, "ContentDescription", mContentDescription);
+ append(builder, "ItemCount", mItemCount);
+ append(builder, "CurrentItemIndex", mCurrentItemIndex);
+
+ appendUnless(true, PROPERTY_ENABLED, builder);
+ appendUnless(false, PROPERTY_PASSWORD, builder);
+ appendUnless(false, PROPERTY_CHECKED, builder);
+ appendUnless(false, PROPERTY_FULL_SCREEN, builder);
+ appendUnless(false, PROPERTY_SCROLLABLE, builder);
+
+ append(builder, "BeforeText", mBeforeText);
+ append(builder, "FromIndex", mFromIndex);
+ append(builder, "ToIndex", mToIndex);
+ append(builder, "ScrollX", mScrollX);
+ append(builder, "ScrollY", mScrollY);
+ append(builder, "MaxScrollX", mMaxScrollX);
+ append(builder, "MaxScrollY", mMaxScrollY);
+ append(builder, "ScrollDeltaX", mScrollDeltaX);
+ append(builder, "ScrollDeltaY", mScrollDeltaY);
+ append(builder, "AddedCount", mAddedCount);
+ append(builder, "RemovedCount", mRemovedCount);
+ append(builder, "ParcelableData", mParcelableData);
+ builder.append(" ]");
+ return builder;
+ }
+
+ private void appendUnless(boolean defValue, int prop, StringBuilder builder) {
+ boolean value = getBooleanProperty(prop);
+ if (DEBUG_CONCISE_TOSTRING && value == defValue) return;
+ appendPropName(builder, singleBooleanPropertyToString(prop))
+ .append(value);
+ }
+
+ private static String singleBooleanPropertyToString(int prop) {
+ switch (prop) {
+ case PROPERTY_CHECKED: return "Checked";
+ case PROPERTY_ENABLED: return "Enabled";
+ case PROPERTY_PASSWORD: return "Password";
+ case PROPERTY_FULL_SCREEN: return "FullScreen";
+ case PROPERTY_SCROLLABLE: return "Scrollable";
+ case PROPERTY_IMPORTANT_FOR_ACCESSIBILITY:
+ return "ImportantForAccessibility";
+ default: return Integer.toHexString(prop);
+ }
+ }
+
+ private void append(StringBuilder builder, String propName, int propValue) {
+ if (DEBUG_CONCISE_TOSTRING && propValue == UNDEFINED) return;
+ appendPropName(builder, propName).append(propValue);
+ }
+
+ private void append(StringBuilder builder, String propName, Object propValue) {
+ if (DEBUG_CONCISE_TOSTRING && propValue == null) return;
+ appendPropName(builder, propName).append(propValue);
+ }
+
+ private StringBuilder appendPropName(StringBuilder builder, String propName) {
+ return builder.append("; ").append(propName).append(": ");
+ }
+}
diff --git a/android/view/accessibility/AccessibilityRequestPreparer.java b/android/view/accessibility/AccessibilityRequestPreparer.java
new file mode 100644
index 0000000..8108d37
--- /dev/null
+++ b/android/view/accessibility/AccessibilityRequestPreparer.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+
+/**
+ * Object responsible to ensuring that a {@link View} is prepared to meet a synchronous request for
+ * accessibility data.
+ * <p>
+ * Because accessibility requests arrive to {@link View}s synchronously on the UI thread, a View
+ * that requires information from other processes can struggle to meet those requests. Registering
+ * an instance of this class with {@link AccessibilityManager} allows a View to be notified when
+ * a request is about to be made, and to asynchronously inform the accessibility system when it is
+ * ready to meet the request.
+ * <p>
+ * <strong>Note:</strong> This class should only be needed in exceptional situations where a
+ * {@link View} cannot otherwise synchronously meet the request for accessibility data.
+ */
+public abstract class AccessibilityRequestPreparer {
+ public static final int REQUEST_TYPE_EXTRA_DATA = 0x00000001;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "REQUEST_TYPE_" }, value = {
+ REQUEST_TYPE_EXTRA_DATA
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RequestTypes {}
+
+ private final WeakReference<View> mViewRef;
+ private final int mAccessibilityViewId;
+ private final int mRequestTypes;
+
+ /**
+ * @param view The view whose requests need preparation. It must be attached to a
+ * window. This object will retain a weak reference to this view, and will unregister itself
+ * from AccessibilityManager if the view is detached from a window. It will not re-register
+ * itself.
+ * @param requestTypes The types of requests that require preparation. Different types may
+ * be ORed together.
+ *
+ * @throws IllegalStateException if the view is not attached to a window.
+ */
+ public AccessibilityRequestPreparer(View view, @RequestTypes int requestTypes) {
+ if (!view.isAttachedToWindow()) {
+ throw new IllegalStateException("View must be attached to a window");
+ }
+ mViewRef = new WeakReference<>(view);
+ mAccessibilityViewId = view.getAccessibilityViewId();
+ mRequestTypes = requestTypes;
+ view.addOnAttachStateChangeListener(new ViewAttachStateListener());
+ }
+
+ /**
+ * Callback to allow preparation for filling extra data. Only called back if
+ * REQUEST_TYPE_EXTRA_DATA is requested.
+ *
+ * @param virtualViewId The ID of a virtual child node, if the {@link View} for this preparer
+ * supports virtual descendents, or {@link AccessibilityNodeProvider#HOST_VIEW_ID}
+ * if the request is for the view itself.
+ * @param extraDataKey The extra data key for the request
+ * @param args The arguments for the request
+ * @param preparationFinishedMessage A message that must be sent to its target when preparations
+ * are complete.
+ *
+ * @see View#addExtraDataToAccessibilityNodeInfo(AccessibilityNodeInfo, String, Bundle)
+ * @see View.AccessibilityDelegate#addExtraDataToAccessibilityNodeInfo(
+ * View, AccessibilityNodeInfo, String, Bundle)
+ * @see AccessibilityNodeProvider#addExtraDataToAccessibilityNodeInfo(
+ * int, AccessibilityNodeInfo, String, Bundle)
+ */
+ public abstract void onPrepareExtraData(int virtualViewId, String extraDataKey,
+ Bundle args, Message preparationFinishedMessage);
+
+ /**
+ * Get the view this object was created with.
+ *
+ * @return The view this object was created with, or {@code null} if the weak reference held
+ * to the view is no longer valid.
+ */
+ public @Nullable View getView() {
+ return mViewRef.get();
+ }
+
+ private class ViewAttachStateListener implements View.OnAttachStateChangeListener {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ Context context = v.getContext();
+ if (context != null) {
+ context.getSystemService(AccessibilityManager.class)
+ .removeAccessibilityRequestPreparer(AccessibilityRequestPreparer.this);
+ }
+ v.removeOnAttachStateChangeListener(this);
+ }
+ }
+
+ int getAccessibilityViewId() {
+ return mAccessibilityViewId;
+ }
+}
diff --git a/android/view/accessibility/AccessibilityWindowInfo.java b/android/view/accessibility/AccessibilityWindowInfo.java
new file mode 100644
index 0000000..edcb59a
--- /dev/null
+++ b/android/view/accessibility/AccessibilityWindowInfo.java
@@ -0,0 +1,893 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.LongArray;
+import android.util.Pools.SynchronizedPool;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.accessibility.AccessibilityEvent.WindowsChangeTypes;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This class represents a state snapshot of a window for accessibility
+ * purposes. The screen content contains one or more windows where some
+ * windows can be descendants of other windows, which is the windows are
+ * hierarchically ordered. Note that there is no root window. Hence, the
+ * screen content can be seen as a collection of window trees.
+ */
+public final class AccessibilityWindowInfo implements Parcelable {
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * Window type: This is an application window. Such a window shows UI for
+ * interacting with an application.
+ */
+ public static final int TYPE_APPLICATION = 1;
+
+ /**
+ * Window type: This is an input method window. Such a window shows UI for
+ * inputting text such as keyboard, suggestions, etc.
+ */
+ public static final int TYPE_INPUT_METHOD = 2;
+
+ /**
+ * Window type: This is a system window. Such a window shows UI for
+ * interacting with the system.
+ */
+ public static final int TYPE_SYSTEM = 3;
+
+ /**
+ * Window type: Windows that are overlaid <em>only</em> by an {@link
+ * android.accessibilityservice.AccessibilityService} for interception of
+ * user interactions without changing the windows an accessibility service
+ * can introspect. In particular, an accessibility service can introspect
+ * only windows that a sighted user can interact with which they can touch
+ * these windows or can type into these windows. For example, if there
+ * is a full screen accessibility overlay that is touchable, the windows
+ * below it will be introspectable by an accessibility service regardless
+ * they are covered by a touchable window.
+ */
+ public static final int TYPE_ACCESSIBILITY_OVERLAY = 4;
+
+ /**
+ * Window type: A system window used to divide the screen in split-screen mode.
+ * This type of window is present only in split-screen mode.
+ */
+ public static final int TYPE_SPLIT_SCREEN_DIVIDER = 5;
+
+ /* Special values for window IDs */
+ /** @hide */
+ public static final int ACTIVE_WINDOW_ID = Integer.MAX_VALUE;
+ /** @hide */
+ public static final int UNDEFINED_CONNECTION_ID = -1;
+ /** @hide */
+ public static final int UNDEFINED_WINDOW_ID = -1;
+ /** @hide */
+ public static final int ANY_WINDOW_ID = -2;
+ /** @hide */
+ public static final int PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID = -3;
+
+ private static final int BOOLEAN_PROPERTY_ACTIVE = 1 << 0;
+ private static final int BOOLEAN_PROPERTY_FOCUSED = 1 << 1;
+ private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 1 << 2;
+ private static final int BOOLEAN_PROPERTY_PICTURE_IN_PICTURE = 1 << 3;
+
+ // Housekeeping.
+ private static final int MAX_POOL_SIZE = 10;
+ private static final SynchronizedPool<AccessibilityWindowInfo> sPool =
+ new SynchronizedPool<AccessibilityWindowInfo>(MAX_POOL_SIZE);
+ // TODO(b/129300068): Remove sNumInstancesInUse.
+ private static AtomicInteger sNumInstancesInUse;
+
+ // Data.
+ private int mDisplayId = Display.INVALID_DISPLAY;
+ private int mType = UNDEFINED_WINDOW_ID;
+ private int mLayer = UNDEFINED_WINDOW_ID;
+ private int mBooleanProperties;
+ private int mId = UNDEFINED_WINDOW_ID;
+ private int mParentId = UNDEFINED_WINDOW_ID;
+ private Region mRegionInScreen = new Region();
+ private LongArray mChildIds;
+ private CharSequence mTitle;
+ private long mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+
+ private int mConnectionId = UNDEFINED_CONNECTION_ID;
+
+ /**
+ * Creates a new {@link AccessibilityWindowInfo}.
+ */
+ public AccessibilityWindowInfo() {
+ }
+
+ /**
+ * Copy constructor. Creates a new {@link AccessibilityWindowInfo}, and this new instance is
+ * initialized from given <code>info</code>.
+ *
+ * @param info The other info.
+ */
+ public AccessibilityWindowInfo(@NonNull AccessibilityWindowInfo info) {
+ init(info);
+ }
+
+ /**
+ * Gets the title of the window.
+ *
+ * @return The title of the window, or {@code null} if none is available.
+ */
+ @Nullable
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets the title of the window.
+ *
+ * @param title The title.
+ *
+ * @hide
+ */
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ }
+
+ /**
+ * Gets the type of the window.
+ *
+ * @return The type.
+ *
+ * @see #TYPE_APPLICATION
+ * @see #TYPE_INPUT_METHOD
+ * @see #TYPE_SYSTEM
+ * @see #TYPE_ACCESSIBILITY_OVERLAY
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Sets the type of the window.
+ *
+ * @param type The type
+ *
+ * @hide
+ */
+ public void setType(int type) {
+ mType = type;
+ }
+
+ /**
+ * Gets the layer which determines the Z-order of the window. Windows
+ * with greater layer appear on top of windows with lesser layer.
+ *
+ * @return The window layer.
+ */
+ public int getLayer() {
+ return mLayer;
+ }
+
+ /**
+ * Sets the layer which determines the Z-order of the window. Windows
+ * with greater layer appear on top of windows with lesser layer.
+ *
+ * @param layer The window layer.
+ *
+ * @hide
+ */
+ public void setLayer(int layer) {
+ mLayer = layer;
+ }
+
+ /**
+ * Gets the root node in the window's hierarchy.
+ *
+ * @return The root node.
+ */
+ public AccessibilityNodeInfo getRoot() {
+ if (mConnectionId == UNDEFINED_WINDOW_ID) {
+ return null;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
+ mId, AccessibilityNodeInfo.ROOT_NODE_ID,
+ true, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
+ }
+
+ /**
+ * Sets the anchor node's ID.
+ *
+ * @param anchorId The anchor's accessibility id in its window.
+ *
+ * @hide
+ */
+ public void setAnchorId(long anchorId) {
+ mAnchorId = anchorId;
+ }
+
+ /**
+ * Gets the node that anchors this window to another.
+ *
+ * @return The anchor node, or {@code null} if none exists.
+ */
+ public AccessibilityNodeInfo getAnchor() {
+ if ((mConnectionId == UNDEFINED_WINDOW_ID)
+ || (mAnchorId == AccessibilityNodeInfo.UNDEFINED_NODE_ID)
+ || (mParentId == UNDEFINED_WINDOW_ID)) {
+ return null;
+ }
+
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
+ mParentId, mAnchorId, true, 0, null);
+ }
+
+ /** @hide */
+ public void setPictureInPicture(boolean pictureInPicture) {
+ setBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE, pictureInPicture);
+ }
+
+ /**
+ * Check if the window is in picture-in-picture mode.
+ *
+ * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise.
+ */
+ public boolean isInPictureInPictureMode() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE);
+ }
+
+ /**
+ * Gets the parent window.
+ *
+ * @return The parent window, or {@code null} if none exists.
+ */
+ public AccessibilityWindowInfo getParent() {
+ if (mConnectionId == UNDEFINED_WINDOW_ID || mParentId == UNDEFINED_WINDOW_ID) {
+ return null;
+ }
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.getWindow(mConnectionId, mParentId);
+ }
+
+ /**
+ * Sets the parent window id.
+ *
+ * @param parentId The parent id.
+ *
+ * @hide
+ */
+ public void setParentId(int parentId) {
+ mParentId = parentId;
+ }
+
+ /**
+ * Gets the unique window id.
+ *
+ * @return windowId The window id.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Sets the unique window id.
+ *
+ * @param id The window id.
+ *
+ * @hide
+ */
+ public void setId(int id) {
+ mId = id;
+ }
+
+ /**
+ * Sets the unique id of the IAccessibilityServiceConnection over which
+ * this instance can send requests to the system.
+ *
+ * @param connectionId The connection id.
+ *
+ * @hide
+ */
+ public void setConnectionId(int connectionId) {
+ mConnectionId = connectionId;
+ }
+
+ /**
+ * Gets the touchable region of this window in the screen.
+ *
+ * @param outRegion The out window region.
+ */
+ public void getRegionInScreen(@NonNull Region outRegion) {
+ outRegion.set(mRegionInScreen);
+ }
+
+ /**
+ * Sets the touchable region of this window in the screen.
+ *
+ * @param region The window region.
+ *
+ * @hide
+ */
+ public void setRegionInScreen(Region region) {
+ mRegionInScreen.set(region);
+ }
+
+ /**
+ * Gets the bounds of this window in the screen. This is equivalent to get the bounds of the
+ * Region from {@link #getRegionInScreen(Region)}.
+ *
+ * @param outBounds The out window bounds.
+ */
+ public void getBoundsInScreen(Rect outBounds) {
+ outBounds.set(mRegionInScreen.getBounds());
+ }
+
+ /**
+ * Gets if this window is active. An active window is the one
+ * the user is currently touching or the window has input focus
+ * and the user is not touching any window.
+ *
+ * @return Whether this is the active window.
+ */
+ public boolean isActive() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE);
+ }
+
+ /**
+ * Sets if this window is active, which is this is the window
+ * the user is currently touching or the window has input focus
+ * and the user is not touching any window.
+ *
+ * @param active Whether this is the active window.
+ *
+ * @hide
+ */
+ public void setActive(boolean active) {
+ setBooleanProperty(BOOLEAN_PROPERTY_ACTIVE, active);
+ }
+
+ /**
+ * Gets if this window has input focus.
+ *
+ * @return Whether has input focus.
+ */
+ public boolean isFocused() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED);
+ }
+
+ /**
+ * Sets if this window has input focus.
+ *
+ * @param focused Whether has input focus.
+ *
+ * @hide
+ */
+ public void setFocused(boolean focused) {
+ setBooleanProperty(BOOLEAN_PROPERTY_FOCUSED, focused);
+ }
+
+ /**
+ * Gets if this window has accessibility focus.
+ *
+ * @return Whether has accessibility focus.
+ */
+ public boolean isAccessibilityFocused() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED);
+ }
+
+ /**
+ * Sets if this window has accessibility focus.
+ *
+ * @param focused Whether has accessibility focus.
+ *
+ * @hide
+ */
+ public void setAccessibilityFocused(boolean focused) {
+ setBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED, focused);
+ }
+
+ /**
+ * Gets the number of child windows.
+ *
+ * @return The child count.
+ */
+ public int getChildCount() {
+ return (mChildIds != null) ? mChildIds.size() : 0;
+ }
+
+ /**
+ * Gets the child window at a given index.
+ *
+ * @param index The index.
+ * @return The child.
+ */
+ public AccessibilityWindowInfo getChild(int index) {
+ if (mChildIds == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (mConnectionId == UNDEFINED_WINDOW_ID) {
+ return null;
+ }
+ final int childId = (int) mChildIds.get(index);
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ return client.getWindow(mConnectionId, childId);
+ }
+
+ /**
+ * Adds a child window.
+ *
+ * @param childId The child window id.
+ *
+ * @hide
+ */
+ public void addChild(int childId) {
+ if (mChildIds == null) {
+ mChildIds = new LongArray();
+ }
+ mChildIds.add(childId);
+ }
+
+ /**
+ * Sets the display Id.
+ *
+ * @param displayId The display id.
+ *
+ * @hide
+ */
+ public void setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ /**
+ * Returns the ID of the display this window is on, for use with
+ * {@link android.hardware.display.DisplayManager#getDisplay(int)}.
+ *
+ * @return The logical display id.
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * created.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityWindowInfo()} instead.
+ *
+ * @return An instance.
+ */
+ public static AccessibilityWindowInfo obtain() {
+ AccessibilityWindowInfo info = sPool.acquire();
+ if (info == null) {
+ info = new AccessibilityWindowInfo();
+ }
+ if (sNumInstancesInUse != null) {
+ sNumInstancesInUse.incrementAndGet();
+ }
+ return info;
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * created. The returned instance is initialized from the given
+ * <code>info</code>.
+ *
+ * <p>In most situations object pooling is not beneficial. Create a new instance using the
+ * constructor {@link #AccessibilityWindowInfo(AccessibilityWindowInfo)} instead.
+ *
+ * @param info The other info.
+ * @return An instance.
+ */
+ public static AccessibilityWindowInfo obtain(AccessibilityWindowInfo info) {
+ AccessibilityWindowInfo infoClone = obtain();
+ infoClone.init(info);
+ return infoClone;
+ }
+
+ /**
+ * Specify a counter that will be incremented on obtain() and decremented on recycle()
+ *
+ * @hide
+ */
+ @TestApi
+ public static void setNumInstancesInUseCounter(AtomicInteger counter) {
+ if (sNumInstancesInUse != null) {
+ sNumInstancesInUse = counter;
+ }
+ }
+
+ /**
+ * Return an instance back to be reused.
+ * <p>
+ * <strong>Note:</strong> You must not touch the object after calling this function.
+ * </p>
+ *
+ * <p>In most situations object pooling is not beneficial, and recycling is not necessary.
+ *
+ * @throws IllegalStateException If the info is already recycled.
+ */
+ public void recycle() {
+ clear();
+ sPool.release(this);
+ if (sNumInstancesInUse != null) {
+ sNumInstancesInUse.decrementAndGet();
+ }
+ }
+
+ /**
+ * Refreshes this window with the latest state of the window it represents.
+ * <p>
+ * <strong>Note:</strong> If this method returns false this info is obsolete
+ * since it represents a window that is no longer exist.
+ * </p>
+ *
+ * @hide
+ */
+ public boolean refresh() {
+ if (mConnectionId == UNDEFINED_CONNECTION_ID || mId == UNDEFINED_WINDOW_ID) {
+ return false;
+ }
+ final AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ final AccessibilityWindowInfo refreshedInfo = client.getWindow(mConnectionId,
+ mId, /* bypassCache */true);
+ if (refreshedInfo == null) {
+ return false;
+ }
+ init(refreshedInfo);
+ refreshedInfo.recycle();
+ return true;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mDisplayId);
+ parcel.writeInt(mType);
+ parcel.writeInt(mLayer);
+ parcel.writeInt(mBooleanProperties);
+ parcel.writeInt(mId);
+ parcel.writeInt(mParentId);
+ mRegionInScreen.writeToParcel(parcel, flags);
+ parcel.writeCharSequence(mTitle);
+ parcel.writeLong(mAnchorId);
+
+ final LongArray childIds = mChildIds;
+ if (childIds == null) {
+ parcel.writeInt(0);
+ } else {
+ final int childCount = childIds.size();
+ parcel.writeInt(childCount);
+ for (int i = 0; i < childCount; i++) {
+ parcel.writeInt((int) childIds.get(i));
+ }
+ }
+
+ parcel.writeInt(mConnectionId);
+ }
+
+ /**
+ * Initializes this instance from another one.
+ *
+ * @param other The other instance.
+ */
+ private void init(AccessibilityWindowInfo other) {
+ mDisplayId = other.mDisplayId;
+ mType = other.mType;
+ mLayer = other.mLayer;
+ mBooleanProperties = other.mBooleanProperties;
+ mId = other.mId;
+ mParentId = other.mParentId;
+ mRegionInScreen.set(other.mRegionInScreen);
+ mTitle = other.mTitle;
+ mAnchorId = other.mAnchorId;
+
+ if (mChildIds != null) mChildIds.clear();
+ if (other.mChildIds != null && other.mChildIds.size() > 0) {
+ if (mChildIds == null) {
+ mChildIds = other.mChildIds.clone();
+ } else {
+ mChildIds.addAll(other.mChildIds);
+ }
+ }
+
+ mConnectionId = other.mConnectionId;
+ }
+
+ private void initFromParcel(Parcel parcel) {
+ mDisplayId = parcel.readInt();
+ mType = parcel.readInt();
+ mLayer = parcel.readInt();
+ mBooleanProperties = parcel.readInt();
+ mId = parcel.readInt();
+ mParentId = parcel.readInt();
+ mRegionInScreen = Region.CREATOR.createFromParcel(parcel);
+ mTitle = parcel.readCharSequence();
+ mAnchorId = parcel.readLong();
+
+ final int childCount = parcel.readInt();
+ if (childCount > 0) {
+ if (mChildIds == null) {
+ mChildIds = new LongArray(childCount);
+ }
+ for (int i = 0; i < childCount; i++) {
+ final int childId = parcel.readInt();
+ mChildIds.add(childId);
+ }
+ }
+
+ mConnectionId = parcel.readInt();
+ }
+
+ @Override
+ public int hashCode() {
+ return mId;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ AccessibilityWindowInfo other = (AccessibilityWindowInfo) obj;
+ return (mId == other.mId);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AccessibilityWindowInfo[");
+ builder.append("title=").append(mTitle);
+ builder.append(", displayId=").append(mDisplayId);
+ builder.append(", id=").append(mId);
+ builder.append(", type=").append(typeToString(mType));
+ builder.append(", layer=").append(mLayer);
+ builder.append(", region=").append(mRegionInScreen);
+ builder.append(", bounds=").append(mRegionInScreen.getBounds());
+ builder.append(", focused=").append(isFocused());
+ builder.append(", active=").append(isActive());
+ builder.append(", pictureInPicture=").append(isInPictureInPictureMode());
+ if (DEBUG) {
+ builder.append(", parent=").append(mParentId);
+ builder.append(", children=[");
+ if (mChildIds != null) {
+ final int childCount = mChildIds.size();
+ for (int i = 0; i < childCount; i++) {
+ builder.append(mChildIds.get(i));
+ if (i < childCount - 1) {
+ builder.append(',');
+ }
+ }
+ } else {
+ builder.append("null");
+ }
+ builder.append(']');
+ } else {
+ builder.append(", hasParent=").append(mParentId != UNDEFINED_WINDOW_ID);
+ builder.append(", isAnchored=")
+ .append(mAnchorId != AccessibilityNodeInfo.UNDEFINED_NODE_ID);
+ builder.append(", hasChildren=").append(mChildIds != null
+ && mChildIds.size() > 0);
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+
+ /**
+ * Clears the internal state.
+ */
+ private void clear() {
+ mDisplayId = Display.INVALID_DISPLAY;
+ mType = UNDEFINED_WINDOW_ID;
+ mLayer = UNDEFINED_WINDOW_ID;
+ mBooleanProperties = 0;
+ mId = UNDEFINED_WINDOW_ID;
+ mParentId = UNDEFINED_WINDOW_ID;
+ mRegionInScreen.setEmpty();
+ mChildIds = null;
+ mConnectionId = UNDEFINED_WINDOW_ID;
+ mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+ mTitle = null;
+ }
+
+ /**
+ * Gets the value of a boolean property.
+ *
+ * @param property The property.
+ * @return The value.
+ */
+ private boolean getBooleanProperty(int property) {
+ return (mBooleanProperties & property) != 0;
+ }
+
+ /**
+ * Sets a boolean property.
+ *
+ * @param property The property.
+ * @param value The value.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ private void setBooleanProperty(int property, boolean value) {
+ if (value) {
+ mBooleanProperties |= property;
+ } else {
+ mBooleanProperties &= ~property;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static String typeToString(int type) {
+ switch (type) {
+ case TYPE_APPLICATION: {
+ return "TYPE_APPLICATION";
+ }
+ case TYPE_INPUT_METHOD: {
+ return "TYPE_INPUT_METHOD";
+ }
+ case TYPE_SYSTEM: {
+ return "TYPE_SYSTEM";
+ }
+ case TYPE_ACCESSIBILITY_OVERLAY: {
+ return "TYPE_ACCESSIBILITY_OVERLAY";
+ }
+ case TYPE_SPLIT_SCREEN_DIVIDER: {
+ return "TYPE_SPLIT_SCREEN_DIVIDER";
+ }
+ default:
+ return "<UNKNOWN:" + type + ">";
+ }
+ }
+
+ /**
+ * Reports how this window differs from a possibly different state of the same window. The
+ * argument must have the same id and type as neither of those properties may change.
+ *
+ * @param other The new state.
+ * @return A set of flags showing how the window has changes, or 0 if the two states are the
+ * same.
+ *
+ * @hide
+ */
+ @WindowsChangeTypes
+ public int differenceFrom(AccessibilityWindowInfo other) {
+ if (other.mId != mId) {
+ throw new IllegalArgumentException("Not same window.");
+ }
+ if (other.mType != mType) {
+ throw new IllegalArgumentException("Not same type.");
+ }
+ int changes = 0;
+ if (!TextUtils.equals(mTitle, other.mTitle)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_TITLE;
+ }
+ if (!mRegionInScreen.equals(other.mRegionInScreen)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_BOUNDS;
+ }
+ if (mLayer != other.mLayer) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_LAYER;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_ACTIVE;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_FOCUSED;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_PIP;
+ }
+ if (mParentId != other.mParentId) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_PARENT;
+ }
+ if (!Objects.equals(mChildIds, other.mChildIds)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_CHILDREN;
+ }
+ //TODO(b/1338122): Add DISPLAY_CHANGED type for multi-display
+ return changes;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AccessibilityWindowInfo> CREATOR =
+ new Creator<AccessibilityWindowInfo>() {
+ @Override
+ public AccessibilityWindowInfo createFromParcel(Parcel parcel) {
+ AccessibilityWindowInfo info = obtain();
+ info.initFromParcel(parcel);
+ return info;
+ }
+
+ @Override
+ public AccessibilityWindowInfo[] newArray(int size) {
+ return new AccessibilityWindowInfo[size];
+ }
+ };
+
+ /**
+ * Transfers a sparsearray with lists having {@link AccessibilityWindowInfo}s across an IPC.
+ * The key of this sparsearray is display Id.
+ *
+ * @hide
+ */
+ public static final class WindowListSparseArray
+ extends SparseArray<List<AccessibilityWindowInfo>> implements Parcelable {
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ final int count = size();
+ dest.writeInt(count);
+ for (int i = 0; i < count; i++) {
+ dest.writeParcelableList(valueAt(i), 0);
+ dest.writeInt(keyAt(i));
+ }
+ }
+
+ public static final Parcelable.Creator<WindowListSparseArray> CREATOR =
+ new Parcelable.Creator<WindowListSparseArray>() {
+ public WindowListSparseArray createFromParcel(
+ Parcel source) {
+ final WindowListSparseArray array = new WindowListSparseArray();
+ final ClassLoader loader = array.getClass().getClassLoader();
+ final int count = source.readInt();
+ for (int i = 0; i < count; i++) {
+ List<AccessibilityWindowInfo> windows = new ArrayList<>();
+ source.readParcelableList(windows, loader);
+ array.put(source.readInt(), windows);
+ }
+ return array;
+ }
+
+ public WindowListSparseArray[] newArray(int size) {
+ return new WindowListSparseArray[size];
+ }
+ };
+ }
+}
diff --git a/android/view/accessibility/CaptioningManager.java b/android/view/accessibility/CaptioningManager.java
new file mode 100644
index 0000000..3d68692
--- /dev/null
+++ b/android/view/accessibility/CaptioningManager.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings.Secure;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * Contains methods for accessing and monitoring preferred video captioning state and visual
+ * properties.
+ */
+@SystemService(Context.CAPTIONING_SERVICE)
+public class CaptioningManager {
+ /** Default captioning enabled value. */
+ private static final int DEFAULT_ENABLED = 0;
+
+ /** Default style preset as an index into {@link CaptionStyle#PRESETS}. */
+ private static final int DEFAULT_PRESET = 0;
+
+ /** Default scaling value for caption fonts. */
+ private static final float DEFAULT_FONT_SCALE = 1;
+
+ private final ArrayList<CaptioningChangeListener> mListeners = new ArrayList<>();
+ private final ContentResolver mContentResolver;
+ private final ContentObserver mContentObserver;
+
+ /**
+ * Creates a new captioning manager for the specified context.
+ *
+ * @hide
+ */
+ public CaptioningManager(Context context) {
+ mContentResolver = context.getContentResolver();
+
+ final Handler handler = new Handler(context.getMainLooper());
+ mContentObserver = new MyContentObserver(handler);
+ }
+
+ /**
+ * @return the user's preferred captioning enabled state
+ */
+ public final boolean isEnabled() {
+ return Secure.getInt(
+ mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_ENABLED, DEFAULT_ENABLED) == 1;
+ }
+
+ /**
+ * @return the raw locale string for the user's preferred captioning
+ * language
+ * @hide
+ */
+ @Nullable
+ public final String getRawLocale() {
+ return Secure.getString(mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_LOCALE);
+ }
+
+ /**
+ * @return the locale for the user's preferred captioning language, or null
+ * if not specified
+ */
+ @Nullable
+ public final Locale getLocale() {
+ final String rawLocale = getRawLocale();
+ if (!TextUtils.isEmpty(rawLocale)) {
+ final String[] splitLocale = rawLocale.split("_");
+ switch (splitLocale.length) {
+ case 3:
+ return new Locale(splitLocale[0], splitLocale[1], splitLocale[2]);
+ case 2:
+ return new Locale(splitLocale[0], splitLocale[1]);
+ case 1:
+ return new Locale(splitLocale[0]);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return the user's preferred font scaling factor for video captions, or 1 if not
+ * specified
+ */
+ public final float getFontScale() {
+ return Secure.getFloat(
+ mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, DEFAULT_FONT_SCALE);
+ }
+
+ /**
+ * @return the raw preset number, or the first preset if not specified
+ * @hide
+ */
+ public int getRawUserStyle() {
+ return Secure.getInt(
+ mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_PRESET, DEFAULT_PRESET);
+ }
+
+ /**
+ * @return the user's preferred visual properties for captions as a
+ * {@link CaptionStyle}, or the default style if not specified
+ */
+ @NonNull
+ public CaptionStyle getUserStyle() {
+ final int preset = getRawUserStyle();
+ if (preset == CaptionStyle.PRESET_CUSTOM) {
+ return CaptionStyle.getCustomStyle(mContentResolver);
+ }
+
+ return CaptionStyle.PRESETS[preset];
+ }
+
+ /**
+ * Adds a listener for changes in the user's preferred captioning enabled
+ * state and visual properties.
+ *
+ * @param listener the listener to add
+ */
+ public void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
+ synchronized (mListeners) {
+ if (mListeners.isEmpty()) {
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_LOCALE);
+ registerObserver(Secure.ACCESSIBILITY_CAPTIONING_PRESET);
+ }
+
+ mListeners.add(listener);
+ }
+ }
+
+ private void registerObserver(String key) {
+ mContentResolver.registerContentObserver(Secure.getUriFor(key), false, mContentObserver);
+ }
+
+ /**
+ * Removes a listener previously added using
+ * {@link #addCaptioningChangeListener}.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
+ synchronized (mListeners) {
+ mListeners.remove(listener);
+
+ if (mListeners.isEmpty()) {
+ mContentResolver.unregisterContentObserver(mContentObserver);
+ }
+ }
+ }
+
+ private void notifyEnabledChanged() {
+ final boolean enabled = isEnabled();
+ synchronized (mListeners) {
+ for (CaptioningChangeListener listener : mListeners) {
+ listener.onEnabledChanged(enabled);
+ }
+ }
+ }
+
+ private void notifyUserStyleChanged() {
+ final CaptionStyle userStyle = getUserStyle();
+ synchronized (mListeners) {
+ for (CaptioningChangeListener listener : mListeners) {
+ listener.onUserStyleChanged(userStyle);
+ }
+ }
+ }
+
+ private void notifyLocaleChanged() {
+ final Locale locale = getLocale();
+ synchronized (mListeners) {
+ for (CaptioningChangeListener listener : mListeners) {
+ listener.onLocaleChanged(locale);
+ }
+ }
+ }
+
+ private void notifyFontScaleChanged() {
+ final float fontScale = getFontScale();
+ synchronized (mListeners) {
+ for (CaptioningChangeListener listener : mListeners) {
+ listener.onFontScaleChanged(fontScale);
+ }
+ }
+ }
+
+ private class MyContentObserver extends ContentObserver {
+ private final Handler mHandler;
+
+ public MyContentObserver(Handler handler) {
+ super(handler);
+
+ mHandler = handler;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ final String uriPath = uri.getPath();
+ final String name = uriPath.substring(uriPath.lastIndexOf('/') + 1);
+ if (Secure.ACCESSIBILITY_CAPTIONING_ENABLED.equals(name)) {
+ notifyEnabledChanged();
+ } else if (Secure.ACCESSIBILITY_CAPTIONING_LOCALE.equals(name)) {
+ notifyLocaleChanged();
+ } else if (Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE.equals(name)) {
+ notifyFontScaleChanged();
+ } else {
+ // We only need a single callback when multiple style properties
+ // change in rapid succession.
+ mHandler.removeCallbacks(mStyleChangedRunnable);
+ mHandler.post(mStyleChangedRunnable);
+ }
+ }
+ };
+
+ /**
+ * Runnable posted when user style properties change. This is used to
+ * prevent unnecessary change notifications when multiple properties change
+ * in rapid succession.
+ */
+ private final Runnable mStyleChangedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ notifyUserStyleChanged();
+ }
+ };
+
+ /**
+ * Specifies visual properties for video captions, including foreground and
+ * background colors, edge properties, and typeface.
+ */
+ public static final class CaptionStyle {
+ /**
+ * Packed value for a color of 'none' and a cached opacity of 100%.
+ *
+ * @hide
+ */
+ private static final int COLOR_NONE_OPAQUE = 0x000000FF;
+
+ /**
+ * Packed value for a color of 'default' and opacity of 100%.
+ *
+ * @hide
+ */
+ public static final int COLOR_UNSPECIFIED = 0x00FFFFFF;
+
+ private static final CaptionStyle WHITE_ON_BLACK;
+ private static final CaptionStyle BLACK_ON_WHITE;
+ private static final CaptionStyle YELLOW_ON_BLACK;
+ private static final CaptionStyle YELLOW_ON_BLUE;
+ private static final CaptionStyle DEFAULT_CUSTOM;
+ private static final CaptionStyle UNSPECIFIED;
+
+ /** The default caption style used to fill in unspecified values. @hide */
+ public static final CaptionStyle DEFAULT;
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public static final CaptionStyle[] PRESETS;
+
+ /** @hide */
+ public static final int PRESET_CUSTOM = -1;
+
+ /** Unspecified edge type value. */
+ public static final int EDGE_TYPE_UNSPECIFIED = -1;
+
+ /** Edge type value specifying no character edges. */
+ public static final int EDGE_TYPE_NONE = 0;
+
+ /** Edge type value specifying uniformly outlined character edges. */
+ public static final int EDGE_TYPE_OUTLINE = 1;
+
+ /** Edge type value specifying drop-shadowed character edges. */
+ public static final int EDGE_TYPE_DROP_SHADOW = 2;
+
+ /** Edge type value specifying raised bevel character edges. */
+ public static final int EDGE_TYPE_RAISED = 3;
+
+ /** Edge type value specifying depressed bevel character edges. */
+ public static final int EDGE_TYPE_DEPRESSED = 4;
+
+ /** The preferred foreground color for video captions. */
+ public final int foregroundColor;
+
+ /** The preferred background color for video captions. */
+ public final int backgroundColor;
+
+ /**
+ * The preferred edge type for video captions, one of:
+ * <ul>
+ * <li>{@link #EDGE_TYPE_UNSPECIFIED}
+ * <li>{@link #EDGE_TYPE_NONE}
+ * <li>{@link #EDGE_TYPE_OUTLINE}
+ * <li>{@link #EDGE_TYPE_DROP_SHADOW}
+ * <li>{@link #EDGE_TYPE_RAISED}
+ * <li>{@link #EDGE_TYPE_DEPRESSED}
+ * </ul>
+ */
+ public final int edgeType;
+
+ /**
+ * The preferred edge color for video captions, if using an edge type
+ * other than {@link #EDGE_TYPE_NONE}.
+ */
+ public final int edgeColor;
+
+ /** The preferred window color for video captions. */
+ public final int windowColor;
+
+ /**
+ * @hide
+ */
+ public final String mRawTypeface;
+
+ private final boolean mHasForegroundColor;
+ private final boolean mHasBackgroundColor;
+ private final boolean mHasEdgeType;
+ private final boolean mHasEdgeColor;
+ private final boolean mHasWindowColor;
+
+ /** Lazily-created typeface based on the raw typeface string. */
+ private Typeface mParsedTypeface;
+
+ private CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor,
+ int windowColor, String rawTypeface) {
+ mHasForegroundColor = hasColor(foregroundColor);
+ mHasBackgroundColor = hasColor(backgroundColor);
+ mHasEdgeType = edgeType != EDGE_TYPE_UNSPECIFIED;
+ mHasEdgeColor = hasColor(edgeColor);
+ mHasWindowColor = hasColor(windowColor);
+
+ // Always use valid colors, even when no override is specified, to
+ // ensure backwards compatibility with apps targeting KitKat MR2.
+ this.foregroundColor = mHasForegroundColor ? foregroundColor : Color.WHITE;
+ this.backgroundColor = mHasBackgroundColor ? backgroundColor : Color.BLACK;
+ this.edgeType = mHasEdgeType ? edgeType : EDGE_TYPE_NONE;
+ this.edgeColor = mHasEdgeColor ? edgeColor : Color.BLACK;
+ this.windowColor = mHasWindowColor ? windowColor : COLOR_NONE_OPAQUE;
+
+ mRawTypeface = rawTypeface;
+ }
+
+ /**
+ * Returns whether a packed color indicates a non-default value.
+ *
+ * @param packedColor the packed color value
+ * @return {@code true} if a non-default value is specified
+ * @hide
+ */
+ public static boolean hasColor(int packedColor) {
+ // Matches the color packing code from Settings. "Default" packed
+ // colors are indicated by zero alpha and non-zero red/blue. The
+ // cached alpha value used by Settings is stored in green.
+ return (packedColor >>> 24) != 0 || (packedColor & 0xFFFF00) == 0;
+ }
+
+ /**
+ * Applies a caption style, overriding any properties that are specified
+ * in the overlay caption.
+ *
+ * @param overlay The style to apply
+ * @return A caption style with the overlay style applied
+ * @hide
+ */
+ @NonNull
+ public CaptionStyle applyStyle(@NonNull CaptionStyle overlay) {
+ final int newForegroundColor = overlay.hasForegroundColor() ?
+ overlay.foregroundColor : foregroundColor;
+ final int newBackgroundColor = overlay.hasBackgroundColor() ?
+ overlay.backgroundColor : backgroundColor;
+ final int newEdgeType = overlay.hasEdgeType() ?
+ overlay.edgeType : edgeType;
+ final int newEdgeColor = overlay.hasEdgeColor() ?
+ overlay.edgeColor : edgeColor;
+ final int newWindowColor = overlay.hasWindowColor() ?
+ overlay.windowColor : windowColor;
+ final String newRawTypeface = overlay.mRawTypeface != null ?
+ overlay.mRawTypeface : mRawTypeface;
+ return new CaptionStyle(newForegroundColor, newBackgroundColor, newEdgeType,
+ newEdgeColor, newWindowColor, newRawTypeface);
+ }
+
+ /**
+ * @return {@code true} if the user has specified a background color
+ * that should override the application default, {@code false}
+ * otherwise
+ */
+ public boolean hasBackgroundColor() {
+ return mHasBackgroundColor;
+ }
+
+ /**
+ * @return {@code true} if the user has specified a foreground color
+ * that should override the application default, {@code false}
+ * otherwise
+ */
+ public boolean hasForegroundColor() {
+ return mHasForegroundColor;
+ }
+
+ /**
+ * @return {@code true} if the user has specified an edge type that
+ * should override the application default, {@code false}
+ * otherwise
+ */
+ public boolean hasEdgeType() {
+ return mHasEdgeType;
+ }
+
+ /**
+ * @return {@code true} if the user has specified an edge color that
+ * should override the application default, {@code false}
+ * otherwise
+ */
+ public boolean hasEdgeColor() {
+ return mHasEdgeColor;
+ }
+
+ /**
+ * @return {@code true} if the user has specified a window color that
+ * should override the application default, {@code false}
+ * otherwise
+ */
+ public boolean hasWindowColor() {
+ return mHasWindowColor;
+ }
+
+ /**
+ * @return the preferred {@link Typeface} for video captions, or null if
+ * not specified
+ */
+ @Nullable
+ public Typeface getTypeface() {
+ if (mParsedTypeface == null && !TextUtils.isEmpty(mRawTypeface)) {
+ mParsedTypeface = Typeface.create(mRawTypeface, Typeface.NORMAL);
+ }
+ return mParsedTypeface;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ public static CaptionStyle getCustomStyle(ContentResolver cr) {
+ final CaptionStyle defStyle = CaptionStyle.DEFAULT_CUSTOM;
+ final int foregroundColor = Secure.getInt(
+ cr, Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR, defStyle.foregroundColor);
+ final int backgroundColor = Secure.getInt(
+ cr, Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR, defStyle.backgroundColor);
+ final int edgeType = Secure.getInt(
+ cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, defStyle.edgeType);
+ final int edgeColor = Secure.getInt(
+ cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, defStyle.edgeColor);
+ final int windowColor = Secure.getInt(
+ cr, Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, defStyle.windowColor);
+
+ String rawTypeface = Secure.getString(cr, Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE);
+ if (rawTypeface == null) {
+ rawTypeface = defStyle.mRawTypeface;
+ }
+
+ return new CaptionStyle(foregroundColor, backgroundColor, edgeType, edgeColor,
+ windowColor, rawTypeface);
+ }
+
+ static {
+ WHITE_ON_BLACK = new CaptionStyle(Color.WHITE, Color.BLACK, EDGE_TYPE_NONE,
+ Color.BLACK, COLOR_NONE_OPAQUE, null);
+ BLACK_ON_WHITE = new CaptionStyle(Color.BLACK, Color.WHITE, EDGE_TYPE_NONE,
+ Color.BLACK, COLOR_NONE_OPAQUE, null);
+ YELLOW_ON_BLACK = new CaptionStyle(Color.YELLOW, Color.BLACK, EDGE_TYPE_NONE,
+ Color.BLACK, COLOR_NONE_OPAQUE, null);
+ YELLOW_ON_BLUE = new CaptionStyle(Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE,
+ Color.BLACK, COLOR_NONE_OPAQUE, null);
+ UNSPECIFIED = new CaptionStyle(COLOR_UNSPECIFIED, COLOR_UNSPECIFIED,
+ EDGE_TYPE_UNSPECIFIED, COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, null);
+
+ // The ordering of these cannot change since we store the index
+ // directly in preferences.
+ PRESETS = new CaptionStyle[] {
+ WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE, UNSPECIFIED
+ };
+
+ DEFAULT_CUSTOM = WHITE_ON_BLACK;
+ DEFAULT = WHITE_ON_BLACK;
+ }
+ }
+
+ /**
+ * Listener for changes in captioning properties, including enabled state
+ * and user style preferences.
+ */
+ public static abstract class CaptioningChangeListener {
+ /**
+ * Called when the captioning enabled state changes.
+ *
+ * @param enabled the user's new preferred captioning enabled state
+ */
+ public void onEnabledChanged(boolean enabled) {}
+
+ /**
+ * Called when the captioning user style changes.
+ *
+ * @param userStyle the user's new preferred style
+ * @see CaptioningManager#getUserStyle()
+ */
+ public void onUserStyleChanged(@NonNull CaptionStyle userStyle) {}
+
+ /**
+ * Called when the captioning locale changes.
+ *
+ * @param locale the preferred captioning locale, or {@code null} if not specified
+ * @see CaptioningManager#getLocale()
+ */
+ public void onLocaleChanged(@Nullable Locale locale) {}
+
+ /**
+ * Called when the captioning font scaling factor changes.
+ *
+ * @param fontScale the preferred font scaling factor
+ * @see CaptioningManager#getFontScale()
+ */
+ public void onFontScaleChanged(float fontScale) {}
+ }
+}
diff --git a/android/view/accessibility/MagnificationAnimationCallback.java b/android/view/accessibility/MagnificationAnimationCallback.java
new file mode 100644
index 0000000..bc9fb0a
--- /dev/null
+++ b/android/view/accessibility/MagnificationAnimationCallback.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+/**
+ * A callback for magnification animation result.
+ * @hide
+ */
+public interface MagnificationAnimationCallback {
+ /**
+ * Called when the animation is finished or interrupted during animating.
+ *
+ * @param success {@code true} if animating successfully with given spec or the spec did not
+ * change. Otherwise {@code false}
+ */
+ void onResult(boolean success);
+}
\ No newline at end of file
diff --git a/android/view/accessibility/WeakSparseArray.java b/android/view/accessibility/WeakSparseArray.java
new file mode 100644
index 0000000..04a4cc7
--- /dev/null
+++ b/android/view/accessibility/WeakSparseArray.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.util.SparseArray;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+
+final class WeakSparseArray<E> {
+
+ private final ReferenceQueue<E> mRefQueue = new ReferenceQueue<>();
+ private final SparseArray<WeakReferenceWithId<E>> mSparseArray = new SparseArray<>();
+
+ public void append(int key, E value) {
+ removeUnreachableValues();
+ mSparseArray.append(key, new WeakReferenceWithId(value, mRefQueue, key));
+ }
+
+ public void remove(int key) {
+ removeUnreachableValues();
+ mSparseArray.remove(key);
+ }
+
+ public E get(int key) {
+ removeUnreachableValues();
+ WeakReferenceWithId<E> ref = mSparseArray.get(key);
+ return ref != null ? ref.get() : null;
+ }
+
+ private void removeUnreachableValues() {
+ for (Reference ref = mRefQueue.poll(); ref != null; ref = mRefQueue.poll()) {
+ mSparseArray.remove(((WeakReferenceWithId) ref).mId);
+ }
+ }
+
+ private static class WeakReferenceWithId<E> extends WeakReference<E> {
+
+ final int mId;
+
+ WeakReferenceWithId(E referent, ReferenceQueue<? super E> q, int id) {
+ super(referent, q);
+ mId = id;
+ }
+ }
+}
+
diff --git a/android/view/animation/AccelerateDecelerateInterpolator.java b/android/view/animation/AccelerateDecelerateInterpolator.java
new file mode 100644
index 0000000..a2bbc5c
--- /dev/null
+++ b/android/view/animation/AccelerateDecelerateInterpolator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the rate of change starts and ends slowly but
+ * accelerates through the middle.
+ */
+@HasNativeInterpolator
+public class AccelerateDecelerateInterpolator extends BaseInterpolator
+ implements NativeInterpolator {
+ public AccelerateDecelerateInterpolator() {
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
+ }
+
+ public float getInterpolation(float input) {
+ return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createAccelerateDecelerateInterpolator();
+ }
+}
diff --git a/android/view/animation/AccelerateInterpolator.java b/android/view/animation/AccelerateInterpolator.java
new file mode 100644
index 0000000..9d4cd32
--- /dev/null
+++ b/android/view/animation/AccelerateInterpolator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * An interpolator where the rate of change starts out slowly and
+ * and then accelerates.
+ *
+ */
+@HasNativeInterpolator
+public class AccelerateInterpolator extends BaseInterpolator implements NativeInterpolator {
+ private final float mFactor;
+ private final double mDoubleFactor;
+
+ public AccelerateInterpolator() {
+ mFactor = 1.0f;
+ mDoubleFactor = 2.0;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param factor Degree to which the animation should be eased. Seting
+ * factor to 1.0f produces a y=x^2 parabola. Increasing factor above
+ * 1.0f exaggerates the ease-in effect (i.e., it starts even
+ * slower and ends evens faster)
+ */
+ public AccelerateInterpolator(float factor) {
+ mFactor = factor;
+ mDoubleFactor = 2 * mFactor;
+ }
+
+ public AccelerateInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public AccelerateInterpolator(Resources res, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.AccelerateInterpolator, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.AccelerateInterpolator);
+ }
+
+ mFactor = a.getFloat(R.styleable.AccelerateInterpolator_factor, 1.0f);
+ mDoubleFactor = 2 * mFactor;
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ public float getInterpolation(float input) {
+ if (mFactor == 1.0f) {
+ return input * input;
+ } else {
+ return (float)Math.pow(input, mDoubleFactor);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createAccelerateInterpolator(mFactor);
+ }
+}
diff --git a/android/view/animation/AlphaAnimation.java b/android/view/animation/AlphaAnimation.java
new file mode 100644
index 0000000..c4d9afc
--- /dev/null
+++ b/android/view/animation/AlphaAnimation.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the alpha level of an object.
+ * Useful for fading things in and out. This animation ends up
+ * changing the alpha property of a {@link Transformation}
+ *
+ */
+public class AlphaAnimation extends Animation {
+ private float mFromAlpha;
+ private float mToAlpha;
+
+ /**
+ * Constructor used when an AlphaAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public AlphaAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AlphaAnimation);
+
+ mFromAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_fromAlpha, 1.0f);
+ mToAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_toAlpha, 1.0f);
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building an AlphaAnimation from code
+ *
+ * @param fromAlpha Starting alpha value for the animation, where 1.0 means
+ * fully opaque and 0.0 means fully transparent.
+ * @param toAlpha Ending alpha value for the animation.
+ */
+ public AlphaAnimation(float fromAlpha, float toAlpha) {
+ mFromAlpha = fromAlpha;
+ mToAlpha = toAlpha;
+ }
+
+ /**
+ * Changes the alpha property of the supplied {@link Transformation}
+ */
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ final float alpha = mFromAlpha;
+ t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
+ }
+
+ @Override
+ public boolean willChangeTransformationMatrix() {
+ return false;
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean hasAlpha() {
+ return true;
+ }
+}
diff --git a/android/view/animation/Animation.java b/android/view/animation/Animation.java
new file mode 100644
index 0000000..b1d618e
--- /dev/null
+++ b/android/view/animation/Animation.java
@@ -0,0 +1,1256 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.annotation.AnimRes;
+import android.annotation.ColorInt;
+import android.annotation.InterpolatorRes;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Handler;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+
+import dalvik.system.CloseGuard;
+
+/**
+ * Abstraction for an Animation that can be applied to Views, Surfaces, or
+ * other objects. See the {@link android.view.animation animation package
+ * description file}.
+ */
+public abstract class Animation implements Cloneable {
+ /**
+ * Repeat the animation indefinitely.
+ */
+ public static final int INFINITE = -1;
+
+ /**
+ * When the animation reaches the end and the repeat count is INFINTE_REPEAT
+ * or a positive value, the animation restarts from the beginning.
+ */
+ public static final int RESTART = 1;
+
+ /**
+ * When the animation reaches the end and the repeat count is INFINTE_REPEAT
+ * or a positive value, the animation plays backward (and then forward again).
+ */
+ public static final int REVERSE = 2;
+
+ /**
+ * Can be used as the start time to indicate the start time should be the current
+ * time when {@link #getTransformation(long, Transformation)} is invoked for the
+ * first animation frame. This can is useful for short animations.
+ */
+ public static final int START_ON_FIRST_FRAME = -1;
+
+ /**
+ * The specified dimension is an absolute number of pixels.
+ */
+ public static final int ABSOLUTE = 0;
+
+ /**
+ * The specified dimension holds a float and should be multiplied by the
+ * height or width of the object being animated.
+ */
+ public static final int RELATIVE_TO_SELF = 1;
+
+ /**
+ * The specified dimension holds a float and should be multiplied by the
+ * height or width of the parent of the object being animated.
+ */
+ public static final int RELATIVE_TO_PARENT = 2;
+
+ /**
+ * Requests that the content being animated be kept in its current Z
+ * order.
+ */
+ public static final int ZORDER_NORMAL = 0;
+
+ /**
+ * Requests that the content being animated be forced on top of all other
+ * content for the duration of the animation.
+ */
+ public static final int ZORDER_TOP = 1;
+
+ /**
+ * Requests that the content being animated be forced under all other
+ * content for the duration of the animation.
+ */
+ public static final int ZORDER_BOTTOM = -1;
+
+ // Use a preload holder to isolate static initialization into inner class, which allows
+ // Animation and its subclasses to be compile-time initialized.
+ private static class NoImagePreloadHolder {
+ public static final boolean USE_CLOSEGUARD
+ = SystemProperties.getBoolean("log.closeguard.Animation", false);
+ }
+
+ /**
+ * Set by {@link #getTransformation(long, Transformation)} when the animation ends.
+ */
+ boolean mEnded = false;
+
+ /**
+ * Set by {@link #getTransformation(long, Transformation)} when the animation starts.
+ */
+ boolean mStarted = false;
+
+ /**
+ * Set by {@link #getTransformation(long, Transformation)} when the animation repeats
+ * in REVERSE mode.
+ */
+ boolean mCycleFlip = false;
+
+ /**
+ * This value must be set to true by {@link #initialize(int, int, int, int)}. It
+ * indicates the animation was successfully initialized and can be played.
+ */
+ boolean mInitialized = false;
+
+ /**
+ * Indicates whether the animation transformation should be applied before the
+ * animation starts. The value of this variable is only relevant if mFillEnabled is true;
+ * otherwise it is assumed to be true.
+ */
+ boolean mFillBefore = true;
+
+ /**
+ * Indicates whether the animation transformation should be applied after the
+ * animation ends.
+ */
+ boolean mFillAfter = false;
+
+ /**
+ * Indicates whether fillBefore should be taken into account.
+ */
+ boolean mFillEnabled = false;
+
+ /**
+ * The time in milliseconds at which the animation must start;
+ */
+ long mStartTime = -1;
+
+ /**
+ * The delay in milliseconds after which the animation must start. When the
+ * start offset is > 0, the start time of the animation is startTime + startOffset.
+ */
+ long mStartOffset;
+
+ /**
+ * The duration of one animation cycle in milliseconds.
+ */
+ long mDuration;
+
+ /**
+ * The number of times the animation must repeat. By default, an animation repeats
+ * indefinitely.
+ */
+ int mRepeatCount = 0;
+
+ /**
+ * Indicates how many times the animation was repeated.
+ */
+ int mRepeated = 0;
+
+ /**
+ * The behavior of the animation when it repeats. The repeat mode is either
+ * {@link #RESTART} or {@link #REVERSE}.
+ *
+ */
+ int mRepeatMode = RESTART;
+
+ /**
+ * The interpolator used by the animation to smooth the movement.
+ */
+ Interpolator mInterpolator;
+
+ /**
+ * An animation listener to be notified when the animation starts, ends or repeats.
+ */
+ // If you need to chain the AnimationListener, wrap the existing Animation into an AnimationSet
+ // and add your new listener to that set
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 117519981)
+ private AnimationListener mListener;
+
+ /**
+ * Desired Z order mode during animation.
+ */
+ private int mZAdjustment;
+
+ /**
+ * Desired background color behind animation.
+ */
+ private int mBackgroundColor;
+
+ /**
+ * scalefactor to apply to pivot points, etc. during animation. Subclasses retrieve the
+ * value via getScaleFactor().
+ */
+ private float mScaleFactor = 1f;
+
+ private boolean mShowWallpaper;
+ private boolean mHasRoundedCorners;
+
+ private boolean mMore = true;
+ private boolean mOneMoreTime = true;
+
+ @UnsupportedAppUsage
+ RectF mPreviousRegion = new RectF();
+ @UnsupportedAppUsage
+ RectF mRegion = new RectF();
+ @UnsupportedAppUsage
+ Transformation mTransformation = new Transformation();
+ @UnsupportedAppUsage
+ Transformation mPreviousTransformation = new Transformation();
+
+ private final CloseGuard guard = CloseGuard.get();
+
+ private Handler mListenerHandler;
+ private Runnable mOnStart;
+ private Runnable mOnRepeat;
+ private Runnable mOnEnd;
+
+ /**
+ * Creates a new animation with a duration of 0ms, the default interpolator, with
+ * fillBefore set to true and fillAfter set to false
+ */
+ public Animation() {
+ ensureInterpolator();
+ }
+
+ /**
+ * Creates a new animation whose parameters come from the specified context and
+ * attributes set.
+ *
+ * @param context the application environment
+ * @param attrs the set of attributes holding the animation parameters
+ */
+ public Animation(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Animation);
+
+ setDuration((long) a.getInt(com.android.internal.R.styleable.Animation_duration, 0));
+ setStartOffset((long) a.getInt(com.android.internal.R.styleable.Animation_startOffset, 0));
+
+ setFillEnabled(a.getBoolean(com.android.internal.R.styleable.Animation_fillEnabled, mFillEnabled));
+ setFillBefore(a.getBoolean(com.android.internal.R.styleable.Animation_fillBefore, mFillBefore));
+ setFillAfter(a.getBoolean(com.android.internal.R.styleable.Animation_fillAfter, mFillAfter));
+
+ setRepeatCount(a.getInt(com.android.internal.R.styleable.Animation_repeatCount, mRepeatCount));
+ setRepeatMode(a.getInt(com.android.internal.R.styleable.Animation_repeatMode, RESTART));
+
+ setZAdjustment(a.getInt(com.android.internal.R.styleable.Animation_zAdjustment, ZORDER_NORMAL));
+
+ setBackgroundColor(a.getInt(com.android.internal.R.styleable.Animation_background, 0));
+
+ setDetachWallpaper(
+ a.getBoolean(com.android.internal.R.styleable.Animation_detachWallpaper, false));
+ setShowWallpaper(
+ a.getBoolean(com.android.internal.R.styleable.Animation_showWallpaper, false));
+ setHasRoundedCorners(
+ a.getBoolean(com.android.internal.R.styleable.Animation_hasRoundedCorners, false));
+
+ final int resID = a.getResourceId(com.android.internal.R.styleable.Animation_interpolator, 0);
+
+ a.recycle();
+
+ if (resID > 0) {
+ setInterpolator(context, resID);
+ }
+
+ ensureInterpolator();
+ }
+
+ @Override
+ protected Animation clone() throws CloneNotSupportedException {
+ final Animation animation = (Animation) super.clone();
+ animation.mPreviousRegion = new RectF();
+ animation.mRegion = new RectF();
+ animation.mTransformation = new Transformation();
+ animation.mPreviousTransformation = new Transformation();
+ return animation;
+ }
+
+ /**
+ * Reset the initialization state of this animation.
+ *
+ * @see #initialize(int, int, int, int)
+ */
+ public void reset() {
+ mPreviousRegion.setEmpty();
+ mPreviousTransformation.clear();
+ mInitialized = false;
+ mCycleFlip = false;
+ mRepeated = 0;
+ mMore = true;
+ mOneMoreTime = true;
+ mListenerHandler = null;
+ }
+
+ /**
+ * Cancel the animation. Cancelling an animation invokes the animation
+ * listener, if set, to notify the end of the animation.
+ *
+ * If you cancel an animation manually, you must call {@link #reset()}
+ * before starting the animation again.
+ *
+ * @see #reset()
+ * @see #start()
+ * @see #startNow()
+ */
+ public void cancel() {
+ if (mStarted && !mEnded) {
+ fireAnimationEnd();
+ mEnded = true;
+ guard.close();
+ }
+ // Make sure we move the animation to the end
+ mStartTime = Long.MIN_VALUE;
+ mMore = mOneMoreTime = false;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void detach() {
+ if (mStarted && !mEnded) {
+ mEnded = true;
+ guard.close();
+ fireAnimationEnd();
+ }
+ }
+
+ /**
+ * Whether or not the animation has been initialized.
+ *
+ * @return Has this animation been initialized.
+ * @see #initialize(int, int, int, int)
+ */
+ public boolean isInitialized() {
+ return mInitialized;
+ }
+
+ /**
+ * Initialize this animation with the dimensions of the object being
+ * animated as well as the objects parents. (This is to support animation
+ * sizes being specified relative to these dimensions.)
+ *
+ * <p>Objects that interpret Animations should call this method when
+ * the sizes of the object being animated and its parent are known, and
+ * before calling {@link #getTransformation}.
+ *
+ *
+ * @param width Width of the object being animated
+ * @param height Height of the object being animated
+ * @param parentWidth Width of the animated object's parent
+ * @param parentHeight Height of the animated object's parent
+ */
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ reset();
+ mInitialized = true;
+ }
+
+ /**
+ * Sets the handler used to invoke listeners.
+ *
+ * @hide
+ */
+ public void setListenerHandler(Handler handler) {
+ if (mListenerHandler == null) {
+ mOnStart = new Runnable() {
+ public void run() {
+ dispatchAnimationStart();
+ }
+ };
+ mOnRepeat = new Runnable() {
+ public void run() {
+ dispatchAnimationRepeat();
+ }
+ };
+ mOnEnd = new Runnable() {
+ public void run() {
+ dispatchAnimationEnd();
+ }
+ };
+ }
+ mListenerHandler = handler;
+ }
+
+ /**
+ * Sets the acceleration curve for this animation. The interpolator is loaded as
+ * a resource from the specified context.
+ *
+ * @param context The application environment
+ * @param resID The resource identifier of the interpolator to load
+ * @attr ref android.R.styleable#Animation_interpolator
+ */
+ public void setInterpolator(Context context, @AnimRes @InterpolatorRes int resID) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resID));
+ }
+
+ /**
+ * Sets the acceleration curve for this animation. Defaults to a linear
+ * interpolation.
+ *
+ * @param i The interpolator which defines the acceleration curve
+ * @attr ref android.R.styleable#Animation_interpolator
+ */
+ public void setInterpolator(Interpolator i) {
+ mInterpolator = i;
+ }
+
+ /**
+ * When this animation should start relative to the start time. This is most
+ * useful when composing complex animations using an {@link AnimationSet }
+ * where some of the animations components start at different times.
+ *
+ * @param startOffset When this Animation should start, in milliseconds from
+ * the start time of the root AnimationSet.
+ * @attr ref android.R.styleable#Animation_startOffset
+ */
+ public void setStartOffset(long startOffset) {
+ mStartOffset = startOffset;
+ }
+
+ /**
+ * How long this animation should last. The duration cannot be negative.
+ *
+ * @param durationMillis Duration in milliseconds
+ *
+ * @throws java.lang.IllegalArgumentException if the duration is < 0
+ *
+ * @attr ref android.R.styleable#Animation_duration
+ */
+ public void setDuration(long durationMillis) {
+ if (durationMillis < 0) {
+ throw new IllegalArgumentException("Animation duration cannot be negative");
+ }
+ mDuration = durationMillis;
+ }
+
+ /**
+ * Ensure that the duration that this animation will run is not longer
+ * than <var>durationMillis</var>. In addition to adjusting the duration
+ * itself, this ensures that the repeat count also will not make it run
+ * longer than the given time.
+ *
+ * @param durationMillis The maximum duration the animation is allowed
+ * to run.
+ */
+ public void restrictDuration(long durationMillis) {
+ // If we start after the duration, then we just won't run.
+ if (mStartOffset > durationMillis) {
+ mStartOffset = durationMillis;
+ mDuration = 0;
+ mRepeatCount = 0;
+ return;
+ }
+
+ long dur = mDuration + mStartOffset;
+ if (dur > durationMillis) {
+ mDuration = durationMillis-mStartOffset;
+ dur = durationMillis;
+ }
+ // If the duration is 0 or less, then we won't run.
+ if (mDuration <= 0) {
+ mDuration = 0;
+ mRepeatCount = 0;
+ return;
+ }
+ // Reduce the number of repeats to keep below the maximum duration.
+ // The comparison between mRepeatCount and duration is to catch
+ // overflows after multiplying them.
+ if (mRepeatCount < 0 || mRepeatCount > durationMillis
+ || (dur*mRepeatCount) > durationMillis) {
+ // Figure out how many times to do the animation. Subtract 1 since
+ // repeat count is the number of times to repeat so 0 runs once.
+ mRepeatCount = (int)(durationMillis/dur) - 1;
+ if (mRepeatCount < 0) {
+ mRepeatCount = 0;
+ }
+ }
+ }
+
+ /**
+ * How much to scale the duration by.
+ *
+ * @param scale The amount to scale the duration.
+ */
+ public void scaleCurrentDuration(float scale) {
+ mDuration = (long) (mDuration * scale);
+ mStartOffset = (long) (mStartOffset * scale);
+ }
+
+ /**
+ * When this animation should start. When the start time is set to
+ * {@link #START_ON_FIRST_FRAME}, the animation will start the first time
+ * {@link #getTransformation(long, Transformation)} is invoked. The time passed
+ * to this method should be obtained by calling
+ * {@link AnimationUtils#currentAnimationTimeMillis()} instead of
+ * {@link System#currentTimeMillis()}.
+ *
+ * @param startTimeMillis the start time in milliseconds
+ */
+ public void setStartTime(long startTimeMillis) {
+ mStartTime = startTimeMillis;
+ mStarted = mEnded = false;
+ mCycleFlip = false;
+ mRepeated = 0;
+ mMore = true;
+ }
+
+ /**
+ * Convenience method to start the animation the first time
+ * {@link #getTransformation(long, Transformation)} is invoked.
+ */
+ public void start() {
+ setStartTime(-1);
+ }
+
+ /**
+ * Convenience method to start the animation at the current time in
+ * milliseconds.
+ */
+ public void startNow() {
+ setStartTime(AnimationUtils.currentAnimationTimeMillis());
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end. This
+ * setting is applied only when the repeat count is either greater than
+ * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
+ *
+ * @param repeatMode {@link #RESTART} or {@link #REVERSE}
+ * @attr ref android.R.styleable#Animation_repeatMode
+ */
+ public void setRepeatMode(int repeatMode) {
+ mRepeatMode = repeatMode;
+ }
+
+ /**
+ * Sets how many times the animation should be repeated. If the repeat
+ * count is 0, the animation is never repeated. If the repeat count is
+ * greater than 0 or {@link #INFINITE}, the repeat mode will be taken
+ * into account. The repeat count is 0 by default.
+ *
+ * @param repeatCount the number of times the animation should be repeated
+ * @attr ref android.R.styleable#Animation_repeatCount
+ */
+ public void setRepeatCount(int repeatCount) {
+ if (repeatCount < 0) {
+ repeatCount = INFINITE;
+ }
+ mRepeatCount = repeatCount;
+ }
+
+ /**
+ * If fillEnabled is true, this animation will apply the value of fillBefore.
+ *
+ * @return true if the animation will take fillBefore into account
+ * @attr ref android.R.styleable#Animation_fillEnabled
+ */
+ public boolean isFillEnabled() {
+ return mFillEnabled;
+ }
+
+ /**
+ * If fillEnabled is true, the animation will apply the value of fillBefore.
+ * Otherwise, fillBefore is ignored and the animation
+ * transformation is always applied until the animation ends.
+ *
+ * @param fillEnabled true if the animation should take the value of fillBefore into account
+ * @attr ref android.R.styleable#Animation_fillEnabled
+ *
+ * @see #setFillBefore(boolean)
+ * @see #setFillAfter(boolean)
+ */
+ public void setFillEnabled(boolean fillEnabled) {
+ mFillEnabled = fillEnabled;
+ }
+
+ /**
+ * If fillBefore is true, this animation will apply its transformation
+ * before the start time of the animation. Defaults to true if
+ * {@link #setFillEnabled(boolean)} is not set to true.
+ * Note that this applies when using an {@link
+ * android.view.animation.AnimationSet AnimationSet} to chain
+ * animations. The transformation is not applied before the AnimationSet
+ * itself starts.
+ *
+ * @param fillBefore true if the animation should apply its transformation before it starts
+ * @attr ref android.R.styleable#Animation_fillBefore
+ *
+ * @see #setFillEnabled(boolean)
+ */
+ public void setFillBefore(boolean fillBefore) {
+ mFillBefore = fillBefore;
+ }
+
+ /**
+ * If fillAfter is true, the transformation that this animation performed
+ * will persist when it is finished. Defaults to false if not set.
+ * Note that this applies to individual animations and when using an {@link
+ * android.view.animation.AnimationSet AnimationSet} to chain
+ * animations.
+ *
+ * @param fillAfter true if the animation should apply its transformation after it ends
+ * @attr ref android.R.styleable#Animation_fillAfter
+ *
+ * @see #setFillEnabled(boolean)
+ */
+ public void setFillAfter(boolean fillAfter) {
+ mFillAfter = fillAfter;
+ }
+
+ /**
+ * Set the Z ordering mode to use while running the animation.
+ *
+ * @param zAdjustment The desired mode, one of {@link #ZORDER_NORMAL},
+ * {@link #ZORDER_TOP}, or {@link #ZORDER_BOTTOM}.
+ * @attr ref android.R.styleable#Animation_zAdjustment
+ */
+ public void setZAdjustment(int zAdjustment) {
+ mZAdjustment = zAdjustment;
+ }
+
+ /**
+ * Set background behind animation.
+ *
+ * @param bg The background color. If 0, no background. Currently must
+ * be black, with any desired alpha level.
+ *
+ * @deprecated None of window animations are running with background color.
+ */
+ @Deprecated
+ public void setBackgroundColor(@ColorInt int bg) {
+ // The background color is not needed any more, do nothing.
+ }
+
+ /**
+ * The scale factor is set by the call to <code>getTransformation</code>. Overrides of
+ * {@link #getTransformation(long, Transformation, float)} will get this value
+ * directly. Overrides of {@link #applyTransformation(float, Transformation)} can
+ * call this method to get the value.
+ *
+ * @return float The scale factor that should be applied to pre-scaled values in
+ * an Animation such as the pivot points in {@link ScaleAnimation} and {@link RotateAnimation}.
+ */
+ protected float getScaleFactor() {
+ return mScaleFactor;
+ }
+
+ /**
+ * If detachWallpaper is true, and this is a window animation of a window
+ * that has a wallpaper background, then the window will be detached from
+ * the wallpaper while it runs. That is, the animation will only be applied
+ * to the window, and the wallpaper behind it will remain static.
+ *
+ * @param detachWallpaper true if the wallpaper should be detached from the animation
+ * @attr ref android.R.styleable#Animation_detachWallpaper
+ *
+ * @deprecated All window animations are running with detached wallpaper.
+ */
+ @Deprecated
+ public void setDetachWallpaper(boolean detachWallpaper) {
+ }
+
+ /**
+ * If this animation is run as a window animation, this will make the wallpaper visible behind
+ * the animation.
+ *
+ * @param showWallpaper Whether the wallpaper should be shown during the animation.
+ * @attr ref android.R.styleable#Animation_detachWallpaper
+ * @hide
+ */
+ public void setShowWallpaper(boolean showWallpaper) {
+ mShowWallpaper = showWallpaper;
+ }
+
+ /**
+ * If this is a window animation, the window will have rounded corners matching the display
+ * corner radius.
+ *
+ * @param hasRoundedCorners Whether the window should have rounded corners or not.
+ * @attr ref android.R.styleable#Animation_hasRoundedCorners
+ * @see com.android.internal.policy.ScreenDecorationsUtils#getWindowCornerRadius(Resources)
+ * @hide
+ */
+ public void setHasRoundedCorners(boolean hasRoundedCorners) {
+ mHasRoundedCorners = hasRoundedCorners;
+ }
+
+ /**
+ * Gets the acceleration curve type for this animation.
+ *
+ * @return the {@link Interpolator} associated to this animation
+ * @attr ref android.R.styleable#Animation_interpolator
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * When this animation should start. If the animation has not startet yet,
+ * this method might return {@link #START_ON_FIRST_FRAME}.
+ *
+ * @return the time in milliseconds when the animation should start or
+ * {@link #START_ON_FIRST_FRAME}
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * How long this animation should last
+ *
+ * @return the duration in milliseconds of the animation
+ * @attr ref android.R.styleable#Animation_duration
+ */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * When this animation should start, relative to StartTime
+ *
+ * @return the start offset in milliseconds
+ * @attr ref android.R.styleable#Animation_startOffset
+ */
+ public long getStartOffset() {
+ return mStartOffset;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end.
+ *
+ * @return either one of {@link #REVERSE} or {@link #RESTART}
+ * @attr ref android.R.styleable#Animation_repeatMode
+ */
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ /**
+ * Defines how many times the animation should repeat. The default value
+ * is 0.
+ *
+ * @return the number of times the animation should repeat, or {@link #INFINITE}
+ * @attr ref android.R.styleable#Animation_repeatCount
+ */
+ public int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ /**
+ * If fillBefore is true, this animation will apply its transformation
+ * before the start time of the animation. If fillBefore is false and
+ * {@link #isFillEnabled() fillEnabled} is true, the transformation will not be applied until
+ * the start time of the animation.
+ *
+ * @return true if the animation applies its transformation before it starts
+ * @attr ref android.R.styleable#Animation_fillBefore
+ */
+ public boolean getFillBefore() {
+ return mFillBefore;
+ }
+
+ /**
+ * If fillAfter is true, this animation will apply its transformation
+ * after the end time of the animation.
+ *
+ * @return true if the animation applies its transformation after it ends
+ * @attr ref android.R.styleable#Animation_fillAfter
+ */
+ public boolean getFillAfter() {
+ return mFillAfter;
+ }
+
+ /**
+ * Returns the Z ordering mode to use while running the animation as
+ * previously set by {@link #setZAdjustment}.
+ *
+ * @return Returns one of {@link #ZORDER_NORMAL},
+ * {@link #ZORDER_TOP}, or {@link #ZORDER_BOTTOM}.
+ * @attr ref android.R.styleable#Animation_zAdjustment
+ */
+ public int getZAdjustment() {
+ return mZAdjustment;
+ }
+
+ /**
+ * Returns the background color behind the animation.
+ *
+ * @deprecated None of window animations are running with background color.
+ */
+ @Deprecated
+ @ColorInt
+ public int getBackgroundColor() {
+ return 0;
+ }
+
+ /**
+ * Return value of {@link #setDetachWallpaper(boolean)}.
+ * @attr ref android.R.styleable#Animation_detachWallpaper
+ *
+ * @deprecated All window animations are running with detached wallpaper.
+ */
+ @Deprecated
+ public boolean getDetachWallpaper() {
+ return true;
+ }
+
+ /**
+ * @return If run as a window animation, returns whether the wallpaper will be shown behind
+ * during the animation.
+ * @attr ref android.R.styleable#Animation_showWallpaper
+ * @hide
+ */
+ public boolean getShowWallpaper() {
+ return mShowWallpaper;
+ }
+
+ /**
+ * @return if a window animation should have rounded corners or not.
+ *
+ * @attr ref android.R.styleable#Animation_hasRoundedCorners
+ * @hide
+ */
+ public boolean hasRoundedCorners() {
+ return mHasRoundedCorners;
+ }
+
+ /**
+ * <p>Indicates whether or not this animation will affect the transformation
+ * matrix. For instance, a fade animation will not affect the matrix whereas
+ * a scale animation will.</p>
+ *
+ * @return true if this animation will change the transformation matrix
+ */
+ public boolean willChangeTransformationMatrix() {
+ // assume we will change the matrix
+ return true;
+ }
+
+ /**
+ * <p>Indicates whether or not this animation will affect the bounds of the
+ * animated view. For instance, a fade animation will not affect the bounds
+ * whereas a 200% scale animation will.</p>
+ *
+ * @return true if this animation will change the view's bounds
+ */
+ public boolean willChangeBounds() {
+ // assume we will change the bounds
+ return true;
+ }
+
+ private boolean hasAnimationListener() {
+ return mListener != null;
+ }
+
+ /**
+ * <p>Binds an animation listener to this animation. The animation listener
+ * is notified of animation events such as the end of the animation or the
+ * repetition of the animation.</p>
+ *
+ * @param listener the animation listener to be notified
+ */
+ public void setAnimationListener(AnimationListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Gurantees that this animation has an interpolator. Will use
+ * a AccelerateDecelerateInterpolator is nothing else was specified.
+ */
+ protected void ensureInterpolator() {
+ if (mInterpolator == null) {
+ mInterpolator = new AccelerateDecelerateInterpolator();
+ }
+ }
+
+ /**
+ * Compute a hint at how long the entire animation may last, in milliseconds.
+ * Animations can be written to cause themselves to run for a different
+ * duration than what is computed here, but generally this should be
+ * accurate.
+ */
+ public long computeDurationHint() {
+ return (getStartOffset() + getDuration()) * (getRepeatCount() + 1);
+ }
+
+ /**
+ * Gets the transformation to apply at a specified point in time. Implementations of this
+ * method should always replace the specified Transformation or document they are doing
+ * otherwise.
+ *
+ * @param currentTime Where we are in the animation. This is wall clock time.
+ * @param outTransformation A transformation object that is provided by the
+ * caller and will be filled in by the animation.
+ * @return True if the animation is still running
+ */
+ public boolean getTransformation(long currentTime, Transformation outTransformation) {
+ if (mStartTime == -1) {
+ mStartTime = currentTime;
+ }
+
+ final long startOffset = getStartOffset();
+ final long duration = mDuration;
+ float normalizedTime;
+ if (duration != 0) {
+ normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
+ (float) duration;
+ } else {
+ // time is a step-change with a zero duration
+ normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
+ }
+
+ final boolean expired = normalizedTime >= 1.0f || isCanceled();
+ mMore = !expired;
+
+ if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
+
+ if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
+ if (!mStarted) {
+ fireAnimationStart();
+ mStarted = true;
+ if (NoImagePreloadHolder.USE_CLOSEGUARD) {
+ guard.open("cancel or detach or getTransformation");
+ }
+ }
+
+ if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
+
+ if (mCycleFlip) {
+ normalizedTime = 1.0f - normalizedTime;
+ }
+
+ final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
+ applyTransformation(interpolatedTime, outTransformation);
+ }
+
+ if (expired) {
+ if (mRepeatCount == mRepeated || isCanceled()) {
+ if (!mEnded) {
+ mEnded = true;
+ guard.close();
+ fireAnimationEnd();
+ }
+ } else {
+ if (mRepeatCount > 0) {
+ mRepeated++;
+ }
+
+ if (mRepeatMode == REVERSE) {
+ mCycleFlip = !mCycleFlip;
+ }
+
+ mStartTime = -1;
+ mMore = true;
+
+ fireAnimationRepeat();
+ }
+ }
+
+ if (!mMore && mOneMoreTime) {
+ mOneMoreTime = false;
+ return true;
+ }
+
+ return mMore;
+ }
+
+ private boolean isCanceled() {
+ return mStartTime == Long.MIN_VALUE;
+ }
+
+ private void fireAnimationStart() {
+ if (hasAnimationListener()) {
+ if (mListenerHandler == null) dispatchAnimationStart();
+ else mListenerHandler.postAtFrontOfQueue(mOnStart);
+ }
+ }
+
+ private void fireAnimationRepeat() {
+ if (hasAnimationListener()) {
+ if (mListenerHandler == null) dispatchAnimationRepeat();
+ else mListenerHandler.postAtFrontOfQueue(mOnRepeat);
+ }
+ }
+
+ private void fireAnimationEnd() {
+ if (hasAnimationListener()) {
+ if (mListenerHandler == null) dispatchAnimationEnd();
+ else mListenerHandler.postAtFrontOfQueue(mOnEnd);
+ }
+ }
+
+ void dispatchAnimationStart() {
+ if (mListener != null) {
+ mListener.onAnimationStart(this);
+ }
+ }
+
+ void dispatchAnimationRepeat() {
+ if (mListener != null) {
+ mListener.onAnimationRepeat(this);
+ }
+ }
+
+ void dispatchAnimationEnd() {
+ if (mListener != null) {
+ mListener.onAnimationEnd(this);
+ }
+ }
+
+ /**
+ * Gets the transformation to apply at a specified point in time. Implementations of this
+ * method should always replace the specified Transformation or document they are doing
+ * otherwise.
+ *
+ * @param currentTime Where we are in the animation. This is wall clock time.
+ * @param outTransformation A transformation object that is provided by the
+ * caller and will be filled in by the animation.
+ * @param scale Scaling factor to apply to any inputs to the transform operation, such
+ * pivot points being rotated or scaled around.
+ * @return True if the animation is still running
+ */
+ public boolean getTransformation(long currentTime, Transformation outTransformation,
+ float scale) {
+ mScaleFactor = scale;
+ return getTransformation(currentTime, outTransformation);
+ }
+
+ /**
+ * <p>Indicates whether this animation has started or not.</p>
+ *
+ * @return true if the animation has started, false otherwise
+ */
+ public boolean hasStarted() {
+ return mStarted;
+ }
+
+ /**
+ * <p>Indicates whether this animation has ended or not.</p>
+ *
+ * @return true if the animation has ended, false otherwise
+ */
+ public boolean hasEnded() {
+ return mEnded;
+ }
+
+ /**
+ * Helper for getTransformation. Subclasses should implement this to apply
+ * their transforms given an interpolation value. Implementations of this
+ * method should always replace the specified Transformation or document
+ * they are doing otherwise.
+ *
+ * @param interpolatedTime The value of the normalized time (0.0 to 1.0)
+ * after it has been run through the interpolation function.
+ * @param t The Transformation object to fill in with the current
+ * transforms.
+ */
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ }
+
+ /**
+ * Convert the information in the description of a size to an actual
+ * dimension
+ *
+ * @param type One of Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param value The dimension associated with the type parameter
+ * @param size The size of the object being animated
+ * @param parentSize The size of the parent of the object being animated
+ * @return The dimension to use for the animation
+ */
+ protected float resolveSize(int type, float value, int size, int parentSize) {
+ switch (type) {
+ case ABSOLUTE:
+ return value;
+ case RELATIVE_TO_SELF:
+ return size * value;
+ case RELATIVE_TO_PARENT:
+ return parentSize * value;
+ default:
+ return value;
+ }
+ }
+
+ /**
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ * @param invalidate
+ * @param transformation
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void getInvalidateRegion(int left, int top, int right, int bottom,
+ RectF invalidate, Transformation transformation) {
+
+ final RectF tempRegion = mRegion;
+ final RectF previousRegion = mPreviousRegion;
+
+ invalidate.set(left, top, right, bottom);
+ transformation.getMatrix().mapRect(invalidate);
+ // Enlarge the invalidate region to account for rounding errors
+ invalidate.inset(-1.0f, -1.0f);
+ tempRegion.set(invalidate);
+ invalidate.union(previousRegion);
+
+ previousRegion.set(tempRegion);
+
+ final Transformation tempTransformation = mTransformation;
+ final Transformation previousTransformation = mPreviousTransformation;
+
+ tempTransformation.set(transformation);
+ transformation.set(previousTransformation);
+ previousTransformation.set(tempTransformation);
+ }
+
+ /**
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void initializeInvalidateRegion(int left, int top, int right, int bottom) {
+ final RectF region = mPreviousRegion;
+ region.set(left, top, right, bottom);
+ // Enlarge the invalidate region to account for rounding errors
+ region.inset(-1.0f, -1.0f);
+ if (mFillBefore) {
+ final Transformation previousTransformation = mPreviousTransformation;
+ applyTransformation(mInterpolator.getInterpolation(0.0f), previousTransformation);
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ if (guard != null) {
+ guard.warnIfOpen();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Return true if this animation changes the view's alpha property.
+ *
+ * @hide
+ */
+ public boolean hasAlpha() {
+ return false;
+ }
+
+ /**
+ * Utility class to parse a string description of a size.
+ */
+ protected static class Description {
+ /**
+ * One of Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ */
+ public int type;
+
+ /**
+ * The absolute or relative dimension for this Description.
+ */
+ public float value;
+
+ /**
+ * Size descriptions can appear inthree forms:
+ * <ol>
+ * <li>An absolute size. This is represented by a number.</li>
+ * <li>A size relative to the size of the object being animated. This
+ * is represented by a number followed by "%".</li> *
+ * <li>A size relative to the size of the parent of object being
+ * animated. This is represented by a number followed by "%p".</li>
+ * </ol>
+ * @param value The typed value to parse
+ * @return The parsed version of the description
+ */
+ static Description parseValue(TypedValue value) {
+ Description d = new Description();
+ if (value == null) {
+ d.type = ABSOLUTE;
+ d.value = 0;
+ } else {
+ if (value.type == TypedValue.TYPE_FRACTION) {
+ d.type = (value.data & TypedValue.COMPLEX_UNIT_MASK) ==
+ TypedValue.COMPLEX_UNIT_FRACTION_PARENT ?
+ RELATIVE_TO_PARENT : RELATIVE_TO_SELF;
+ d.value = TypedValue.complexToFloat(value.data);
+ return d;
+ } else if (value.type == TypedValue.TYPE_FLOAT) {
+ d.type = ABSOLUTE;
+ d.value = value.getFloat();
+ return d;
+ } else if (value.type >= TypedValue.TYPE_FIRST_INT &&
+ value.type <= TypedValue.TYPE_LAST_INT) {
+ d.type = ABSOLUTE;
+ d.value = value.data;
+ return d;
+ }
+ }
+
+ d.type = ABSOLUTE;
+ d.value = 0.0f;
+
+ return d;
+ }
+ }
+
+ /**
+ * <p>An animation listener receives notifications from an animation.
+ * Notifications indicate animation related events, such as the end or the
+ * repetition of the animation.</p>
+ */
+ public static interface AnimationListener {
+ /**
+ * <p>Notifies the start of the animation.</p>
+ *
+ * @param animation The started animation.
+ */
+ void onAnimationStart(Animation animation);
+
+ /**
+ * <p>Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * @param animation The animation which reached its end.
+ */
+ void onAnimationEnd(Animation animation);
+
+ /**
+ * <p>Notifies the repetition of the animation.</p>
+ *
+ * @param animation The animation which was repeated.
+ */
+ void onAnimationRepeat(Animation animation);
+ }
+}
diff --git a/android/view/animation/AnimationSet.java b/android/view/animation/AnimationSet.java
new file mode 100644
index 0000000..03c6ca6
--- /dev/null
+++ b/android/view/animation/AnimationSet.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a group of Animations that should be played together.
+ * The transformation of each individual animation are composed
+ * together into a single transform.
+ * If AnimationSet sets any properties that its children also set
+ * (for example, duration or fillBefore), the values of AnimationSet
+ * override the child values.
+ *
+ * <p>The way that AnimationSet inherits behavior from Animation is important to
+ * understand. Some of the Animation attributes applied to AnimationSet affect the
+ * AnimationSet itself, some are pushed down to the children, and some are ignored,
+ * as follows:
+ * <ul>
+ * <li>duration, repeatMode, fillBefore, fillAfter: These properties, when set
+ * on an AnimationSet object, will be pushed down to all child animations.</li>
+ * <li>repeatCount, fillEnabled: These properties are ignored for AnimationSet.</li>
+ * <li>startOffset, shareInterpolator: These properties apply to the AnimationSet itself.</li>
+ * </ul>
+ * Starting with {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH},
+ * the behavior of these properties is the same in XML resources and at runtime (prior to that
+ * release, the values set in XML were ignored for AnimationSet). That is, calling
+ * <code>setDuration(500)</code> on an AnimationSet has the same effect as declaring
+ * <code>android:duration="500"</code> in an XML resource for an AnimationSet object.</p>
+ */
+public class AnimationSet extends Animation {
+ private static final int PROPERTY_FILL_AFTER_MASK = 0x1;
+ private static final int PROPERTY_FILL_BEFORE_MASK = 0x2;
+ private static final int PROPERTY_REPEAT_MODE_MASK = 0x4;
+ private static final int PROPERTY_START_OFFSET_MASK = 0x8;
+ private static final int PROPERTY_SHARE_INTERPOLATOR_MASK = 0x10;
+ private static final int PROPERTY_DURATION_MASK = 0x20;
+ private static final int PROPERTY_MORPH_MATRIX_MASK = 0x40;
+ private static final int PROPERTY_CHANGE_BOUNDS_MASK = 0x80;
+
+ private int mFlags = 0;
+ private boolean mDirty;
+ private boolean mHasAlpha;
+
+ private ArrayList<Animation> mAnimations = new ArrayList<Animation>();
+
+ private Transformation mTempTransformation = new Transformation();
+
+ private long mLastEnd;
+
+ private long[] mStoredOffsets;
+
+ /**
+ * Constructor used when an AnimationSet is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public AnimationSet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AnimationSet);
+
+ setFlag(PROPERTY_SHARE_INTERPOLATOR_MASK,
+ a.getBoolean(com.android.internal.R.styleable.AnimationSet_shareInterpolator, true));
+ init();
+
+ if (context.getApplicationInfo().targetSdkVersion >=
+ Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if (a.hasValue(com.android.internal.R.styleable.AnimationSet_duration)) {
+ mFlags |= PROPERTY_DURATION_MASK;
+ }
+ if (a.hasValue(com.android.internal.R.styleable.AnimationSet_fillBefore)) {
+ mFlags |= PROPERTY_FILL_BEFORE_MASK;
+ }
+ if (a.hasValue(com.android.internal.R.styleable.AnimationSet_fillAfter)) {
+ mFlags |= PROPERTY_FILL_AFTER_MASK;
+ }
+ if (a.hasValue(com.android.internal.R.styleable.AnimationSet_repeatMode)) {
+ mFlags |= PROPERTY_REPEAT_MODE_MASK;
+ }
+ if (a.hasValue(com.android.internal.R.styleable.AnimationSet_startOffset)) {
+ mFlags |= PROPERTY_START_OFFSET_MASK;
+ }
+ }
+
+ a.recycle();
+ }
+
+
+ /**
+ * Constructor to use when building an AnimationSet from code
+ *
+ * @param shareInterpolator Pass true if all of the animations in this set
+ * should use the interpolator associated with this AnimationSet.
+ * Pass false if each animation should use its own interpolator.
+ */
+ public AnimationSet(boolean shareInterpolator) {
+ setFlag(PROPERTY_SHARE_INTERPOLATOR_MASK, shareInterpolator);
+ init();
+ }
+
+ @Override
+ protected AnimationSet clone() throws CloneNotSupportedException {
+ final AnimationSet animation = (AnimationSet) super.clone();
+ animation.mTempTransformation = new Transformation();
+ animation.mAnimations = new ArrayList<Animation>();
+
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+
+ for (int i = 0; i < count; i++) {
+ animation.mAnimations.add(animations.get(i).clone());
+ }
+
+ return animation;
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+
+ private void init() {
+ mStartTime = 0;
+ }
+
+ @Override
+ public void setFillAfter(boolean fillAfter) {
+ mFlags |= PROPERTY_FILL_AFTER_MASK;
+ super.setFillAfter(fillAfter);
+ }
+
+ @Override
+ public void setFillBefore(boolean fillBefore) {
+ mFlags |= PROPERTY_FILL_BEFORE_MASK;
+ super.setFillBefore(fillBefore);
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ mFlags |= PROPERTY_REPEAT_MODE_MASK;
+ super.setRepeatMode(repeatMode);
+ }
+
+ @Override
+ public void setStartOffset(long startOffset) {
+ mFlags |= PROPERTY_START_OFFSET_MASK;
+ super.setStartOffset(startOffset);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean hasAlpha() {
+ if (mDirty) {
+ mDirty = mHasAlpha = false;
+
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+
+ for (int i = 0; i < count; i++) {
+ if (animations.get(i).hasAlpha()) {
+ mHasAlpha = true;
+ break;
+ }
+ }
+ }
+
+ return mHasAlpha;
+ }
+
+ /**
+ * <p>Sets the duration of every child animation.</p>
+ *
+ * @param durationMillis the duration of the animation, in milliseconds, for
+ * every child in this set
+ */
+ @Override
+ public void setDuration(long durationMillis) {
+ mFlags |= PROPERTY_DURATION_MASK;
+ super.setDuration(durationMillis);
+ mLastEnd = mStartOffset + mDuration;
+ }
+
+ /**
+ * Add a child animation to this animation set.
+ * The transforms of the child animations are applied in the order
+ * that they were added
+ * @param a Animation to add.
+ */
+ public void addAnimation(Animation a) {
+ mAnimations.add(a);
+
+ boolean noMatrix = (mFlags & PROPERTY_MORPH_MATRIX_MASK) == 0;
+ if (noMatrix && a.willChangeTransformationMatrix()) {
+ mFlags |= PROPERTY_MORPH_MATRIX_MASK;
+ }
+
+ boolean changeBounds = (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == 0;
+
+
+ if (changeBounds && a.willChangeBounds()) {
+ mFlags |= PROPERTY_CHANGE_BOUNDS_MASK;
+ }
+
+ if ((mFlags & PROPERTY_DURATION_MASK) == PROPERTY_DURATION_MASK) {
+ mLastEnd = mStartOffset + mDuration;
+ } else {
+ if (mAnimations.size() == 1) {
+ mDuration = a.getStartOffset() + a.getDuration();
+ mLastEnd = mStartOffset + mDuration;
+ } else {
+ mLastEnd = Math.max(mLastEnd, mStartOffset + a.getStartOffset() + a.getDuration());
+ mDuration = mLastEnd - mStartOffset;
+ }
+ }
+
+ mDirty = true;
+ }
+
+ /**
+ * Sets the start time of this animation and all child animations
+ *
+ * @see android.view.animation.Animation#setStartTime(long)
+ */
+ @Override
+ public void setStartTime(long startTimeMillis) {
+ super.setStartTime(startTimeMillis);
+
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+
+ for (int i = 0; i < count; i++) {
+ Animation a = animations.get(i);
+ a.setStartTime(startTimeMillis);
+ }
+ }
+
+ @Override
+ public long getStartTime() {
+ long startTime = Long.MAX_VALUE;
+
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+
+ for (int i = 0; i < count; i++) {
+ Animation a = animations.get(i);
+ startTime = Math.min(startTime, a.getStartTime());
+ }
+
+ return startTime;
+ }
+
+ @Override
+ public void restrictDuration(long durationMillis) {
+ super.restrictDuration(durationMillis);
+
+ final ArrayList<Animation> animations = mAnimations;
+ int count = animations.size();
+
+ for (int i = 0; i < count; i++) {
+ animations.get(i).restrictDuration(durationMillis);
+ }
+ }
+
+ /**
+ * The duration of an AnimationSet is defined to be the
+ * duration of the longest child animation.
+ *
+ * @see android.view.animation.Animation#getDuration()
+ */
+ @Override
+ public long getDuration() {
+ final ArrayList<Animation> animations = mAnimations;
+ final int count = animations.size();
+ long duration = 0;
+
+ boolean durationSet = (mFlags & PROPERTY_DURATION_MASK) == PROPERTY_DURATION_MASK;
+ if (durationSet) {
+ duration = mDuration;
+ } else {
+ for (int i = 0; i < count; i++) {
+ duration = Math.max(duration, animations.get(i).getDuration());
+ }
+ }
+
+ return duration;
+ }
+
+ /**
+ * The duration hint of an animation set is the maximum of the duration
+ * hints of all of its component animations.
+ *
+ * @see android.view.animation.Animation#computeDurationHint
+ */
+ public long computeDurationHint() {
+ long duration = 0;
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+ for (int i = count - 1; i >= 0; --i) {
+ final long d = animations.get(i).computeDurationHint();
+ if (d > duration) duration = d;
+ }
+ return duration;
+ }
+
+ /**
+ * @hide
+ */
+ public void initializeInvalidateRegion(int left, int top, int right, int bottom) {
+ final RectF region = mPreviousRegion;
+ region.set(left, top, right, bottom);
+ region.inset(-1.0f, -1.0f);
+
+ if (mFillBefore) {
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+ final Transformation temp = mTempTransformation;
+
+ final Transformation previousTransformation = mPreviousTransformation;
+
+ for (int i = count - 1; i >= 0; --i) {
+ final Animation a = animations.get(i);
+ if (!a.isFillEnabled() || a.getFillBefore() || a.getStartOffset() == 0) {
+ temp.clear();
+ final Interpolator interpolator = a.mInterpolator;
+ a.applyTransformation(interpolator != null ? interpolator.getInterpolation(0.0f)
+ : 0.0f, temp);
+ previousTransformation.compose(temp);
+ }
+ }
+ }
+ }
+
+ /**
+ * The transformation of an animation set is the concatenation of all of its
+ * component animations.
+ *
+ * @see android.view.animation.Animation#getTransformation
+ */
+ @Override
+ public boolean getTransformation(long currentTime, Transformation t) {
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+ final Transformation temp = mTempTransformation;
+
+ boolean more = false;
+ boolean started = false;
+ boolean ended = true;
+
+ t.clear();
+
+ for (int i = count - 1; i >= 0; --i) {
+ final Animation a = animations.get(i);
+
+ temp.clear();
+ more = a.getTransformation(currentTime, temp, getScaleFactor()) || more;
+ t.compose(temp);
+
+ started = started || a.hasStarted();
+ ended = a.hasEnded() && ended;
+ }
+
+ if (started && !mStarted) {
+ dispatchAnimationStart();
+ mStarted = true;
+ }
+
+ if (ended != mEnded) {
+ dispatchAnimationEnd();
+ mEnded = ended;
+ }
+
+ return more;
+ }
+
+ /**
+ * @see android.view.animation.Animation#scaleCurrentDuration(float)
+ */
+ @Override
+ public void scaleCurrentDuration(float scale) {
+ final ArrayList<Animation> animations = mAnimations;
+ int count = animations.size();
+ for (int i = 0; i < count; i++) {
+ animations.get(i).scaleCurrentDuration(scale);
+ }
+ }
+
+ /**
+ * @see android.view.animation.Animation#initialize(int, int, int, int)
+ */
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+
+ boolean durationSet = (mFlags & PROPERTY_DURATION_MASK) == PROPERTY_DURATION_MASK;
+ boolean fillAfterSet = (mFlags & PROPERTY_FILL_AFTER_MASK) == PROPERTY_FILL_AFTER_MASK;
+ boolean fillBeforeSet = (mFlags & PROPERTY_FILL_BEFORE_MASK) == PROPERTY_FILL_BEFORE_MASK;
+ boolean repeatModeSet = (mFlags & PROPERTY_REPEAT_MODE_MASK) == PROPERTY_REPEAT_MODE_MASK;
+ boolean shareInterpolator = (mFlags & PROPERTY_SHARE_INTERPOLATOR_MASK)
+ == PROPERTY_SHARE_INTERPOLATOR_MASK;
+ boolean startOffsetSet = (mFlags & PROPERTY_START_OFFSET_MASK)
+ == PROPERTY_START_OFFSET_MASK;
+
+ if (shareInterpolator) {
+ ensureInterpolator();
+ }
+
+ final ArrayList<Animation> children = mAnimations;
+ final int count = children.size();
+
+ final long duration = mDuration;
+ final boolean fillAfter = mFillAfter;
+ final boolean fillBefore = mFillBefore;
+ final int repeatMode = mRepeatMode;
+ final Interpolator interpolator = mInterpolator;
+ final long startOffset = mStartOffset;
+
+
+ long[] storedOffsets = mStoredOffsets;
+ if (startOffsetSet) {
+ if (storedOffsets == null || storedOffsets.length != count) {
+ storedOffsets = mStoredOffsets = new long[count];
+ }
+ } else if (storedOffsets != null) {
+ storedOffsets = mStoredOffsets = null;
+ }
+
+ for (int i = 0; i < count; i++) {
+ Animation a = children.get(i);
+ if (durationSet) {
+ a.setDuration(duration);
+ }
+ if (fillAfterSet) {
+ a.setFillAfter(fillAfter);
+ }
+ if (fillBeforeSet) {
+ a.setFillBefore(fillBefore);
+ }
+ if (repeatModeSet) {
+ a.setRepeatMode(repeatMode);
+ }
+ if (shareInterpolator) {
+ a.setInterpolator(interpolator);
+ }
+ if (startOffsetSet) {
+ long offset = a.getStartOffset();
+ a.setStartOffset(offset + startOffset);
+ storedOffsets[i] = offset;
+ }
+ a.initialize(width, height, parentWidth, parentHeight);
+ }
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ restoreChildrenStartOffset();
+ }
+
+ /**
+ * @hide
+ */
+ void restoreChildrenStartOffset() {
+ final long[] offsets = mStoredOffsets;
+ if (offsets == null) return;
+
+ final ArrayList<Animation> children = mAnimations;
+ final int count = children.size();
+
+ for (int i = 0; i < count; i++) {
+ children.get(i).setStartOffset(offsets[i]);
+ }
+ }
+
+ /**
+ * @return All the child animations in this AnimationSet. Note that
+ * this may include other AnimationSets, which are not expanded.
+ */
+ public List<Animation> getAnimations() {
+ return mAnimations;
+ }
+
+ @Override
+ public boolean willChangeTransformationMatrix() {
+ return (mFlags & PROPERTY_MORPH_MATRIX_MASK) == PROPERTY_MORPH_MATRIX_MASK;
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == PROPERTY_CHANGE_BOUNDS_MASK;
+ }
+}
diff --git a/android/view/animation/AnimationUtils.java b/android/view/animation/AnimationUtils.java
new file mode 100644
index 0000000..7ce0f45
--- /dev/null
+++ b/android/view/animation/AnimationUtils.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.annotation.AnimRes;
+import android.annotation.InterpolatorRes;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.Resources.Theme;
+import android.content.res.XmlResourceParser;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Defines common utilities for working with animations.
+ *
+ */
+public class AnimationUtils {
+
+ /**
+ * These flags are used when parsing AnimatorSet objects
+ */
+ private static final int TOGETHER = 0;
+ private static final int SEQUENTIALLY = 1;
+
+ private static class AnimationState {
+ boolean animationClockLocked;
+ long currentVsyncTimeMillis;
+ long lastReportedTimeMillis;
+ };
+
+ private static ThreadLocal<AnimationState> sAnimationState
+ = new ThreadLocal<AnimationState>() {
+ @Override
+ protected AnimationState initialValue() {
+ return new AnimationState();
+ }
+ };
+
+ /**
+ * Locks AnimationUtils{@link #currentAnimationTimeMillis()} to a fixed value for the current
+ * thread. This is used by {@link android.view.Choreographer} to ensure that all accesses
+ * during a vsync update are synchronized to the timestamp of the vsync.
+ *
+ * It is also exposed to tests to allow for rapid, flake-free headless testing.
+ *
+ * Must be followed by a call to {@link #unlockAnimationClock()} to allow time to
+ * progress. Failing to do this will result in stuck animations, scrolls, and flings.
+ *
+ * Note that time is not allowed to "rewind" and must perpetually flow forward. So the
+ * lock may fail if the time is in the past from a previously returned value, however
+ * time will be frozen for the duration of the lock. The clock is a thread-local, so
+ * ensure that {@link #lockAnimationClock(long)}, {@link #unlockAnimationClock()}, and
+ * {@link #currentAnimationTimeMillis()} are all called on the same thread.
+ *
+ * This is also not reference counted in any way. Any call to {@link #unlockAnimationClock()}
+ * will unlock the clock for everyone on the same thread. It is therefore recommended
+ * for tests to use their own thread to ensure that there is no collision with any existing
+ * {@link android.view.Choreographer} instance.
+ *
+ * @hide
+ * */
+ @TestApi
+ public static void lockAnimationClock(long vsyncMillis) {
+ AnimationState state = sAnimationState.get();
+ state.animationClockLocked = true;
+ state.currentVsyncTimeMillis = vsyncMillis;
+ }
+
+ /**
+ * Frees the time lock set in place by {@link #lockAnimationClock(long)}. Must be called
+ * to allow the animation clock to self-update.
+ *
+ * @hide
+ */
+ @TestApi
+ public static void unlockAnimationClock() {
+ sAnimationState.get().animationClockLocked = false;
+ }
+
+ /**
+ * Returns the current animation time in milliseconds. This time should be used when invoking
+ * {@link Animation#setStartTime(long)}. Refer to {@link android.os.SystemClock} for more
+ * information about the different available clocks. The clock used by this method is
+ * <em>not</em> the "wall" clock (it is not {@link System#currentTimeMillis}).
+ *
+ * @return the current animation time in milliseconds
+ *
+ * @see android.os.SystemClock
+ */
+ public static long currentAnimationTimeMillis() {
+ AnimationState state = sAnimationState.get();
+ if (state.animationClockLocked) {
+ // It's important that time never rewinds
+ return Math.max(state.currentVsyncTimeMillis,
+ state.lastReportedTimeMillis);
+ }
+ state.lastReportedTimeMillis = SystemClock.uptimeMillis();
+ return state.lastReportedTimeMillis;
+ }
+
+ /**
+ * Loads an {@link Animation} object from a resource
+ *
+ * @param context Application context used to access resources
+ * @param id The resource id of the animation to load
+ * @return The animation object referenced by the specified id
+ * @throws NotFoundException when the animation cannot be loaded
+ */
+ public static Animation loadAnimation(Context context, @AnimRes int id)
+ throws NotFoundException {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createAnimationFromXml(context, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+
+ return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
+ }
+
+ @UnsupportedAppUsage
+ private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
+ AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
+
+ Animation anim = null;
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if (name.equals("set")) {
+ anim = new AnimationSet(c, attrs);
+ createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
+ } else if (name.equals("alpha")) {
+ anim = new AlphaAnimation(c, attrs);
+ } else if (name.equals("scale")) {
+ anim = new ScaleAnimation(c, attrs);
+ } else if (name.equals("rotate")) {
+ anim = new RotateAnimation(c, attrs);
+ } else if (name.equals("translate")) {
+ anim = new TranslateAnimation(c, attrs);
+ } else if (name.equals("cliprect")) {
+ anim = new ClipRectAnimation(c, attrs);
+ } else {
+ throw new RuntimeException("Unknown animation name: " + parser.getName());
+ }
+
+ if (parent != null) {
+ parent.addAnimation(anim);
+ }
+ }
+
+ return anim;
+
+ }
+
+ /**
+ * Loads a {@link LayoutAnimationController} object from a resource
+ *
+ * @param context Application context used to access resources
+ * @param id The resource id of the animation to load
+ * @return The animation controller object referenced by the specified id
+ * @throws NotFoundException when the layout animation controller cannot be loaded
+ */
+ public static LayoutAnimationController loadLayoutAnimation(Context context, @AnimRes int id)
+ throws NotFoundException {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createLayoutAnimationFromXml(context, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private static LayoutAnimationController createLayoutAnimationFromXml(Context c,
+ XmlPullParser parser) throws XmlPullParserException, IOException {
+
+ return createLayoutAnimationFromXml(c, parser, Xml.asAttributeSet(parser));
+ }
+
+ private static LayoutAnimationController createLayoutAnimationFromXml(Context c,
+ XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
+
+ LayoutAnimationController controller = null;
+
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if ("layoutAnimation".equals(name)) {
+ controller = new LayoutAnimationController(c, attrs);
+ } else if ("gridLayoutAnimation".equals(name)) {
+ controller = new GridLayoutAnimationController(c, attrs);
+ } else {
+ throw new RuntimeException("Unknown layout animation name: " + name);
+ }
+ }
+
+ return controller;
+ }
+
+ /**
+ * Make an animation for objects becoming visible. Uses a slide and fade
+ * effect.
+ *
+ * @param c Context for loading resources
+ * @param fromLeft is the object to be animated coming from the left
+ * @return The new animation
+ */
+ public static Animation makeInAnimation(Context c, boolean fromLeft) {
+ Animation a;
+ if (fromLeft) {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_left);
+ } else {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_right);
+ }
+
+ a.setInterpolator(new DecelerateInterpolator());
+ a.setStartTime(currentAnimationTimeMillis());
+ return a;
+ }
+
+ /**
+ * Make an animation for objects becoming invisible. Uses a slide and fade
+ * effect.
+ *
+ * @param c Context for loading resources
+ * @param toRight is the object to be animated exiting to the right
+ * @return The new animation
+ */
+ public static Animation makeOutAnimation(Context c, boolean toRight) {
+ Animation a;
+ if (toRight) {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_right);
+ } else {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_left);
+ }
+
+ a.setInterpolator(new AccelerateInterpolator());
+ a.setStartTime(currentAnimationTimeMillis());
+ return a;
+ }
+
+
+ /**
+ * Make an animation for objects becoming visible. Uses a slide up and fade
+ * effect.
+ *
+ * @param c Context for loading resources
+ * @return The new animation
+ */
+ public static Animation makeInChildBottomAnimation(Context c) {
+ Animation a;
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_child_bottom);
+ a.setInterpolator(new AccelerateInterpolator());
+ a.setStartTime(currentAnimationTimeMillis());
+ return a;
+ }
+
+ /**
+ * Loads an {@link Interpolator} object from a resource
+ *
+ * @param context Application context used to access resources
+ * @param id The resource id of the animation to load
+ * @return The interpolator object referenced by the specified id
+ * @throws NotFoundException
+ */
+ public static Interpolator loadInterpolator(Context context, @AnimRes @InterpolatorRes int id)
+ throws NotFoundException {
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createInterpolatorFromXml(context.getResources(), context.getTheme(), parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+
+ }
+
+ /**
+ * Loads an {@link Interpolator} object from a resource
+ *
+ * @param res The resources
+ * @param id The resource id of the animation to load
+ * @return The interpolator object referenced by the specified id
+ * @throws NotFoundException
+ * @hide
+ */
+ public static Interpolator loadInterpolator(Resources res, Theme theme, int id) throws NotFoundException {
+ XmlResourceParser parser = null;
+ try {
+ parser = res.getAnimation(id);
+ return createInterpolatorFromXml(res, theme, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null)
+ parser.close();
+ }
+
+ }
+
+ private static Interpolator createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+
+ BaseInterpolator interpolator = null;
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ String name = parser.getName();
+
+ if (name.equals("linearInterpolator")) {
+ interpolator = new LinearInterpolator();
+ } else if (name.equals("accelerateInterpolator")) {
+ interpolator = new AccelerateInterpolator(res, theme, attrs);
+ } else if (name.equals("decelerateInterpolator")) {
+ interpolator = new DecelerateInterpolator(res, theme, attrs);
+ } else if (name.equals("accelerateDecelerateInterpolator")) {
+ interpolator = new AccelerateDecelerateInterpolator();
+ } else if (name.equals("cycleInterpolator")) {
+ interpolator = new CycleInterpolator(res, theme, attrs);
+ } else if (name.equals("anticipateInterpolator")) {
+ interpolator = new AnticipateInterpolator(res, theme, attrs);
+ } else if (name.equals("overshootInterpolator")) {
+ interpolator = new OvershootInterpolator(res, theme, attrs);
+ } else if (name.equals("anticipateOvershootInterpolator")) {
+ interpolator = new AnticipateOvershootInterpolator(res, theme, attrs);
+ } else if (name.equals("bounceInterpolator")) {
+ interpolator = new BounceInterpolator();
+ } else if (name.equals("pathInterpolator")) {
+ interpolator = new PathInterpolator(res, theme, attrs);
+ } else {
+ throw new RuntimeException("Unknown interpolator name: " + parser.getName());
+ }
+ }
+ return interpolator;
+ }
+}
diff --git a/android/view/animation/AnticipateInterpolator.java b/android/view/animation/AnticipateInterpolator.java
new file mode 100644
index 0000000..d146394
--- /dev/null
+++ b/android/view/animation/AnticipateInterpolator.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * An interpolator where the change starts backward then flings forward.
+ */
+@HasNativeInterpolator
+public class AnticipateInterpolator extends BaseInterpolator implements NativeInterpolator {
+ private final float mTension;
+
+ public AnticipateInterpolator() {
+ mTension = 2.0f;
+ }
+
+ /**
+ * @param tension Amount of anticipation. When tension equals 0.0f, there is
+ * no anticipation and the interpolator becomes a simple
+ * acceleration interpolator.
+ */
+ public AnticipateInterpolator(float tension) {
+ mTension = tension;
+ }
+
+ public AnticipateInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public AnticipateInterpolator(Resources res, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.AnticipateInterpolator, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.AnticipateInterpolator);
+ }
+
+ mTension = a.getFloat(R.styleable.AnticipateInterpolator_tension, 2.0f);
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ public float getInterpolation(float t) {
+ // a(t) = t * t * ((tension + 1) * t - tension)
+ return t * t * ((mTension + 1) * t - mTension);
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createAnticipateInterpolator(mTension);
+ }
+}
diff --git a/android/view/animation/AnticipateOvershootInterpolator.java b/android/view/animation/AnticipateOvershootInterpolator.java
new file mode 100644
index 0000000..4d6a390
--- /dev/null
+++ b/android/view/animation/AnticipateOvershootInterpolator.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.animation;
+
+import static com.android.internal.R.styleable.AnticipateOvershootInterpolator;
+import static com.android.internal.R.styleable.AnticipateOvershootInterpolator_extraTension;
+import static com.android.internal.R.styleable.AnticipateOvershootInterpolator_tension;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+
+/**
+ * An interpolator where the change starts backward then flings forward and overshoots
+ * the target value and finally goes back to the final value.
+ */
+@HasNativeInterpolator
+public class AnticipateOvershootInterpolator extends BaseInterpolator
+ implements NativeInterpolator {
+ private final float mTension;
+
+ public AnticipateOvershootInterpolator() {
+ mTension = 2.0f * 1.5f;
+ }
+
+ /**
+ * @param tension Amount of anticipation/overshoot. When tension equals 0.0f,
+ * there is no anticipation/overshoot and the interpolator becomes
+ * a simple acceleration/deceleration interpolator.
+ */
+ public AnticipateOvershootInterpolator(float tension) {
+ mTension = tension * 1.5f;
+ }
+
+ /**
+ * @param tension Amount of anticipation/overshoot. When tension equals 0.0f,
+ * there is no anticipation/overshoot and the interpolator becomes
+ * a simple acceleration/deceleration interpolator.
+ * @param extraTension Amount by which to multiply the tension. For instance,
+ * to get the same overshoot as an OvershootInterpolator with
+ * a tension of 2.0f, you would use an extraTension of 1.5f.
+ */
+ public AnticipateOvershootInterpolator(float tension, float extraTension) {
+ mTension = tension * extraTension;
+ }
+
+ public AnticipateOvershootInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public AnticipateOvershootInterpolator(Resources res, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, AnticipateOvershootInterpolator, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, AnticipateOvershootInterpolator);
+ }
+
+ mTension = a.getFloat(AnticipateOvershootInterpolator_tension, 2.0f) *
+ a.getFloat(AnticipateOvershootInterpolator_extraTension, 1.5f);
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ private static float a(float t, float s) {
+ return t * t * ((s + 1) * t - s);
+ }
+
+ private static float o(float t, float s) {
+ return t * t * ((s + 1) * t + s);
+ }
+
+ public float getInterpolation(float t) {
+ // a(t, s) = t * t * ((s + 1) * t - s)
+ // o(t, s) = t * t * ((s + 1) * t + s)
+ // f(t) = 0.5 * a(t * 2, tension * extraTension), when t < 0.5
+ // f(t) = 0.5 * (o(t * 2 - 2, tension * extraTension) + 2), when t <= 1.0
+ if (t < 0.5f) return 0.5f * a(t * 2.0f, mTension);
+ else return 0.5f * (o(t * 2.0f - 2.0f, mTension) + 2.0f);
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createAnticipateOvershootInterpolator(mTension);
+ }
+}
diff --git a/android/view/animation/BaseInterpolator.java b/android/view/animation/BaseInterpolator.java
new file mode 100644
index 0000000..a78fa1e
--- /dev/null
+++ b/android/view/animation/BaseInterpolator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.pm.ActivityInfo.Config;
+
+/**
+ * An abstract class which is extended by default interpolators.
+ */
+abstract public class BaseInterpolator implements Interpolator {
+ private @Config int mChangingConfiguration;
+ /**
+ * @hide
+ */
+ public @Config int getChangingConfiguration() {
+ return mChangingConfiguration;
+ }
+
+ /**
+ * @hide
+ */
+ void setChangingConfiguration(@Config int changingConfiguration) {
+ mChangingConfiguration = changingConfiguration;
+ }
+}
diff --git a/android/view/animation/BounceInterpolator.java b/android/view/animation/BounceInterpolator.java
new file mode 100644
index 0000000..d3f6a3f
--- /dev/null
+++ b/android/view/animation/BounceInterpolator.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.animation;
+
+import android.content.Context;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the change bounces at the end.
+ */
+@HasNativeInterpolator
+public class BounceInterpolator extends BaseInterpolator implements NativeInterpolator {
+ public BounceInterpolator() {
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public BounceInterpolator(Context context, AttributeSet attrs) {
+ }
+
+ private static float bounce(float t) {
+ return t * t * 8.0f;
+ }
+
+ public float getInterpolation(float t) {
+ // _b(t) = t * t * 8
+ // bs(t) = _b(t) for t < 0.3535
+ // bs(t) = _b(t - 0.54719) + 0.7 for t < 0.7408
+ // bs(t) = _b(t - 0.8526) + 0.9 for t < 0.9644
+ // bs(t) = _b(t - 1.0435) + 0.95 for t <= 1.0
+ // b(t) = bs(t * 1.1226)
+ t *= 1.1226f;
+ if (t < 0.3535f) return bounce(t);
+ else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
+ else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
+ else return bounce(t - 1.0435f) + 0.95f;
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createBounceInterpolator();
+ }
+}
\ No newline at end of file
diff --git a/android/view/animation/ClipRectAnimation.java b/android/view/animation/ClipRectAnimation.java
new file mode 100644
index 0000000..21509d3
--- /dev/null
+++ b/android/view/animation/ClipRectAnimation.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+
+/**
+ * An animation that controls the clip of an object. See the
+ * {@link android.view.animation full package} description for details and
+ * sample code.
+ *
+ * @hide
+ */
+public class ClipRectAnimation extends Animation {
+ protected final Rect mFromRect = new Rect();
+ protected final Rect mToRect = new Rect();
+
+ private int mFromLeftType = ABSOLUTE;
+ private int mFromTopType = ABSOLUTE;
+ private int mFromRightType = ABSOLUTE;
+ private int mFromBottomType = ABSOLUTE;
+
+ private int mToLeftType = ABSOLUTE;
+ private int mToTopType = ABSOLUTE;
+ private int mToRightType = ABSOLUTE;
+ private int mToBottomType = ABSOLUTE;
+
+ private float mFromLeftValue;
+ private float mFromTopValue;
+ private float mFromRightValue;
+ private float mFromBottomValue;
+
+ private float mToLeftValue;
+ private float mToTopValue;
+ private float mToRightValue;
+ private float mToBottomValue;
+
+ /**
+ * Constructor used when a ClipRectAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public ClipRectAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ClipRectAnimation);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromLeft));
+ mFromLeftType = d.type;
+ mFromLeftValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromTop));
+ mFromTopType = d.type;
+ mFromTopValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromRight));
+ mFromRightType = d.type;
+ mFromRightValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromBottom));
+ mFromBottomType = d.type;
+ mFromBottomValue = d.value;
+
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toLeft));
+ mToLeftType = d.type;
+ mToLeftValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toTop));
+ mToTopType = d.type;
+ mToTopValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toRight));
+ mToRightType = d.type;
+ mToRightValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toBottom));
+ mToBottomType = d.type;
+ mToBottomValue = d.value;
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building a ClipRectAnimation from code
+ *
+ * @param fromClip the clip rect to animate from
+ * @param toClip the clip rect to animate to
+ */
+ public ClipRectAnimation(Rect fromClip, Rect toClip) {
+ if (fromClip == null || toClip == null) {
+ throw new RuntimeException("Expected non-null animation clip rects");
+ }
+ mFromLeftValue = fromClip.left;
+ mFromTopValue = fromClip.top;
+ mFromRightValue= fromClip.right;
+ mFromBottomValue = fromClip.bottom;
+
+ mToLeftValue = toClip.left;
+ mToTopValue = toClip.top;
+ mToRightValue= toClip.right;
+ mToBottomValue = toClip.bottom;
+ }
+
+ /**
+ * Constructor to use when building a ClipRectAnimation from code
+ */
+ public ClipRectAnimation(int fromL, int fromT, int fromR, int fromB,
+ int toL, int toT, int toR, int toB) {
+ this(new Rect(fromL, fromT, fromR, fromB), new Rect(toL, toT, toR, toB));
+ }
+
+ @Override
+ protected void applyTransformation(float it, Transformation tr) {
+ int l = mFromRect.left + (int) ((mToRect.left - mFromRect.left) * it);
+ int t = mFromRect.top + (int) ((mToRect.top - mFromRect.top) * it);
+ int r = mFromRect.right + (int) ((mToRect.right - mFromRect.right) * it);
+ int b = mFromRect.bottom + (int) ((mToRect.bottom - mFromRect.bottom) * it);
+ tr.setClipRect(l, t, r, b);
+ }
+
+ @Override
+ public boolean willChangeTransformationMatrix() {
+ return false;
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mFromRect.set((int) resolveSize(mFromLeftType, mFromLeftValue, width, parentWidth),
+ (int) resolveSize(mFromTopType, mFromTopValue, height, parentHeight),
+ (int) resolveSize(mFromRightType, mFromRightValue, width, parentWidth),
+ (int) resolveSize(mFromBottomType, mFromBottomValue, height, parentHeight));
+ mToRect.set((int) resolveSize(mToLeftType, mToLeftValue, width, parentWidth),
+ (int) resolveSize(mToTopType, mToTopValue, height, parentHeight),
+ (int) resolveSize(mToRightType, mToRightValue, width, parentWidth),
+ (int) resolveSize(mToBottomType, mToBottomValue, height, parentHeight));
+ }
+}
diff --git a/android/view/animation/CycleInterpolator.java b/android/view/animation/CycleInterpolator.java
new file mode 100644
index 0000000..6b1a80a
--- /dev/null
+++ b/android/view/animation/CycleInterpolator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * Repeats the animation for a specified number of cycles. The
+ * rate of change follows a sinusoidal pattern.
+ *
+ */
+@HasNativeInterpolator
+public class CycleInterpolator extends BaseInterpolator implements NativeInterpolator {
+ public CycleInterpolator(float cycles) {
+ mCycles = cycles;
+ }
+
+ public CycleInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public CycleInterpolator(Resources resources, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.CycleInterpolator, 0, 0);
+ } else {
+ a = resources.obtainAttributes(attrs, R.styleable.CycleInterpolator);
+ }
+
+ mCycles = a.getFloat(R.styleable.CycleInterpolator_cycles, 1.0f);
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ public float getInterpolation(float input) {
+ return (float)(Math.sin(2 * mCycles * Math.PI * input));
+ }
+
+ private float mCycles;
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createCycleInterpolator(mCycles);
+ }
+}
diff --git a/android/view/animation/DecelerateInterpolator.java b/android/view/animation/DecelerateInterpolator.java
new file mode 100644
index 0000000..2d2f770
--- /dev/null
+++ b/android/view/animation/DecelerateInterpolator.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * An interpolator where the rate of change starts out quickly and
+ * and then decelerates.
+ *
+ */
+@HasNativeInterpolator
+public class DecelerateInterpolator extends BaseInterpolator implements NativeInterpolator {
+ public DecelerateInterpolator() {
+ }
+
+ /**
+ * Constructor
+ *
+ * @param factor Degree to which the animation should be eased. Setting factor to 1.0f produces
+ * an upside-down y=x^2 parabola. Increasing factor above 1.0f exaggerates the
+ * ease-out effect (i.e., it starts even faster and ends evens slower).
+ */
+ public DecelerateInterpolator(float factor) {
+ mFactor = factor;
+ }
+
+ public DecelerateInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public DecelerateInterpolator(Resources res, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.DecelerateInterpolator, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.DecelerateInterpolator);
+ }
+
+ mFactor = a.getFloat(R.styleable.DecelerateInterpolator_factor, 1.0f);
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ public float getInterpolation(float input) {
+ float result;
+ if (mFactor == 1.0f) {
+ result = (float)(1.0f - (1.0f - input) * (1.0f - input));
+ } else {
+ result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
+ }
+ return result;
+ }
+
+ private float mFactor = 1.0f;
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createDecelerateInterpolator(mFactor);
+ }
+}
diff --git a/android/view/animation/GridLayoutAnimationController.java b/android/view/animation/GridLayoutAnimationController.java
new file mode 100644
index 0000000..0f189ae
--- /dev/null
+++ b/android/view/animation/GridLayoutAnimationController.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Random;
+
+/**
+ * A layout animation controller is used to animated a grid layout's children.
+ *
+ * While {@link LayoutAnimationController} relies only on the index of the child
+ * in the view group to compute the animation delay, this class uses both the
+ * X and Y coordinates of the child within a grid.
+ *
+ * In addition, the animation direction can be controlled. The default direction
+ * is <code>DIRECTION_LEFT_TO_RIGHT | DIRECTION_TOP_TO_BOTTOM</code>. You can
+ * also set the animation priority to columns or rows. The default priority is
+ * none.
+ *
+ * Information used to compute the animation delay of each child are stored
+ * in an instance of
+ * {@link android.view.animation.GridLayoutAnimationController.AnimationParameters},
+ * itself stored in the {@link android.view.ViewGroup.LayoutParams} of the view.
+ *
+ * @see LayoutAnimationController
+ * @see android.widget.GridView
+ *
+ * @attr ref android.R.styleable#GridLayoutAnimation_columnDelay
+ * @attr ref android.R.styleable#GridLayoutAnimation_rowDelay
+ * @attr ref android.R.styleable#GridLayoutAnimation_direction
+ * @attr ref android.R.styleable#GridLayoutAnimation_directionPriority
+ */
+public class GridLayoutAnimationController extends LayoutAnimationController {
+ /**
+ * Animates the children starting from the left of the grid to the right.
+ */
+ public static final int DIRECTION_LEFT_TO_RIGHT = 0x0;
+
+ /**
+ * Animates the children starting from the right of the grid to the left.
+ */
+ public static final int DIRECTION_RIGHT_TO_LEFT = 0x1;
+
+ /**
+ * Animates the children starting from the top of the grid to the bottom.
+ */
+ public static final int DIRECTION_TOP_TO_BOTTOM = 0x0;
+
+ /**
+ * Animates the children starting from the bottom of the grid to the top.
+ */
+ public static final int DIRECTION_BOTTOM_TO_TOP = 0x2;
+
+ /**
+ * Bitmask used to retrieve the horizontal component of the direction.
+ */
+ public static final int DIRECTION_HORIZONTAL_MASK = 0x1;
+
+ /**
+ * Bitmask used to retrieve the vertical component of the direction.
+ */
+ public static final int DIRECTION_VERTICAL_MASK = 0x2;
+
+ /**
+ * Rows and columns are animated at the same time.
+ */
+ public static final int PRIORITY_NONE = 0;
+
+ /**
+ * Columns are animated first.
+ */
+ public static final int PRIORITY_COLUMN = 1;
+
+ /**
+ * Rows are animated first.
+ */
+ public static final int PRIORITY_ROW = 2;
+
+ private float mColumnDelay;
+ private float mRowDelay;
+
+ private int mDirection;
+ private int mDirectionPriority;
+
+ /**
+ * Creates a new grid layout animation controller from external resources.
+ *
+ * @param context the Context the view group is running in, through which
+ * it can access the resources
+ * @param attrs the attributes of the XML tag that is inflating the
+ * layout animation controller
+ */
+ public GridLayoutAnimationController(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.GridLayoutAnimation);
+
+ Animation.Description d = Animation.Description.parseValue(
+ a.peekValue(com.android.internal.R.styleable.GridLayoutAnimation_columnDelay));
+ mColumnDelay = d.value;
+ d = Animation.Description.parseValue(
+ a.peekValue(com.android.internal.R.styleable.GridLayoutAnimation_rowDelay));
+ mRowDelay = d.value;
+ //noinspection PointlessBitwiseExpression
+ mDirection = a.getInt(com.android.internal.R.styleable.GridLayoutAnimation_direction,
+ DIRECTION_LEFT_TO_RIGHT | DIRECTION_TOP_TO_BOTTOM);
+ mDirectionPriority = a.getInt(com.android.internal.R.styleable.GridLayoutAnimation_directionPriority,
+ PRIORITY_NONE);
+
+ a.recycle();
+ }
+
+ /**
+ * Creates a new layout animation controller with a delay of 50%
+ * for both rows and columns and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ */
+ public GridLayoutAnimationController(Animation animation) {
+ this(animation, 0.5f, 0.5f);
+ }
+
+ /**
+ * Creates a new layout animation controller with the specified delays
+ * and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ * @param columnDelay the delay by which each column animation must be offset
+ * @param rowDelay the delay by which each row animation must be offset
+ */
+ public GridLayoutAnimationController(Animation animation, float columnDelay, float rowDelay) {
+ super(animation);
+ mColumnDelay = columnDelay;
+ mRowDelay = rowDelay;
+ }
+
+ /**
+ * Returns the delay by which the children's animation are offset from one
+ * column to the other. The delay is expressed as a fraction of the
+ * animation duration.
+ *
+ * @return a fraction of the animation duration
+ *
+ * @see #setColumnDelay(float)
+ * @see #getRowDelay()
+ * @see #setRowDelay(float)
+ */
+ public float getColumnDelay() {
+ return mColumnDelay;
+ }
+
+ /**
+ * Sets the delay, as a fraction of the animation duration, by which the
+ * children's animations are offset from one column to the other.
+ *
+ * @param columnDelay a fraction of the animation duration
+ *
+ * @see #getColumnDelay()
+ * @see #getRowDelay()
+ * @see #setRowDelay(float)
+ */
+ public void setColumnDelay(float columnDelay) {
+ mColumnDelay = columnDelay;
+ }
+
+ /**
+ * Returns the delay by which the children's animation are offset from one
+ * row to the other. The delay is expressed as a fraction of the
+ * animation duration.
+ *
+ * @return a fraction of the animation duration
+ *
+ * @see #setRowDelay(float)
+ * @see #getColumnDelay()
+ * @see #setColumnDelay(float)
+ */
+ public float getRowDelay() {
+ return mRowDelay;
+ }
+
+ /**
+ * Sets the delay, as a fraction of the animation duration, by which the
+ * children's animations are offset from one row to the other.
+ *
+ * @param rowDelay a fraction of the animation duration
+ *
+ * @see #getRowDelay()
+ * @see #getColumnDelay()
+ * @see #setColumnDelay(float)
+ */
+ public void setRowDelay(float rowDelay) {
+ mRowDelay = rowDelay;
+ }
+
+ /**
+ * Returns the direction of the animation. {@link #DIRECTION_HORIZONTAL_MASK}
+ * and {@link #DIRECTION_VERTICAL_MASK} can be used to retrieve the
+ * horizontal and vertical components of the direction.
+ *
+ * @return the direction of the animation
+ *
+ * @see #setDirection(int)
+ * @see #DIRECTION_BOTTOM_TO_TOP
+ * @see #DIRECTION_TOP_TO_BOTTOM
+ * @see #DIRECTION_LEFT_TO_RIGHT
+ * @see #DIRECTION_RIGHT_TO_LEFT
+ * @see #DIRECTION_HORIZONTAL_MASK
+ * @see #DIRECTION_VERTICAL_MASK
+ */
+ public int getDirection() {
+ return mDirection;
+ }
+
+ /**
+ * Sets the direction of the animation. The direction is expressed as an
+ * integer containing a horizontal and vertical component. For instance,
+ * <code>DIRECTION_BOTTOM_TO_TOP | DIRECTION_RIGHT_TO_LEFT</code>.
+ *
+ * @param direction the direction of the animation
+ *
+ * @see #getDirection()
+ * @see #DIRECTION_BOTTOM_TO_TOP
+ * @see #DIRECTION_TOP_TO_BOTTOM
+ * @see #DIRECTION_LEFT_TO_RIGHT
+ * @see #DIRECTION_RIGHT_TO_LEFT
+ * @see #DIRECTION_HORIZONTAL_MASK
+ * @see #DIRECTION_VERTICAL_MASK
+ */
+ public void setDirection(int direction) {
+ mDirection = direction;
+ }
+
+ /**
+ * Returns the direction priority for the animation. The priority can
+ * be either {@link #PRIORITY_NONE}, {@link #PRIORITY_COLUMN} or
+ * {@link #PRIORITY_ROW}.
+ *
+ * @return the priority of the animation direction
+ *
+ * @see #setDirectionPriority(int)
+ * @see #PRIORITY_COLUMN
+ * @see #PRIORITY_NONE
+ * @see #PRIORITY_ROW
+ */
+ public int getDirectionPriority() {
+ return mDirectionPriority;
+ }
+
+ /**
+ * Specifies the direction priority of the animation. For instance,
+ * {@link #PRIORITY_COLUMN} will give priority to columns: the animation
+ * will first play on the column, then on the rows.Z
+ *
+ * @param directionPriority the direction priority of the animation
+ *
+ * @see #getDirectionPriority()
+ * @see #PRIORITY_COLUMN
+ * @see #PRIORITY_NONE
+ * @see #PRIORITY_ROW
+ */
+ public void setDirectionPriority(int directionPriority) {
+ mDirectionPriority = directionPriority;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean willOverlap() {
+ return mColumnDelay < 1.0f || mRowDelay < 1.0f;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected long getDelayForView(View view) {
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+ AnimationParameters params = (AnimationParameters) lp.layoutAnimationParameters;
+
+ if (params == null) {
+ return 0;
+ }
+
+ final int column = getTransformedColumnIndex(params);
+ final int row = getTransformedRowIndex(params);
+
+ final int rowsCount = params.rowsCount;
+ final int columnsCount = params.columnsCount;
+
+ final long duration = mAnimation.getDuration();
+ final float columnDelay = mColumnDelay * duration;
+ final float rowDelay = mRowDelay * duration;
+
+ float totalDelay;
+ long viewDelay;
+
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ switch (mDirectionPriority) {
+ case PRIORITY_COLUMN:
+ viewDelay = (long) (row * rowDelay + column * rowsCount * rowDelay);
+ totalDelay = rowsCount * rowDelay + columnsCount * rowsCount * rowDelay;
+ break;
+ case PRIORITY_ROW:
+ viewDelay = (long) (column * columnDelay + row * columnsCount * columnDelay);
+ totalDelay = columnsCount * columnDelay + rowsCount * columnsCount * columnDelay;
+ break;
+ case PRIORITY_NONE:
+ default:
+ viewDelay = (long) (column * columnDelay + row * rowDelay);
+ totalDelay = columnsCount * columnDelay + rowsCount * rowDelay;
+ break;
+ }
+
+ float normalizedDelay = viewDelay / totalDelay;
+ normalizedDelay = mInterpolator.getInterpolation(normalizedDelay);
+
+ return (long) (normalizedDelay * totalDelay);
+ }
+
+ private int getTransformedColumnIndex(AnimationParameters params) {
+ int index;
+ switch (getOrder()) {
+ case ORDER_REVERSE:
+ index = params.columnsCount - 1 - params.column;
+ break;
+ case ORDER_RANDOM:
+ if (mRandomizer == null) {
+ mRandomizer = new Random();
+ }
+ index = (int) (params.columnsCount * mRandomizer.nextFloat());
+ break;
+ case ORDER_NORMAL:
+ default:
+ index = params.column;
+ break;
+ }
+
+ int direction = mDirection & DIRECTION_HORIZONTAL_MASK;
+ if (direction == DIRECTION_RIGHT_TO_LEFT) {
+ index = params.columnsCount - 1 - index;
+ }
+
+ return index;
+ }
+
+ private int getTransformedRowIndex(AnimationParameters params) {
+ int index;
+ switch (getOrder()) {
+ case ORDER_REVERSE:
+ index = params.rowsCount - 1 - params.row;
+ break;
+ case ORDER_RANDOM:
+ if (mRandomizer == null) {
+ mRandomizer = new Random();
+ }
+ index = (int) (params.rowsCount * mRandomizer.nextFloat());
+ break;
+ case ORDER_NORMAL:
+ default:
+ index = params.row;
+ break;
+ }
+
+ int direction = mDirection & DIRECTION_VERTICAL_MASK;
+ if (direction == DIRECTION_BOTTOM_TO_TOP) {
+ index = params.rowsCount - 1 - index;
+ }
+
+ return index;
+ }
+
+ /**
+ * The set of parameters that has to be attached to each view contained in
+ * the view group animated by the grid layout animation controller. These
+ * parameters are used to compute the start time of each individual view's
+ * animation.
+ */
+ public static class AnimationParameters extends
+ LayoutAnimationController.AnimationParameters {
+ /**
+ * The view group's column to which the view belongs.
+ */
+ public int column;
+
+ /**
+ * The view group's row to which the view belongs.
+ */
+ public int row;
+
+ /**
+ * The number of columns in the view's enclosing grid layout.
+ */
+ public int columnsCount;
+
+ /**
+ * The number of rows in the view's enclosing grid layout.
+ */
+ public int rowsCount;
+ }
+}
diff --git a/android/view/animation/Interpolator.java b/android/view/animation/Interpolator.java
new file mode 100644
index 0000000..5d0fe7e
--- /dev/null
+++ b/android/view/animation/Interpolator.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.animation.TimeInterpolator;
+
+/**
+ * An interpolator defines the rate of change of an animation. This allows
+ * the basic animation effects (alpha, scale, translate, rotate) to be
+ * accelerated, decelerated, repeated, etc.
+ */
+public interface Interpolator extends TimeInterpolator {
+ // A new interface, TimeInterpolator, was introduced for the new android.animation
+ // package. This older Interpolator interface extends TimeInterpolator so that users of
+ // the new Animator-based animations can use either the old Interpolator implementations or
+ // new classes that implement TimeInterpolator directly.
+}
diff --git a/android/view/animation/LayoutAnimationController.java b/android/view/animation/LayoutAnimationController.java
new file mode 100644
index 0000000..e2b7519
--- /dev/null
+++ b/android/view/animation/LayoutAnimationController.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.annotation.AnimRes;
+import android.annotation.InterpolatorRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Random;
+
+/**
+ * A layout animation controller is used to animate the children of a layout or a view
+ * group. Each child uses the same animation but for every one of
+ * them, the animation starts at a different time. A layout animation controller
+ * is used by {@link android.view.ViewGroup} to compute the delay by which each
+ * child's animation start must be offset. The delay is computed by using
+ * characteristics of each child, like its index in the view group.
+ *
+ * This standard implementation computes the delay by multiplying a fixed
+ * amount of miliseconds by the index of the child in its parent view group.
+ * Subclasses are supposed to override
+ * {@link #getDelayForView(android.view.View)} to implement a different way
+ * of computing the delay. For instance, a
+ * {@link android.view.animation.GridLayoutAnimationController} will compute the
+ * delay based on the column and row indices of the child in its parent view
+ * group.
+ *
+ * Information used to compute the animation delay of each child are stored
+ * in an instance of
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters},
+ * itself stored in the {@link android.view.ViewGroup.LayoutParams} of the view.
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_delay
+ * @attr ref android.R.styleable#LayoutAnimation_animationOrder
+ * @attr ref android.R.styleable#LayoutAnimation_interpolator
+ * @attr ref android.R.styleable#LayoutAnimation_animation
+ */
+public class LayoutAnimationController {
+ /**
+ * Distributes the animation delays in the order in which view were added
+ * to their view group.
+ */
+ public static final int ORDER_NORMAL = 0;
+
+ /**
+ * Distributes the animation delays in the reverse order in which view were
+ * added to their view group.
+ */
+ public static final int ORDER_REVERSE = 1;
+
+ /**
+ * Randomly distributes the animation delays.
+ */
+ public static final int ORDER_RANDOM = 2;
+
+ /**
+ * The animation applied on each child of the view group on which this
+ * layout animation controller is set.
+ */
+ protected Animation mAnimation;
+
+ /**
+ * The randomizer used when the order is set to random. Subclasses should
+ * use this object to avoid creating their own.
+ */
+ protected Random mRandomizer;
+
+ /**
+ * The interpolator used to interpolate the delays.
+ */
+ protected Interpolator mInterpolator;
+
+ private float mDelay;
+ private int mOrder;
+
+ private long mDuration;
+ private long mMaxDelay;
+
+ /**
+ * Creates a new layout animation controller from external resources.
+ *
+ * @param context the Context the view group is running in, through which
+ * it can access the resources
+ * @param attrs the attributes of the XML tag that is inflating the
+ * layout animation controller
+ */
+ public LayoutAnimationController(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LayoutAnimation);
+
+ Animation.Description d = Animation.Description.parseValue(
+ a.peekValue(com.android.internal.R.styleable.LayoutAnimation_delay));
+ mDelay = d.value;
+
+ mOrder = a.getInt(com.android.internal.R.styleable.LayoutAnimation_animationOrder, ORDER_NORMAL);
+
+ int resource = a.getResourceId(com.android.internal.R.styleable.LayoutAnimation_animation, 0);
+ if (resource > 0) {
+ setAnimation(context, resource);
+ }
+
+ resource = a.getResourceId(com.android.internal.R.styleable.LayoutAnimation_interpolator, 0);
+ if (resource > 0) {
+ setInterpolator(context, resource);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Creates a new layout animation controller with a delay of 50%
+ * and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ */
+ public LayoutAnimationController(Animation animation) {
+ this(animation, 0.5f);
+ }
+
+ /**
+ * Creates a new layout animation controller with the specified delay
+ * and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ * @param delay the delay by which each child's animation must be offset
+ */
+ public LayoutAnimationController(Animation animation, float delay) {
+ mDelay = delay;
+ setAnimation(animation);
+ }
+
+ /**
+ * Returns the order used to compute the delay of each child's animation.
+ *
+ * @return one of {@link #ORDER_NORMAL}, {@link #ORDER_REVERSE} or
+ * {@link #ORDER_RANDOM}
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animationOrder
+ */
+ public int getOrder() {
+ return mOrder;
+ }
+
+ /**
+ * Sets the order used to compute the delay of each child's animation.
+ *
+ * @param order one of {@link #ORDER_NORMAL}, {@link #ORDER_REVERSE} or
+ * {@link #ORDER_RANDOM}
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animationOrder
+ */
+ public void setOrder(int order) {
+ mOrder = order;
+ }
+
+ /**
+ * Sets the animation to be run on each child of the view group on which
+ * this layout animation controller is .
+ *
+ * @param context the context from which the animation must be inflated
+ * @param resourceID the resource identifier of the animation
+ *
+ * @see #setAnimation(Animation)
+ * @see #getAnimation()
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animation
+ */
+ public void setAnimation(Context context, @AnimRes int resourceID) {
+ setAnimation(AnimationUtils.loadAnimation(context, resourceID));
+ }
+
+ /**
+ * Sets the animation to be run on each child of the view group on which
+ * this layout animation controller is .
+ *
+ * @param animation the animation to run on each child of the view group
+
+ * @see #setAnimation(android.content.Context, int)
+ * @see #getAnimation()
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animation
+ */
+ public void setAnimation(Animation animation) {
+ mAnimation = animation;
+ mAnimation.setFillBefore(true);
+ }
+
+ /**
+ * Returns the animation applied to each child of the view group on which
+ * this controller is set.
+ *
+ * @return an {@link android.view.animation.Animation} instance
+ *
+ * @see #setAnimation(android.content.Context, int)
+ * @see #setAnimation(Animation)
+ */
+ public Animation getAnimation() {
+ return mAnimation;
+ }
+
+ /**
+ * Sets the interpolator used to interpolate the delays between the
+ * children.
+ *
+ * @param context the context from which the interpolator must be inflated
+ * @param resourceID the resource identifier of the interpolator
+ *
+ * @see #getInterpolator()
+ * @see #setInterpolator(Interpolator)
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_interpolator
+ */
+ public void setInterpolator(Context context, @InterpolatorRes int resourceID) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resourceID));
+ }
+
+ /**
+ * Sets the interpolator used to interpolate the delays between the
+ * children.
+ *
+ * @param interpolator the interpolator
+ *
+ * @see #getInterpolator()
+ * @see #setInterpolator(Interpolator)
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_interpolator
+ */
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Returns the interpolator used to interpolate the delays between the
+ * children.
+ *
+ * @return an {@link android.view.animation.Interpolator}
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Returns the delay by which the children's animation are offset. The
+ * delay is expressed as a fraction of the animation duration.
+ *
+ * @return a fraction of the animation duration
+ *
+ * @see #setDelay(float)
+ */
+ public float getDelay() {
+ return mDelay;
+ }
+
+ /**
+ * Sets the delay, as a fraction of the animation duration, by which the
+ * children's animations are offset. The general formula is:
+ *
+ * <pre>
+ * child animation delay = child index * delay * animation duration
+ * </pre>
+ *
+ * @param delay a fraction of the animation duration
+ *
+ * @see #getDelay()
+ */
+ public void setDelay(float delay) {
+ mDelay = delay;
+ }
+
+ /**
+ * Indicates whether two children's animations will overlap. Animations
+ * overlap when the delay is lower than 100% (or 1.0).
+ *
+ * @return true if animations will overlap, false otherwise
+ */
+ public boolean willOverlap() {
+ return mDelay < 1.0f;
+ }
+
+ /**
+ * Starts the animation.
+ */
+ public void start() {
+ mDuration = mAnimation.getDuration();
+ mMaxDelay = Long.MIN_VALUE;
+ mAnimation.setStartTime(-1);
+ }
+
+ /**
+ * Returns the animation to be applied to the specified view. The returned
+ * animation is delayed by an offset computed according to the information
+ * provided by
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters}.
+ * This method is called by view groups to obtain the animation to set on
+ * a specific child.
+ *
+ * @param view the view to animate
+ * @return an animation delayed by the number of milliseconds returned by
+ * {@link #getDelayForView(android.view.View)}
+ *
+ * @see #getDelay()
+ * @see #setDelay(float)
+ * @see #getDelayForView(android.view.View)
+ */
+ public final Animation getAnimationForView(View view) {
+ final long delay = getDelayForView(view) + mAnimation.getStartOffset();
+ mMaxDelay = Math.max(mMaxDelay, delay);
+
+ try {
+ final Animation animation = mAnimation.clone();
+ animation.setStartOffset(delay);
+ return animation;
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Indicates whether the layout animation is over or not. A layout animation
+ * is considered done when the animation with the longest delay is done.
+ *
+ * @return true if all of the children's animations are over, false otherwise
+ */
+ public boolean isDone() {
+ return AnimationUtils.currentAnimationTimeMillis() >
+ mAnimation.getStartTime() + mMaxDelay + mDuration;
+ }
+
+ /**
+ * Returns the amount of milliseconds by which the specified view's
+ * animation must be delayed or offset. Subclasses should override this
+ * method to return a suitable value.
+ *
+ * This implementation returns <code>child animation delay</code>
+ * milliseconds where:
+ *
+ * <pre>
+ * child animation delay = child index * delay
+ * </pre>
+ *
+ * The index is retrieved from the
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters}
+ * found in the view's {@link android.view.ViewGroup.LayoutParams}.
+ *
+ * @param view the view for which to obtain the animation's delay
+ * @return a delay in milliseconds
+ *
+ * @see #getAnimationForView(android.view.View)
+ * @see #getDelay()
+ * @see #getTransformedIndex(android.view.animation.LayoutAnimationController.AnimationParameters)
+ * @see android.view.ViewGroup.LayoutParams
+ */
+ protected long getDelayForView(View view) {
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+ AnimationParameters params = lp.layoutAnimationParameters;
+
+ if (params == null) {
+ return 0;
+ }
+
+ final float delay = mDelay * mAnimation.getDuration();
+ final long viewDelay = (long) (getTransformedIndex(params) * delay);
+ final float totalDelay = delay * params.count;
+
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ float normalizedDelay = viewDelay / totalDelay;
+ normalizedDelay = mInterpolator.getInterpolation(normalizedDelay);
+
+ return (long) (normalizedDelay * totalDelay);
+ }
+
+ /**
+ * Transforms the index stored in
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters}
+ * by the order returned by {@link #getOrder()}. Subclasses should override
+ * this method to provide additional support for other types of ordering.
+ * This method should be invoked by
+ * {@link #getDelayForView(android.view.View)} prior to any computation.
+ *
+ * @param params the animation parameters containing the index
+ * @return a transformed index
+ */
+ protected int getTransformedIndex(AnimationParameters params) {
+ switch (getOrder()) {
+ case ORDER_REVERSE:
+ return params.count - 1 - params.index;
+ case ORDER_RANDOM:
+ if (mRandomizer == null) {
+ mRandomizer = new Random();
+ }
+ return (int) (params.count * mRandomizer.nextFloat());
+ case ORDER_NORMAL:
+ default:
+ return params.index;
+ }
+ }
+
+ /**
+ * The set of parameters that has to be attached to each view contained in
+ * the view group animated by the layout animation controller. These
+ * parameters are used to compute the start time of each individual view's
+ * animation.
+ */
+ public static class AnimationParameters {
+ /**
+ * The number of children in the view group containing the view to which
+ * these parameters are attached.
+ */
+ public int count;
+
+ /**
+ * The index of the view to which these parameters are attached in its
+ * containing view group.
+ */
+ public int index;
+ }
+}
diff --git a/android/view/animation/LinearInterpolator.java b/android/view/animation/LinearInterpolator.java
new file mode 100644
index 0000000..f6a820c
--- /dev/null
+++ b/android/view/animation/LinearInterpolator.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the rate of change is constant
+ */
+@HasNativeInterpolator
+public class LinearInterpolator extends BaseInterpolator implements NativeInterpolator {
+
+ public LinearInterpolator() {
+ }
+
+ public LinearInterpolator(Context context, AttributeSet attrs) {
+ }
+
+ public float getInterpolation(float input) {
+ return input;
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createLinearInterpolator();
+ }
+}
diff --git a/android/view/animation/OvershootInterpolator.java b/android/view/animation/OvershootInterpolator.java
new file mode 100644
index 0000000..e6445d7
--- /dev/null
+++ b/android/view/animation/OvershootInterpolator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * An interpolator where the change flings forward and overshoots the last value
+ * then comes back.
+ */
+@HasNativeInterpolator
+public class OvershootInterpolator extends BaseInterpolator implements NativeInterpolator {
+ private final float mTension;
+
+ public OvershootInterpolator() {
+ mTension = 2.0f;
+ }
+
+ /**
+ * @param tension Amount of overshoot. When tension equals 0.0f, there is
+ * no overshoot and the interpolator becomes a simple
+ * deceleration interpolator.
+ */
+ public OvershootInterpolator(float tension) {
+ mTension = tension;
+ }
+
+ public OvershootInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public OvershootInterpolator(Resources res, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.OvershootInterpolator, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.OvershootInterpolator);
+ }
+
+ mTension = a.getFloat(R.styleable.OvershootInterpolator_tension, 2.0f);
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ public float getInterpolation(float t) {
+ // _o(t) = t * t * ((tension + 1) * t + tension)
+ // o(t) = _o(t - 1) + 1
+ t -= 1.0f;
+ return t * t * ((mTension + 1) * t + mTension) + 1.0f;
+ }
+
+ /** @hide */
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createOvershootInterpolator(mTension);
+ }
+}
diff --git a/android/view/animation/PathInterpolator.java b/android/view/animation/PathInterpolator.java
new file mode 100644
index 0000000..99d6b9c
--- /dev/null
+++ b/android/view/animation/PathInterpolator.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Path;
+import android.graphics.animation.HasNativeInterpolator;
+import android.graphics.animation.NativeInterpolator;
+import android.graphics.animation.NativeInterpolatorFactory;
+import android.util.AttributeSet;
+import android.util.PathParser;
+import android.view.InflateException;
+
+import com.android.internal.R;
+
+/**
+ * An interpolator that can traverse a Path that extends from <code>Point</code>
+ * <code>(0, 0)</code> to <code>(1, 1)</code>. The x coordinate along the <code>Path</code>
+ * is the input value and the output is the y coordinate of the line at that point.
+ * This means that the Path must conform to a function <code>y = f(x)</code>.
+ *
+ * <p>The <code>Path</code> must not have gaps in the x direction and must not
+ * loop back on itself such that there can be two points sharing the same x coordinate.
+ * It is alright to have a disjoint line in the vertical direction:</p>
+ * <p><blockquote><pre>
+ * Path path = new Path();
+ * path.lineTo(0.25f, 0.25f);
+ * path.moveTo(0.25f, 0.5f);
+ * path.lineTo(1f, 1f);
+ * </pre></blockquote></p>
+ */
+@HasNativeInterpolator
+public class PathInterpolator extends BaseInterpolator implements NativeInterpolator {
+
+ // This governs how accurate the approximation of the Path is.
+ private static final float PRECISION = 0.002f;
+
+ private float[] mX; // x coordinates in the line
+
+ private float[] mY; // y coordinates in the line
+
+ /**
+ * Create an interpolator for an arbitrary <code>Path</code>. The <code>Path</code>
+ * must begin at <code>(0, 0)</code> and end at <code>(1, 1)</code>.
+ *
+ * @param path The <code>Path</code> to use to make the line representing the interpolator.
+ */
+ public PathInterpolator(Path path) {
+ initPath(path);
+ }
+
+ /**
+ * Create an interpolator for a quadratic Bezier curve. The end points
+ * <code>(0, 0)</code> and <code>(1, 1)</code> are assumed.
+ *
+ * @param controlX The x coordinate of the quadratic Bezier control point.
+ * @param controlY The y coordinate of the quadratic Bezier control point.
+ */
+ public PathInterpolator(float controlX, float controlY) {
+ initQuad(controlX, controlY);
+ }
+
+ /**
+ * Create an interpolator for a cubic Bezier curve. The end points
+ * <code>(0, 0)</code> and <code>(1, 1)</code> are assumed.
+ *
+ * @param controlX1 The x coordinate of the first control point of the cubic Bezier.
+ * @param controlY1 The y coordinate of the first control point of the cubic Bezier.
+ * @param controlX2 The x coordinate of the second control point of the cubic Bezier.
+ * @param controlY2 The y coordinate of the second control point of the cubic Bezier.
+ */
+ public PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2) {
+ initCubic(controlX1, controlY1, controlX2, controlY2);
+ }
+
+ public PathInterpolator(Context context, AttributeSet attrs) {
+ this(context.getResources(), context.getTheme(), attrs);
+ }
+
+ /** @hide */
+ public PathInterpolator(Resources res, Theme theme, AttributeSet attrs) {
+ TypedArray a;
+ if (theme != null) {
+ a = theme.obtainStyledAttributes(attrs, R.styleable.PathInterpolator, 0, 0);
+ } else {
+ a = res.obtainAttributes(attrs, R.styleable.PathInterpolator);
+ }
+ parseInterpolatorFromTypeArray(a);
+ setChangingConfiguration(a.getChangingConfigurations());
+ a.recycle();
+ }
+
+ private void parseInterpolatorFromTypeArray(TypedArray a) {
+ // If there is pathData defined in the xml file, then the controls points
+ // will be all coming from pathData.
+ if (a.hasValue(R.styleable.PathInterpolator_pathData)) {
+ String pathData = a.getString(R.styleable.PathInterpolator_pathData);
+ Path path = PathParser.createPathFromPathData(pathData);
+ if (path == null) {
+ throw new InflateException("The path is null, which is created"
+ + " from " + pathData);
+ }
+ initPath(path);
+ } else {
+ if (!a.hasValue(R.styleable.PathInterpolator_controlX1)) {
+ throw new InflateException("pathInterpolator requires the controlX1 attribute");
+ } else if (!a.hasValue(R.styleable.PathInterpolator_controlY1)) {
+ throw new InflateException("pathInterpolator requires the controlY1 attribute");
+ }
+ float x1 = a.getFloat(R.styleable.PathInterpolator_controlX1, 0);
+ float y1 = a.getFloat(R.styleable.PathInterpolator_controlY1, 0);
+
+ boolean hasX2 = a.hasValue(R.styleable.PathInterpolator_controlX2);
+ boolean hasY2 = a.hasValue(R.styleable.PathInterpolator_controlY2);
+
+ if (hasX2 != hasY2) {
+ throw new InflateException(
+ "pathInterpolator requires both controlX2 and controlY2 for cubic Beziers.");
+ }
+
+ if (!hasX2) {
+ initQuad(x1, y1);
+ } else {
+ float x2 = a.getFloat(R.styleable.PathInterpolator_controlX2, 0);
+ float y2 = a.getFloat(R.styleable.PathInterpolator_controlY2, 0);
+ initCubic(x1, y1, x2, y2);
+ }
+ }
+ }
+
+ private void initQuad(float controlX, float controlY) {
+ Path path = new Path();
+ path.moveTo(0, 0);
+ path.quadTo(controlX, controlY, 1f, 1f);
+ initPath(path);
+ }
+
+ private void initCubic(float x1, float y1, float x2, float y2) {
+ Path path = new Path();
+ path.moveTo(0, 0);
+ path.cubicTo(x1, y1, x2, y2, 1f, 1f);
+ initPath(path);
+ }
+
+ private void initPath(Path path) {
+ float[] pointComponents = path.approximate(PRECISION);
+
+ int numPoints = pointComponents.length / 3;
+ if (pointComponents[1] != 0 || pointComponents[2] != 0
+ || pointComponents[pointComponents.length - 2] != 1
+ || pointComponents[pointComponents.length - 1] != 1) {
+ throw new IllegalArgumentException("The Path must start at (0,0) and end at (1,1)");
+ }
+
+ mX = new float[numPoints];
+ mY = new float[numPoints];
+ float prevX = 0;
+ float prevFraction = 0;
+ int componentIndex = 0;
+ for (int i = 0; i < numPoints; i++) {
+ float fraction = pointComponents[componentIndex++];
+ float x = pointComponents[componentIndex++];
+ float y = pointComponents[componentIndex++];
+ if (fraction == prevFraction && x != prevX) {
+ throw new IllegalArgumentException(
+ "The Path cannot have discontinuity in the X axis.");
+ }
+ if (x < prevX) {
+ throw new IllegalArgumentException("The Path cannot loop back on itself.");
+ }
+ mX[i] = x;
+ mY[i] = y;
+ prevX = x;
+ prevFraction = fraction;
+ }
+ }
+
+ /**
+ * Using the line in the Path in this interpolator that can be described as
+ * <code>y = f(x)</code>, finds the y coordinate of the line given <code>t</code>
+ * as the x coordinate. Values less than 0 will always return 0 and values greater
+ * than 1 will always return 1.
+ *
+ * @param t Treated as the x coordinate along the line.
+ * @return The y coordinate of the Path along the line where x = <code>t</code>.
+ * @see Interpolator#getInterpolation(float)
+ */
+ @Override
+ public float getInterpolation(float t) {
+ if (t <= 0) {
+ return 0;
+ } else if (t >= 1) {
+ return 1;
+ }
+ // Do a binary search for the correct x to interpolate between.
+ int startIndex = 0;
+ int endIndex = mX.length - 1;
+
+ while (endIndex - startIndex > 1) {
+ int midIndex = (startIndex + endIndex) / 2;
+ if (t < mX[midIndex]) {
+ endIndex = midIndex;
+ } else {
+ startIndex = midIndex;
+ }
+ }
+
+ float xRange = mX[endIndex] - mX[startIndex];
+ if (xRange == 0) {
+ return mY[startIndex];
+ }
+
+ float tInRange = t - mX[startIndex];
+ float fraction = tInRange / xRange;
+
+ float startY = mY[startIndex];
+ float endY = mY[endIndex];
+ return startY + (fraction * (endY - startY));
+ }
+
+ /** @hide **/
+ @Override
+ public long createNativeInterpolator() {
+ return NativeInterpolatorFactory.createPathInterpolator(mX, mY);
+ }
+
+}
diff --git a/android/view/animation/RotateAnimation.java b/android/view/animation/RotateAnimation.java
new file mode 100644
index 0000000..3c325d9
--- /dev/null
+++ b/android/view/animation/RotateAnimation.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the rotation of an object. This rotation takes
+ * place in the X-Y plane. You can specify the point to use for the center of
+ * the rotation, where (0,0) is the top left point. If not specified, (0,0) is
+ * the default rotation point.
+ *
+ */
+public class RotateAnimation extends Animation {
+ private float mFromDegrees;
+ private float mToDegrees;
+
+ private int mPivotXType = ABSOLUTE;
+ private int mPivotYType = ABSOLUTE;
+ private float mPivotXValue = 0.0f;
+ private float mPivotYValue = 0.0f;
+
+ private float mPivotX;
+ private float mPivotY;
+
+ /**
+ * Constructor used when a RotateAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public RotateAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.RotateAnimation);
+
+ mFromDegrees = a.getFloat(
+ com.android.internal.R.styleable.RotateAnimation_fromDegrees, 0.0f);
+ mToDegrees = a.getFloat(com.android.internal.R.styleable.RotateAnimation_toDegrees, 0.0f);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.RotateAnimation_pivotX));
+ mPivotXType = d.type;
+ mPivotXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.RotateAnimation_pivotY));
+ mPivotYType = d.type;
+ mPivotYValue = d.value;
+
+ a.recycle();
+
+ initializePivotPoint();
+ }
+
+ /**
+ * Constructor to use when building a RotateAnimation from code.
+ * Default pivotX/pivotY point is (0,0).
+ *
+ * @param fromDegrees Rotation offset to apply at the start of the
+ * animation.
+ *
+ * @param toDegrees Rotation offset to apply at the end of the animation.
+ */
+ public RotateAnimation(float fromDegrees, float toDegrees) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+ mPivotX = 0.0f;
+ mPivotY = 0.0f;
+ }
+
+ /**
+ * Constructor to use when building a RotateAnimation from code
+ *
+ * @param fromDegrees Rotation offset to apply at the start of the
+ * animation.
+ *
+ * @param toDegrees Rotation offset to apply at the end of the animation.
+ *
+ * @param pivotX The X coordinate of the point about which the object is
+ * being rotated, specified as an absolute number where 0 is the left
+ * edge.
+ * @param pivotY The Y coordinate of the point about which the object is
+ * being rotated, specified as an absolute number where 0 is the top
+ * edge.
+ */
+ public RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+
+ mPivotXType = ABSOLUTE;
+ mPivotYType = ABSOLUTE;
+ mPivotXValue = pivotX;
+ mPivotYValue = pivotY;
+ initializePivotPoint();
+ }
+
+ /**
+ * Constructor to use when building a RotateAnimation from code
+ *
+ * @param fromDegrees Rotation offset to apply at the start of the
+ * animation.
+ *
+ * @param toDegrees Rotation offset to apply at the end of the animation.
+ *
+ * @param pivotXType Specifies how pivotXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotXValue The X coordinate of the point about which the object
+ * is being rotated, specified as an absolute number where 0 is the
+ * left edge. This value can either be an absolute number if
+ * pivotXType is ABSOLUTE, or a percentage (where 1.0 is 100%)
+ * otherwise.
+ * @param pivotYType Specifies how pivotYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotYValue The Y coordinate of the point about which the object
+ * is being rotated, specified as an absolute number where 0 is the
+ * top edge. This value can either be an absolute number if
+ * pivotYType is ABSOLUTE, or a percentage (where 1.0 is 100%)
+ * otherwise.
+ */
+ public RotateAnimation(float fromDegrees, float toDegrees, int pivotXType, float pivotXValue,
+ int pivotYType, float pivotYValue) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+
+ mPivotXValue = pivotXValue;
+ mPivotXType = pivotXType;
+ mPivotYValue = pivotYValue;
+ mPivotYType = pivotYType;
+ initializePivotPoint();
+ }
+
+ /**
+ * Called at the end of constructor methods to initialize, if possible, values for
+ * the pivot point. This is only possible for ABSOLUTE pivot values.
+ */
+ private void initializePivotPoint() {
+ if (mPivotXType == ABSOLUTE) {
+ mPivotX = mPivotXValue;
+ }
+ if (mPivotYType == ABSOLUTE) {
+ mPivotY = mPivotYValue;
+ }
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
+ float scale = getScaleFactor();
+
+ if (mPivotX == 0.0f && mPivotY == 0.0f) {
+ t.getMatrix().setRotate(degrees);
+ } else {
+ t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale);
+ }
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth);
+ mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight);
+ }
+}
diff --git a/android/view/animation/ScaleAnimation.java b/android/view/animation/ScaleAnimation.java
new file mode 100644
index 0000000..e9a8436
--- /dev/null
+++ b/android/view/animation/ScaleAnimation.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+
+/**
+ * An animation that controls the scale of an object. You can specify the point
+ * to use for the center of scaling.
+ *
+ */
+public class ScaleAnimation extends Animation {
+ private final Resources mResources;
+
+ private float mFromX;
+ private float mToX;
+ private float mFromY;
+ private float mToY;
+
+ private int mFromXType = TypedValue.TYPE_NULL;
+ private int mToXType = TypedValue.TYPE_NULL;
+ private int mFromYType = TypedValue.TYPE_NULL;
+ private int mToYType = TypedValue.TYPE_NULL;
+
+ private int mFromXData = 0;
+ private int mToXData = 0;
+ private int mFromYData = 0;
+ private int mToYData = 0;
+
+ private int mPivotXType = ABSOLUTE;
+ private int mPivotYType = ABSOLUTE;
+ private float mPivotXValue = 0.0f;
+ private float mPivotYValue = 0.0f;
+
+ private float mPivotX;
+ private float mPivotY;
+
+ /**
+ * Constructor used when a ScaleAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public ScaleAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mResources = context.getResources();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ScaleAnimation);
+
+ TypedValue tv = a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_fromXScale);
+ mFromX = 0.0f;
+ if (tv != null) {
+ if (tv.type == TypedValue.TYPE_FLOAT) {
+ // This is a scaling factor.
+ mFromX = tv.getFloat();
+ } else {
+ mFromXType = tv.type;
+ mFromXData = tv.data;
+ }
+ }
+ tv = a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_toXScale);
+ mToX = 0.0f;
+ if (tv != null) {
+ if (tv.type == TypedValue.TYPE_FLOAT) {
+ // This is a scaling factor.
+ mToX = tv.getFloat();
+ } else {
+ mToXType = tv.type;
+ mToXData = tv.data;
+ }
+ }
+
+ tv = a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_fromYScale);
+ mFromY = 0.0f;
+ if (tv != null) {
+ if (tv.type == TypedValue.TYPE_FLOAT) {
+ // This is a scaling factor.
+ mFromY = tv.getFloat();
+ } else {
+ mFromYType = tv.type;
+ mFromYData = tv.data;
+ }
+ }
+ tv = a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_toYScale);
+ mToY = 0.0f;
+ if (tv != null) {
+ if (tv.type == TypedValue.TYPE_FLOAT) {
+ // This is a scaling factor.
+ mToY = tv.getFloat();
+ } else {
+ mToYType = tv.type;
+ mToYData = tv.data;
+ }
+ }
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_pivotX));
+ mPivotXType = d.type;
+ mPivotXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_pivotY));
+ mPivotYType = d.type;
+ mPivotYValue = d.value;
+
+ a.recycle();
+
+ initializePivotPoint();
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromX Horizontal scaling factor to apply at the start of the
+ * animation
+ * @param toX Horizontal scaling factor to apply at the end of the animation
+ * @param fromY Vertical scaling factor to apply at the start of the
+ * animation
+ * @param toY Vertical scaling factor to apply at the end of the animation
+ */
+ public ScaleAnimation(float fromX, float toX, float fromY, float toY) {
+ mResources = null;
+ mFromX = fromX;
+ mToX = toX;
+ mFromY = fromY;
+ mToY = toY;
+ mPivotX = 0;
+ mPivotY = 0;
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromX Horizontal scaling factor to apply at the start of the
+ * animation
+ * @param toX Horizontal scaling factor to apply at the end of the animation
+ * @param fromY Vertical scaling factor to apply at the start of the
+ * animation
+ * @param toY Vertical scaling factor to apply at the end of the animation
+ * @param pivotX The X coordinate of the point about which the object is
+ * being scaled, specified as an absolute number where 0 is the left
+ * edge. (This point remains fixed while the object changes size.)
+ * @param pivotY The Y coordinate of the point about which the object is
+ * being scaled, specified as an absolute number where 0 is the top
+ * edge. (This point remains fixed while the object changes size.)
+ */
+ public ScaleAnimation(float fromX, float toX, float fromY, float toY,
+ float pivotX, float pivotY) {
+ mResources = null;
+ mFromX = fromX;
+ mToX = toX;
+ mFromY = fromY;
+ mToY = toY;
+
+ mPivotXType = ABSOLUTE;
+ mPivotYType = ABSOLUTE;
+ mPivotXValue = pivotX;
+ mPivotYValue = pivotY;
+ initializePivotPoint();
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromX Horizontal scaling factor to apply at the start of the
+ * animation
+ * @param toX Horizontal scaling factor to apply at the end of the animation
+ * @param fromY Vertical scaling factor to apply at the start of the
+ * animation
+ * @param toY Vertical scaling factor to apply at the end of the animation
+ * @param pivotXType Specifies how pivotXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotXValue The X coordinate of the point about which the object
+ * is being scaled, specified as an absolute number where 0 is the
+ * left edge. (This point remains fixed while the object changes
+ * size.) This value can either be an absolute number if pivotXType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param pivotYType Specifies how pivotYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotYValue The Y coordinate of the point about which the object
+ * is being scaled, specified as an absolute number where 0 is the
+ * top edge. (This point remains fixed while the object changes
+ * size.) This value can either be an absolute number if pivotYType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ */
+ public ScaleAnimation(float fromX, float toX, float fromY, float toY,
+ int pivotXType, float pivotXValue, int pivotYType, float pivotYValue) {
+ mResources = null;
+ mFromX = fromX;
+ mToX = toX;
+ mFromY = fromY;
+ mToY = toY;
+
+ mPivotXValue = pivotXValue;
+ mPivotXType = pivotXType;
+ mPivotYValue = pivotYValue;
+ mPivotYType = pivotYType;
+ initializePivotPoint();
+ }
+
+ /**
+ * Called at the end of constructor methods to initialize, if possible, values for
+ * the pivot point. This is only possible for ABSOLUTE pivot values.
+ */
+ private void initializePivotPoint() {
+ if (mPivotXType == ABSOLUTE) {
+ mPivotX = mPivotXValue;
+ }
+ if (mPivotYType == ABSOLUTE) {
+ mPivotY = mPivotYValue;
+ }
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float sx = 1.0f;
+ float sy = 1.0f;
+ float scale = getScaleFactor();
+
+ if (mFromX != 1.0f || mToX != 1.0f) {
+ sx = mFromX + ((mToX - mFromX) * interpolatedTime);
+ }
+ if (mFromY != 1.0f || mToY != 1.0f) {
+ sy = mFromY + ((mToY - mFromY) * interpolatedTime);
+ }
+
+ if (mPivotX == 0 && mPivotY == 0) {
+ t.getMatrix().setScale(sx, sy);
+ } else {
+ t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
+ }
+ }
+
+ float resolveScale(float scale, int type, int data, int size, int psize) {
+ float targetSize;
+ if (type == TypedValue.TYPE_FRACTION) {
+ targetSize = TypedValue.complexToFraction(data, size, psize);
+ } else if (type == TypedValue.TYPE_DIMENSION) {
+ targetSize = TypedValue.complexToDimension(data, mResources.getDisplayMetrics());
+ } else {
+ return scale;
+ }
+
+ if (size == 0) {
+ return 1;
+ }
+
+ return targetSize/(float)size;
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+
+ mFromX = resolveScale(mFromX, mFromXType, mFromXData, width, parentWidth);
+ mToX = resolveScale(mToX, mToXType, mToXData, width, parentWidth);
+ mFromY = resolveScale(mFromY, mFromYType, mFromYData, height, parentHeight);
+ mToY = resolveScale(mToY, mToYType, mToYData, height, parentHeight);
+
+ mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth);
+ mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight);
+ }
+}
diff --git a/android/view/animation/Transformation.java b/android/view/animation/Transformation.java
new file mode 100644
index 0000000..b35a66e
--- /dev/null
+++ b/android/view/animation/Transformation.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.annotation.FloatRange;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+
+import java.io.PrintWriter;
+
+/**
+ * Defines the transformation to be applied at
+ * one point in time of an Animation.
+ *
+ */
+public class Transformation {
+ /**
+ * Indicates a transformation that has no effect (alpha = 1 and identity matrix.)
+ */
+ public static final int TYPE_IDENTITY = 0x0;
+ /**
+ * Indicates a transformation that applies an alpha only (uses an identity matrix.)
+ */
+ public static final int TYPE_ALPHA = 0x1;
+ /**
+ * Indicates a transformation that applies a matrix only (alpha = 1.)
+ */
+ public static final int TYPE_MATRIX = 0x2;
+ /**
+ * Indicates a transformation that applies an alpha and a matrix.
+ */
+ public static final int TYPE_BOTH = TYPE_ALPHA | TYPE_MATRIX;
+
+ protected Matrix mMatrix;
+ protected float mAlpha;
+ protected int mTransformationType;
+
+ private boolean mHasClipRect;
+ private Rect mClipRect = new Rect();
+
+ /**
+ * Creates a new transformation with alpha = 1 and the identity matrix.
+ */
+ public Transformation() {
+ clear();
+ }
+
+ /**
+ * Reset the transformation to a state that leaves the object
+ * being animated in an unmodified state. The transformation type is
+ * {@link #TYPE_BOTH} by default.
+ */
+ public void clear() {
+ if (mMatrix == null) {
+ mMatrix = new Matrix();
+ } else {
+ mMatrix.reset();
+ }
+ mClipRect.setEmpty();
+ mHasClipRect = false;
+ mAlpha = 1.0f;
+ mTransformationType = TYPE_BOTH;
+ }
+
+ /**
+ * Indicates the nature of this transformation.
+ *
+ * @return {@link #TYPE_ALPHA}, {@link #TYPE_MATRIX},
+ * {@link #TYPE_BOTH} or {@link #TYPE_IDENTITY}.
+ */
+ public int getTransformationType() {
+ return mTransformationType;
+ }
+
+ /**
+ * Sets the transformation type.
+ *
+ * @param transformationType One of {@link #TYPE_ALPHA},
+ * {@link #TYPE_MATRIX}, {@link #TYPE_BOTH} or
+ * {@link #TYPE_IDENTITY}.
+ */
+ public void setTransformationType(int transformationType) {
+ mTransformationType = transformationType;
+ }
+
+ /**
+ * Clones the specified transformation.
+ *
+ * @param t The transformation to clone.
+ */
+ public void set(Transformation t) {
+ mAlpha = t.getAlpha();
+ mMatrix.set(t.getMatrix());
+ if (t.mHasClipRect) {
+ setClipRect(t.getClipRect());
+ } else {
+ mHasClipRect = false;
+ mClipRect.setEmpty();
+ }
+ mTransformationType = t.getTransformationType();
+ }
+
+ /**
+ * Apply this Transformation to an existing Transformation, e.g. apply
+ * a scale effect to something that has already been rotated.
+ * @param t
+ */
+ public void compose(Transformation t) {
+ mAlpha *= t.getAlpha();
+ mMatrix.preConcat(t.getMatrix());
+ if (t.mHasClipRect) {
+ Rect bounds = t.getClipRect();
+ if (mHasClipRect) {
+ setClipRect(mClipRect.left + bounds.left, mClipRect.top + bounds.top,
+ mClipRect.right + bounds.right, mClipRect.bottom + bounds.bottom);
+ } else {
+ setClipRect(bounds);
+ }
+ }
+ }
+
+ /**
+ * Like {@link #compose(Transformation)} but does this.postConcat(t) of
+ * the transformation matrix.
+ * @hide
+ */
+ public void postCompose(Transformation t) {
+ mAlpha *= t.getAlpha();
+ mMatrix.postConcat(t.getMatrix());
+ if (t.mHasClipRect) {
+ Rect bounds = t.getClipRect();
+ if (mHasClipRect) {
+ setClipRect(mClipRect.left + bounds.left, mClipRect.top + bounds.top,
+ mClipRect.right + bounds.right, mClipRect.bottom + bounds.bottom);
+ } else {
+ setClipRect(bounds);
+ }
+ }
+ }
+
+ /**
+ * @return The 3x3 Matrix representing the trnasformation to apply to the
+ * coordinates of the object being animated
+ */
+ public Matrix getMatrix() {
+ return mMatrix;
+ }
+
+ /**
+ * Sets the degree of transparency
+ * @param alpha 1.0 means fully opaqe and 0.0 means fully transparent
+ */
+ public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * Sets the current Transform's clip rect
+ * @hide
+ */
+ public void setClipRect(Rect r) {
+ setClipRect(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Sets the current Transform's clip rect
+ * @hide
+ */
+ public void setClipRect(int l, int t, int r, int b) {
+ mClipRect.set(l, t, r, b);
+ mHasClipRect = true;
+ }
+
+ /**
+ * Returns the current Transform's clip rect
+ * @hide
+ */
+ public Rect getClipRect() {
+ return mClipRect;
+ }
+
+ /**
+ * Returns whether the current Transform's clip rect is set
+ * @hide
+ */
+ public boolean hasClipRect() {
+ return mHasClipRect;
+ }
+
+ /**
+ * @return The degree of transparency
+ */
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ sb.append("Transformation");
+ toShortString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * Return a string representation of the transformation in a compact form.
+ */
+ public String toShortString() {
+ StringBuilder sb = new StringBuilder(64);
+ toShortString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * @hide
+ */
+ public void toShortString(StringBuilder sb) {
+ sb.append("{alpha="); sb.append(mAlpha);
+ sb.append(" matrix="); sb.append(mMatrix.toShortString());
+ sb.append('}');
+ }
+
+ /**
+ * Print short string, to optimize dumping.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void printShortString(PrintWriter pw) {
+ pw.print("{alpha="); pw.print(mAlpha);
+ pw.print(" matrix=");
+ mMatrix.dump(pw);
+ pw.print('}');
+ }
+}
diff --git a/android/view/animation/TranslateAnimation.java b/android/view/animation/TranslateAnimation.java
new file mode 100644
index 0000000..ec55a02
--- /dev/null
+++ b/android/view/animation/TranslateAnimation.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the position of an object. See the
+ * {@link android.view.animation full package} description for details and
+ * sample code.
+ *
+ */
+public class TranslateAnimation extends Animation {
+ private int mFromXType = ABSOLUTE;
+ private int mToXType = ABSOLUTE;
+
+ private int mFromYType = ABSOLUTE;
+ private int mToYType = ABSOLUTE;
+
+ /** @hide */
+ @UnsupportedAppUsage
+ protected float mFromXValue = 0.0f;
+ /** @hide */
+ @UnsupportedAppUsage
+ protected float mToXValue = 0.0f;
+
+ /** @hide */
+ @UnsupportedAppUsage
+ protected float mFromYValue = 0.0f;
+ /** @hide */
+ @UnsupportedAppUsage
+ protected float mToYValue = 0.0f;
+
+ /** @hide */
+ protected float mFromXDelta;
+ /** @hide */
+ protected float mToXDelta;
+ /** @hide */
+ protected float mFromYDelta;
+ /** @hide */
+ protected float mToYDelta;
+
+ /**
+ * Constructor used when a TranslateAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public TranslateAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.TranslateAnimation);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_fromXDelta));
+ mFromXType = d.type;
+ mFromXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_toXDelta));
+ mToXType = d.type;
+ mToXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_fromYDelta));
+ mFromYType = d.type;
+ mFromYValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_toYDelta));
+ mToYType = d.type;
+ mToYValue = d.value;
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building a TranslateAnimation from code
+ *
+ * @param fromXDelta Change in X coordinate to apply at the start of the
+ * animation
+ * @param toXDelta Change in X coordinate to apply at the end of the
+ * animation
+ * @param fromYDelta Change in Y coordinate to apply at the start of the
+ * animation
+ * @param toYDelta Change in Y coordinate to apply at the end of the
+ * animation
+ */
+ public TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) {
+ mFromXValue = fromXDelta;
+ mToXValue = toXDelta;
+ mFromYValue = fromYDelta;
+ mToYValue = toYDelta;
+
+ mFromXType = ABSOLUTE;
+ mToXType = ABSOLUTE;
+ mFromYType = ABSOLUTE;
+ mToYType = ABSOLUTE;
+ }
+
+ /**
+ * Constructor to use when building a TranslateAnimation from code
+ *
+ * @param fromXType Specifies how fromXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param fromXValue Change in X coordinate to apply at the start of the
+ * animation. This value can either be an absolute number if fromXType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param toXType Specifies how toXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param toXValue Change in X coordinate to apply at the end of the
+ * animation. This value can either be an absolute number if toXType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param fromYType Specifies how fromYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param fromYValue Change in Y coordinate to apply at the start of the
+ * animation. This value can either be an absolute number if fromYType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param toYType Specifies how toYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param toYValue Change in Y coordinate to apply at the end of the
+ * animation. This value can either be an absolute number if toYType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ */
+ public TranslateAnimation(int fromXType, float fromXValue, int toXType, float toXValue,
+ int fromYType, float fromYValue, int toYType, float toYValue) {
+
+ mFromXValue = fromXValue;
+ mToXValue = toXValue;
+ mFromYValue = fromYValue;
+ mToYValue = toYValue;
+
+ mFromXType = fromXType;
+ mToXType = toXType;
+ mFromYType = fromYType;
+ mToYType = toYType;
+ }
+
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float dx = mFromXDelta;
+ float dy = mFromYDelta;
+ if (mFromXDelta != mToXDelta) {
+ dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
+ }
+ if (mFromYDelta != mToYDelta) {
+ dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
+ }
+ t.getMatrix().setTranslate(dx, dy);
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth);
+ mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth);
+ mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight);
+ mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight);
+ }
+}
diff --git a/android/view/animation/TranslateXAnimation.java b/android/view/animation/TranslateXAnimation.java
new file mode 100644
index 0000000..d75323f
--- /dev/null
+++ b/android/view/animation/TranslateXAnimation.java
@@ -0,0 +1,55 @@
+/*
+ * 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.view.animation;
+
+import android.graphics.Matrix;
+
+/**
+ * Special case of TranslateAnimation that translates only horizontally, picking up the
+ * vertical values from whatever is set on the Transformation already. When used in
+ * conjunction with a TranslateYAnimation, allows independent animation of x and y
+ * position.
+ * @hide
+ */
+public class TranslateXAnimation extends TranslateAnimation {
+ float[] mTmpValues = new float[9];
+
+ /**
+ * Constructor. Passes in 0 for the y parameters of TranslateAnimation
+ */
+ public TranslateXAnimation(float fromXDelta, float toXDelta) {
+ super(fromXDelta, toXDelta, 0, 0);
+ }
+
+ /**
+ * Constructor. Passes in 0 for the y parameters of TranslateAnimation
+ */
+ public TranslateXAnimation(int fromXType, float fromXValue, int toXType, float toXValue) {
+ super(fromXType, fromXValue, toXType, toXValue, ABSOLUTE, 0, ABSOLUTE, 0);
+ }
+
+ /**
+ * Calculates and sets x translation values on given transformation.
+ */
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ Matrix m = t.getMatrix();
+ m.getValues(mTmpValues);
+ float dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
+ t.getMatrix().setTranslate(dx, mTmpValues[Matrix.MTRANS_Y]);
+ }
+}
diff --git a/android/view/animation/TranslateYAnimation.java b/android/view/animation/TranslateYAnimation.java
new file mode 100644
index 0000000..1a1dfbf
--- /dev/null
+++ b/android/view/animation/TranslateYAnimation.java
@@ -0,0 +1,57 @@
+/*
+ * 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.view.animation;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Matrix;
+
+/**
+ * Special case of TranslateAnimation that translates only vertically, picking up the
+ * horizontal values from whatever is set on the Transformation already. When used in
+ * conjunction with a TranslateXAnimation, allows independent animation of x and y
+ * position.
+ * @hide
+ */
+public class TranslateYAnimation extends TranslateAnimation {
+ float[] mTmpValues = new float[9];
+
+ /**
+ * Constructor. Passes in 0 for the x parameters of TranslateAnimation
+ */
+ public TranslateYAnimation(float fromYDelta, float toYDelta) {
+ super(0, 0, fromYDelta, toYDelta);
+ }
+
+ /**
+ * Constructor. Passes in 0 for the x parameters of TranslateAnimation
+ */
+ @UnsupportedAppUsage
+ public TranslateYAnimation(int fromYType, float fromYValue, int toYType, float toYValue) {
+ super(ABSOLUTE, 0, ABSOLUTE, 0, fromYType, fromYValue, toYType, toYValue);
+ }
+
+ /**
+ * Calculates and sets y translation values on given transformation.
+ */
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ Matrix m = t.getMatrix();
+ m.getValues(mTmpValues);
+ float dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
+ t.getMatrix().setTranslate(mTmpValues[Matrix.MTRANS_X], dy);
+ }
+}
diff --git a/android/view/autofill/AbstractAutofillPerfTestCase.java b/android/view/autofill/AbstractAutofillPerfTestCase.java
new file mode 100644
index 0000000..54e1860
--- /dev/null
+++ b/android/view/autofill/AbstractAutofillPerfTestCase.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Looper;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.PerfTestActivity;
+import android.perftests.utils.SettingsStateKeeperRule;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+
+/**
+ * Base class for all autofill tests.
+ */
+public abstract class AbstractAutofillPerfTestCase {
+
+ private static final String TAG = "AbstractAutofillPerfTestCase";
+
+ @ClassRule
+ public static final SettingsStateKeeperRule mServiceSettingsKeeper =
+ new SettingsStateKeeperRule(InstrumentationRegistry.getTargetContext(),
+ Settings.Secure.AUTOFILL_SERVICE);
+
+ protected final AutofillTestWatcher mTestWatcher = MyAutofillService.getTestWatcher();
+ protected ActivityTestRule<PerfTestActivity> mActivityRule =
+ new ActivityTestRule<>(PerfTestActivity.class);
+ protected PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Rule
+ public final RuleChain mAllRules = RuleChain
+ .outerRule(mTestWatcher)
+ .around(mPerfStatusReporter)
+ .around(mActivityRule);
+
+ private final int mLayoutId;
+
+ protected AbstractAutofillPerfTestCase(int layoutId) {
+ mLayoutId = layoutId;
+ }
+
+ @BeforeClass
+ public static void disableDefaultAugmentedService() {
+ Log.v(TAG, "@BeforeClass: disableDefaultAugmentedService()");
+ setDefaultAugmentedAutofillServiceEnabled(false);
+ }
+
+ @AfterClass
+ public static void enableDefaultAugmentedService() {
+ Log.v(TAG, "@AfterClass: enableDefaultAugmentedService()");
+ setDefaultAugmentedAutofillServiceEnabled(true);
+ }
+
+ /**
+ * Enables / disables the default augmented autofill service.
+ */
+ private static void setDefaultAugmentedAutofillServiceEnabled(boolean enabled) {
+ Log.d(TAG, "setDefaultAugmentedAutofillServiceEnabled(): " + enabled);
+ runShellCommand("cmd autofill set default-augmented-service-enabled 0 %s",
+ Boolean.toString(enabled));
+ }
+
+ /**
+ * Prepares the activity so that by the time the test is run it has reference to its fields.
+ */
+ @Before
+ public void prepareActivity() throws Throwable {
+ mActivityRule.runOnUiThread(() -> {
+ assertTrue("We should be running on the main thread",
+ Looper.getMainLooper().getThread() == Thread.currentThread());
+ assertTrue("We should be running on the main thread",
+ Looper.myLooper() == Looper.getMainLooper());
+ PerfTestActivity activity = mActivityRule.getActivity();
+ activity.setContentView(mLayoutId);
+ onCreate(activity);
+ });
+ }
+
+ /**
+ * Initializes the {@link PerfTestActivity} after it was launched.
+ */
+ protected abstract void onCreate(PerfTestActivity activity);
+}
diff --git a/android/view/autofill/AutofillId.java b/android/view/autofill/AutofillId.java
new file mode 100644
index 0000000..ae145de
--- /dev/null
+++ b/android/view/autofill/AutofillId.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+
+/**
+ * A unique identifier for an autofill node inside an {@link android.app.Activity}.
+ */
+public final class AutofillId implements Parcelable {
+
+ /** @hide */
+ public static final int NO_SESSION = 0;
+
+ private static final int FLAG_IS_VIRTUAL_INT = 0x1;
+ private static final int FLAG_IS_VIRTUAL_LONG = 0x2;
+ private static final int FLAG_HAS_SESSION = 0x4;
+
+ private final int mViewId;
+ private int mFlags;
+ private final int mVirtualIntId;
+ private final long mVirtualLongId;
+ private int mSessionId;
+
+ /** @hide */
+ @TestApi
+ public AutofillId(int id) {
+ this(/* flags= */ 0, id, View.NO_ID, NO_SESSION);
+ }
+
+ /** @hide */
+ @TestApi
+ public AutofillId(@NonNull AutofillId hostId, int virtualChildId) {
+ this(FLAG_IS_VIRTUAL_INT, hostId.mViewId, virtualChildId, NO_SESSION);
+ }
+
+ /** @hide */
+ @TestApi
+ public AutofillId(int hostId, int virtualChildId) {
+ this(FLAG_IS_VIRTUAL_INT, hostId, virtualChildId, NO_SESSION);
+ }
+
+ /** @hide */
+ @TestApi
+ public AutofillId(@NonNull AutofillId hostId, long virtualChildId, int sessionId) {
+ this(FLAG_IS_VIRTUAL_LONG | FLAG_HAS_SESSION, hostId.mViewId, virtualChildId, sessionId);
+ }
+
+ private AutofillId(int flags, int parentId, long virtualChildId, int sessionId) {
+ mFlags = flags;
+ mViewId = parentId;
+ mVirtualIntId = ((flags & FLAG_IS_VIRTUAL_INT) != 0) ? (int) virtualChildId : View.NO_ID;
+ mVirtualLongId = ((flags & FLAG_IS_VIRTUAL_LONG) != 0) ? virtualChildId : View.NO_ID;
+ mSessionId = sessionId;
+ }
+
+ /** @hide */
+ @NonNull
+ @TestApi
+ public static AutofillId withoutSession(@NonNull AutofillId id) {
+ final int flags = id.mFlags & ~FLAG_HAS_SESSION;
+ final long virtualChildId =
+ ((id.mFlags & FLAG_IS_VIRTUAL_LONG) != 0) ? id.mVirtualLongId
+ : id.mVirtualIntId;
+ return new AutofillId(flags, id.mViewId, virtualChildId, NO_SESSION);
+ }
+
+ /** @hide */
+ public int getViewId() {
+ return mViewId;
+ }
+
+ /**
+ * Gets the virtual child id.
+ *
+ * <p>Should only be used on subsystems where such id is represented by an {@code int}
+ * (Assist and Autofill).
+ *
+ * @hide
+ */
+ public int getVirtualChildIntId() {
+ return mVirtualIntId;
+ }
+
+ /**
+ * Gets the virtual child id.
+ *
+ * <p>Should only be used on subsystems where such id is represented by a {@code long}
+ * (ContentCapture).
+ *
+ * @hide
+ */
+ public long getVirtualChildLongId() {
+ return mVirtualLongId;
+ }
+
+ /**
+ * Checks whether this node represents a virtual child, whose id is represented by an
+ * {@code int}.
+ *
+ * <p>Should only be used on subsystems where such id is represented by an {@code int}
+ * (Assist and Autofill).
+ *
+ * @hide
+ */
+ public boolean isVirtualInt() {
+ return (mFlags & FLAG_IS_VIRTUAL_INT) != 0;
+ }
+
+ /**
+ * Checks whether this node represents a virtual child, whose id is represented by an
+ * {@code long}.
+ *
+ * <p>Should only be used on subsystems where such id is represented by a {@code long}
+ * (ContentCapture).
+ *
+ * @hide
+ */
+ public boolean isVirtualLong() {
+ return (mFlags & FLAG_IS_VIRTUAL_LONG) != 0;
+ }
+
+ /**
+ * Checks whether this node represents a non-virtual child.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean isNonVirtual() {
+ return !isVirtualInt() && !isVirtualLong();
+ }
+
+ /** @hide */
+ public boolean hasSession() {
+ return (mFlags & FLAG_HAS_SESSION) != 0;
+ }
+
+ /** @hide */
+ public int getSessionId() {
+ return mSessionId;
+ }
+
+ /** @hide */
+ public void setSessionId(int sessionId) {
+ mFlags |= FLAG_HAS_SESSION;
+ mSessionId = sessionId;
+ }
+
+ /** @hide */
+ public void resetSessionId() {
+ mFlags &= ~FLAG_HAS_SESSION;
+ mSessionId = NO_SESSION;
+ }
+
+ /////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mViewId;
+ result = prime * result + mVirtualIntId;
+ result = prime * result + (int) (mVirtualLongId ^ (mVirtualLongId >>> 32));
+ result = prime * result + mSessionId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final AutofillId other = (AutofillId) obj;
+ if (mViewId != other.mViewId) return false;
+ if (mVirtualIntId != other.mVirtualIntId) return false;
+ if (mVirtualLongId != other.mVirtualLongId) return false;
+ if (mSessionId != other.mSessionId) return false;
+ return true;
+ }
+
+ /** @hide */
+ @TestApi
+ public boolean equalsIgnoreSession(@Nullable AutofillId other) {
+ if (this == other) return true;
+ if (other == null) return false;
+ if (mViewId != other.mViewId) return false;
+ if (mVirtualIntId != other.mVirtualIntId) return false;
+ if (mVirtualLongId != other.mVirtualLongId) return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder().append(mViewId);
+ if (isVirtualInt()) {
+ builder.append(':').append(mVirtualIntId);
+ } else if (isVirtualLong()) {
+ builder.append(':').append(mVirtualLongId);
+ }
+
+ if (hasSession()) {
+ builder.append('@').append(mSessionId);
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mViewId);
+ parcel.writeInt(mFlags);
+ if (hasSession()) {
+ parcel.writeInt(mSessionId);
+ }
+ if (isVirtualInt()) {
+ parcel.writeInt(mVirtualIntId);
+ } else if (isVirtualLong()) {
+ parcel.writeLong(mVirtualLongId);
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AutofillId> CREATOR =
+ new Parcelable.Creator<AutofillId>() {
+ @Override
+ public AutofillId createFromParcel(Parcel source) {
+ final int viewId = source.readInt();
+ final int flags = source.readInt();
+ final int sessionId = (flags & FLAG_HAS_SESSION) != 0 ? source.readInt() : NO_SESSION;
+ if ((flags & FLAG_IS_VIRTUAL_INT) != 0) {
+ return new AutofillId(flags, viewId, source.readInt(), sessionId);
+ }
+ if ((flags & FLAG_IS_VIRTUAL_LONG) != 0) {
+ return new AutofillId(flags, viewId, source.readLong(), sessionId);
+ }
+ return new AutofillId(flags, viewId, View.NO_ID, sessionId);
+ }
+
+ @Override
+ public AutofillId[] newArray(int size) {
+ return new AutofillId[size];
+ }
+ };
+}
diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java
new file mode 100644
index 0000000..d065147
--- /dev/null
+++ b/android/view/autofill/AutofillManager.java
@@ -0,0 +1,3755 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
+import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE;
+import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED;
+import static android.view.ContentInfo.SOURCE_AUTOFILL;
+import static android.view.autofill.Helper.sDebug;
+import static android.view.autofill.Helper.sVerbose;
+import static android.view.autofill.Helper.toList;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.ViewNodeBuilder;
+import android.app.assist.AssistStructure.ViewNodeParcelable;
+import android.content.AutofillOptions;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Rect;
+import android.metrics.LogMaker;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.service.autofill.AutofillService;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Choreographer;
+import android.view.ContentInfo;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.SyncResultReceiver;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import sun.misc.Cleaner;
+
+//TODO: use java.lang.ref.Cleaner once Android supports Java 9
+
+/**
+ * <p>The {@link AutofillManager} class provides ways for apps and custom views to
+ * integrate with the Autofill Framework lifecycle.
+ *
+ * <p>To learn about using Autofill in your app, read
+ * the <a href="/guide/topics/text/autofill">Autofill Framework</a> guides.
+ *
+ * <h3 id="autofill-lifecycle">Autofill lifecycle</h3>
+ *
+ * <p>The autofill lifecycle starts with the creation of an autofill context associated with an
+ * activity context. The autofill context is created when one of the following methods is called for
+ * the first time in an activity context, and the current user has an enabled autofill service:
+ *
+ * <ul>
+ * <li>{@link #notifyViewEntered(View)}
+ * <li>{@link #notifyViewEntered(View, int, Rect)}
+ * <li>{@link #requestAutofill(View)}
+ * </ul>
+ *
+ * <p>Typically, the context is automatically created when the first view of the activity is
+ * focused because {@code View.onFocusChanged()} indirectly calls
+ * {@link #notifyViewEntered(View)}. App developers can call {@link #requestAutofill(View)} to
+ * explicitly create it (for example, a custom view developer could offer a contextual menu action
+ * in a text-field view to let users manually request autofill).
+ *
+ * <p>After the context is created, the Android System creates a {@link android.view.ViewStructure}
+ * that represents the view hierarchy by calling
+ * {@link View#dispatchProvideAutofillStructure(android.view.ViewStructure, int)} in the root views
+ * of all application windows. By default, {@code dispatchProvideAutofillStructure()} results in
+ * subsequent calls to {@link View#onProvideAutofillStructure(android.view.ViewStructure, int)} and
+ * {@link View#onProvideAutofillVirtualStructure(android.view.ViewStructure, int)} for each view in
+ * the hierarchy.
+ *
+ * <p>The resulting {@link android.view.ViewStructure} is then passed to the autofill service, which
+ * parses it looking for views that can be autofilled. If the service finds such views, it returns
+ * a data structure to the Android System containing the following optional info:
+ *
+ * <ul>
+ * <li>Datasets used to autofill subsets of views in the activity.
+ * <li>Id of views that the service can save their values for future autofilling.
+ * </ul>
+ *
+ * <p>When the service returns datasets, the Android System displays an autofill dataset picker
+ * UI associated with the view, when the view is focused on and is part of a dataset.
+ * The application can be notified when the UI is shown by registering an
+ * {@link AutofillCallback} through {@link #registerCallback(AutofillCallback)}. When the user
+ * selects a dataset from the UI, all views present in the dataset are autofilled, through
+ * calls to {@link View#autofill(AutofillValue)} or {@link View#autofill(SparseArray)}.
+ *
+ * <p>When the service returns ids of savable views, the Android System keeps track of changes
+ * made to these views, so they can be used to determine if the autofill save UI is shown later.
+ *
+ * <p>The context is then finished when one of the following occurs:
+ *
+ * <ul>
+ * <li>{@link #commit()} is called or all savable views are gone.
+ * <li>{@link #cancel()} is called.
+ * </ul>
+ *
+ * <p>Finally, after the autofill context is commited (i.e., not cancelled), the Android System
+ * shows an autofill save UI if the value of savable views have changed. If the user selects the
+ * option to Save, the current value of the views is then sent to the autofill service.
+ *
+ * <h3 id="additional-notes">Additional notes</h3>
+ *
+ * <p>It is safe to call <code>AutofillManager</code> methods from any thread.
+ */
+@SystemService(Context.AUTOFILL_MANAGER_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_AUTOFILL)
+public final class AutofillManager {
+
+ private static final String TAG = "AutofillManager";
+
+ /**
+ * Intent extra: The assist structure which captures the filled screen.
+ *
+ * <p>
+ * Type: {@link android.app.assist.AssistStructure}
+ */
+ public static final String EXTRA_ASSIST_STRUCTURE =
+ "android.view.autofill.extra.ASSIST_STRUCTURE";
+
+ /**
+ * Intent extra: The result of an authentication operation. It is
+ * either a fully populated {@link android.service.autofill.FillResponse}
+ * or a fully populated {@link android.service.autofill.Dataset} if
+ * a response or a dataset is being authenticated respectively.
+ *
+ * <p>
+ * Type: {@link android.service.autofill.FillResponse} or a
+ * {@link android.service.autofill.Dataset}
+ */
+ public static final String EXTRA_AUTHENTICATION_RESULT =
+ "android.view.autofill.extra.AUTHENTICATION_RESULT";
+
+ /**
+ * Intent extra: The optional boolean extra field provided by the
+ * {@link android.service.autofill.AutofillService} accompanying the {@link
+ * android.service.autofill.Dataset} result of an authentication operation.
+ *
+ * <p> Before {@link android.os.Build.VERSION_CODES#R}, if the authentication result is a
+ * {@link android.service.autofill.Dataset}, it'll be used to autofill the fields, and also
+ * replace the existing dataset in the cached {@link android.service.autofill.FillResponse}.
+ * That means if the user clears the field values, the autofill suggestion will show up again
+ * with the new authenticated Dataset.
+ *
+ * <p> In {@link android.os.Build.VERSION_CODES#R}, we added an exception to this behavior
+ * that if the Dataset being authenticated is a pinned dataset (see
+ * {@link android.service.autofill.InlinePresentation#isPinned()}), the old Dataset will not be
+ * replaced.
+ *
+ * <p> In {@link android.os.Build.VERSION_CODES#S}, we added this boolean extra field to
+ * allow the {@link android.service.autofill.AutofillService} to explicitly specify whether
+ * the returned authenticated Dataset is ephemeral. An ephemeral Dataset will be used to
+ * autofill once and then thrown away. Therefore, when the boolean extra is set to true, the
+ * returned Dataset will not replace the old dataset from the existing
+ * {@link android.service.autofill.FillResponse}. When it's set to false, it will. When it's not
+ * set, the old dataset will be replaced, unless it is a pinned inline suggestion, which is
+ * consistent with the behavior in {@link android.os.Build.VERSION_CODES#R}.
+ */
+ public static final String EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET =
+ "android.view.autofill.extra.AUTHENTICATION_RESULT_EPHEMERAL_DATASET";
+
+ /**
+ * Intent extra: The optional extras provided by the
+ * {@link android.service.autofill.AutofillService}.
+ *
+ * <p>For example, when the service responds to a {@link
+ * android.service.autofill.FillCallback#onSuccess(android.service.autofill.FillResponse)} with
+ * a {@code FillResponse} that requires authentication, the Intent that launches the
+ * service authentication will contain the Bundle set by
+ * {@link android.service.autofill.FillResponse.Builder#setClientState(Bundle)} on this extra.
+ *
+ * <p>On Android {@link android.os.Build.VERSION_CODES#P} and higher, the autofill service
+ * can also add this bundle to the {@link Intent} set as the
+ * {@link android.app.Activity#setResult(int, Intent) result} for an authentication request,
+ * so the bundle can be recovered later on
+ * {@link android.service.autofill.SaveRequest#getClientState()}.
+ *
+ * <p>
+ * Type: {@link android.os.Bundle}
+ */
+ public static final String EXTRA_CLIENT_STATE =
+ "android.view.autofill.extra.CLIENT_STATE";
+
+ /**
+ * Intent extra: the {@link android.view.inputmethod.InlineSuggestionsRequest} in the
+ * autofill request.
+ *
+ * <p>This is filled in the authentication intent so the
+ * {@link android.service.autofill.AutofillService} can use it to create the inline
+ * suggestion {@link android.service.autofill.Dataset} in the response, if the original autofill
+ * request contains the {@link android.view.inputmethod.InlineSuggestionsRequest}.
+ */
+ public static final String EXTRA_INLINE_SUGGESTIONS_REQUEST =
+ "android.view.autofill.extra.INLINE_SUGGESTIONS_REQUEST";
+
+ /** @hide */
+ public static final String EXTRA_RESTORE_SESSION_TOKEN =
+ "android.view.autofill.extra.RESTORE_SESSION_TOKEN";
+
+ /** @hide */
+ public static final String EXTRA_RESTORE_CROSS_ACTIVITY =
+ "android.view.autofill.extra.RESTORE_CROSS_ACTIVITY";
+
+ /**
+ * Internal extra used to pass a binder to the {@link IAugmentedAutofillManagerClient}.
+ *
+ * @hide
+ */
+ public static final String EXTRA_AUGMENTED_AUTOFILL_CLIENT =
+ "android.view.autofill.extra.AUGMENTED_AUTOFILL_CLIENT";
+
+ private static final String SESSION_ID_TAG = "android:sessionId";
+ private static final String STATE_TAG = "android:state";
+ private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData";
+
+ /** @hide */ public static final int ACTION_START_SESSION = 1;
+ /** @hide */ public static final int ACTION_VIEW_ENTERED = 2;
+ /** @hide */ public static final int ACTION_VIEW_EXITED = 3;
+ /** @hide */ public static final int ACTION_VALUE_CHANGED = 4;
+ /** @hide */ public static final int ACTION_RESPONSE_EXPIRED = 5;
+
+ /** @hide */ public static final int NO_LOGGING = 0;
+ /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED = 0x1;
+ /** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2;
+ /** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4;
+ /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY = 0x8;
+
+ // NOTE: flag below is used by the session start receiver only, hence it can have values above
+ /** @hide */ public static final int RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY = 0x1;
+
+ /** @hide */
+ public static final int DEFAULT_LOGGING_LEVEL = Build.IS_DEBUGGABLE
+ ? AutofillManager.FLAG_ADD_CLIENT_DEBUG
+ : AutofillManager.NO_LOGGING;
+
+ /** @hide */
+ public static final int DEFAULT_MAX_PARTITIONS_SIZE = 10;
+
+ /** Which bits in an authentication id are used for the dataset id */
+ private static final int AUTHENTICATION_ID_DATASET_ID_MASK = 0xFFFF;
+ /** How many bits in an authentication id are used for the dataset id */
+ private static final int AUTHENTICATION_ID_DATASET_ID_SHIFT = 16;
+ /** @hide The index for an undefined data set */
+ public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF;
+
+ /**
+ * Used on {@link #onPendingSaveUi(int, IBinder)} to cancel the pending UI.
+ *
+ * @hide
+ */
+ public static final int PENDING_UI_OPERATION_CANCEL = 1;
+
+ /**
+ * Used on {@link #onPendingSaveUi(int, IBinder)} to restore the pending UI.
+ *
+ * @hide
+ */
+ public static final int PENDING_UI_OPERATION_RESTORE = 2;
+
+ /**
+ * 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 = 0;
+
+ /**
+ * State where the autofill context hasn't been {@link #commit() finished} nor
+ * {@link #cancel() canceled} yet.
+ *
+ * @hide
+ */
+ 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 activity.
+ *
+ * <p>In this state, most apps callback (such as {@link #notifyViewEntered(View)}) are ignored,
+ * exception {@link #requestAutofill(View)} (and {@link #requestAutofill(View, int, Rect)}).
+ *
+ * @hide
+ */
+ public static final int STATE_FINISHED = 2;
+
+ /**
+ * State where the autofill context has been {@link #commit() finished} but the server still has
+ * a session because the Save UI hasn't been dismissed yet.
+ *
+ * @hide
+ */
+ public static final int STATE_SHOWING_SAVE_UI = 3;
+
+ /**
+ * State where the autofill is disabled because the service cannot autofill the activity at all.
+ *
+ * <p>In this state, every call is ignored, even {@link #requestAutofill(View)}
+ * (and {@link #requestAutofill(View, int, Rect)}).
+ *
+ * @hide
+ */
+ public static final int STATE_DISABLED_BY_SERVICE = 4;
+
+ /**
+ * Same as {@link #STATE_UNKNOWN}, but used on
+ * {@link AutofillManagerClient#setSessionFinished(int, List)} when the session was finished
+ * because the URL bar changed on client mode
+ *
+ * @hide
+ */
+ public static final int STATE_UNKNOWN_COMPAT_MODE = 5;
+
+ /**
+ * Same as {@link #STATE_UNKNOWN}, but used on
+ * {@link AutofillManagerClient#setSessionFinished(int, List)} when the session was finished
+ * because the service failed to fullfil a request.
+ *
+ * @hide
+ */
+ public static final int STATE_UNKNOWN_FAILED = 6;
+
+ /**
+ * Timeout in ms for calls to the field classification service.
+ * @hide
+ */
+ public static final int FC_SERVICE_TIMEOUT = 5000;
+
+ /**
+ * Timeout for calls to system_server.
+ */
+ private static final int SYNC_CALLS_TIMEOUT_MS = 5000;
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
+
+ /**
+ * Disables Augmented Autofill.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final int FLAG_SMART_SUGGESTION_OFF = 0x0;
+
+ /**
+ * Displays the Augment Autofill window using the same mechanism (such as a popup-window
+ * attached to the focused view) as the standard autofill.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final int FLAG_SMART_SUGGESTION_SYSTEM = 0x1;
+
+ /** @hide */
+ @IntDef(flag = false, value = { FLAG_SMART_SUGGESTION_OFF, FLAG_SMART_SUGGESTION_SYSTEM })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SmartSuggestionMode {}
+
+ /**
+ * {@code DeviceConfig} property used to set which Smart Suggestion modes for Augmented Autofill
+ * are available.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES =
+ "smart_suggestion_supported_modes";
+
+ /**
+ * Sets how long (in ms) the augmented autofill service is bound while idle.
+ *
+ * <p>Use {@code 0} to keep it permanently bound.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT =
+ "augmented_service_idle_unbind_timeout";
+
+ /**
+ * Sets how long (in ms) the augmented autofill service request is killed if not replied.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT =
+ "augmented_service_request_timeout";
+
+ /** @hide */
+ public static final int RESULT_OK = 0;
+ /** @hide */
+ public static final int RESULT_CODE_NOT_SERVICE = -1;
+
+ /**
+ * Makes an authentication id from a request id and a dataset id.
+ *
+ * @param requestId The request id.
+ * @param datasetId The dataset id.
+ * @return The authentication id.
+ * @hide
+ */
+ public static int makeAuthenticationId(int requestId, int datasetId) {
+ return (requestId << AUTHENTICATION_ID_DATASET_ID_SHIFT)
+ | (datasetId & AUTHENTICATION_ID_DATASET_ID_MASK);
+ }
+
+ /**
+ * Gets the request id from an authentication id.
+ *
+ * @param authRequestId The authentication id.
+ * @return The request id.
+ * @hide
+ */
+ public static int getRequestIdFromAuthenticationId(int authRequestId) {
+ return (authRequestId >> AUTHENTICATION_ID_DATASET_ID_SHIFT);
+ }
+
+ /**
+ * Gets the dataset id from an authentication id.
+ *
+ * @param authRequestId The authentication id.
+ * @return The dataset id.
+ * @hide
+ */
+ public static int getDatasetIdFromAuthenticationId(int authRequestId) {
+ return (authRequestId & AUTHENTICATION_ID_DATASET_ID_MASK);
+ }
+
+ private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+ /**
+ * There is currently no session running.
+ * {@hide}
+ */
+ public static final int NO_SESSION = Integer.MAX_VALUE;
+
+ private final IAutoFillManager mService;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private IAutoFillManagerClient mServiceClient;
+
+ @GuardedBy("mLock")
+ private Cleaner mServiceClientCleaner;
+
+ @GuardedBy("mLock")
+ private IAugmentedAutofillManagerClient mAugmentedAutofillServiceClient;
+
+ @GuardedBy("mLock")
+ private AutofillCallback mCallback;
+
+ private final Context mContext;
+
+ @GuardedBy("mLock")
+ private int mSessionId = NO_SESSION;
+
+ @GuardedBy("mLock")
+ private int mState = STATE_UNKNOWN;
+
+ @GuardedBy("mLock")
+ private boolean mEnabled;
+
+ /** If a view changes to this mapping the autofill operation was successful */
+ @GuardedBy("mLock")
+ @Nullable private ParcelableMap mLastAutofilledData;
+
+ /** If view tracking is enabled, contains the tracking state */
+ @GuardedBy("mLock")
+ @Nullable private TrackedViews mTrackedViews;
+
+ /** Views that are only tracked because they are fillable and could be anchoring the UI. */
+ @GuardedBy("mLock")
+ @Nullable private ArraySet<AutofillId> mFillableIds;
+
+ /** id of last requested autofill ui */
+ @Nullable private AutofillId mIdShownFillUi;
+
+ /**
+ * Views that were already "entered" - if they're entered again when the session is not active,
+ * they're ignored
+ * */
+ @GuardedBy("mLock")
+ @Nullable private ArraySet<AutofillId> mEnteredIds;
+
+ /**
+ * Views that were otherwised not important for autofill but triggered a session because the
+ * context is allowlisted for augmented autofill.
+ */
+ @GuardedBy("mLock")
+ @Nullable private Set<AutofillId> mEnteredForAugmentedAutofillIds;
+
+ /** If set, session is commited when the field is clicked. */
+ @GuardedBy("mLock")
+ @Nullable private AutofillId mSaveTriggerId;
+
+ /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */
+ @GuardedBy("mLock")
+ private boolean mOnInvisibleCalled;
+
+ /** If set, session is commited when the activity is finished; otherwise session is canceled. */
+ @GuardedBy("mLock")
+ private boolean mSaveOnFinish;
+
+ /** If compatibility mode is enabled - this is a bridge to interact with a11y */
+ @GuardedBy("mLock")
+ private CompatibilityBridge mCompatibilityBridge;
+
+ @Nullable
+ private final AutofillOptions mOptions;
+
+ /** When set, session is only used for augmented autofill requests. */
+ @GuardedBy("mLock")
+ private boolean mForAugmentedAutofillOnly;
+
+ /**
+ * When set, standard autofill is disabled, but sessions can still be created for augmented
+ * autofill only.
+ */
+ @GuardedBy("mLock")
+ private boolean mEnabledForAugmentedAutofillOnly;
+
+ /** @hide */
+ public interface AutofillClient {
+ /**
+ * Asks the client to start an authentication flow.
+ *
+ * @param authenticationId A unique id of the authentication operation.
+ * @param intent The authentication intent.
+ * @param fillInIntent The authentication fill-in intent.
+ */
+ void autofillClientAuthenticate(int authenticationId, IntentSender intent,
+ Intent fillInIntent, boolean authenticateInline);
+
+ /**
+ * Tells the client this manager has state to be reset.
+ */
+ void autofillClientResetableStateAvailable();
+
+ /**
+ * Request showing the autofill UI.
+ *
+ * @param anchor The real view the UI needs to anchor to.
+ * @param width The width of the fill UI content.
+ * @param height The height of the fill UI content.
+ * @param virtualBounds The bounds of the virtual decendant of the anchor.
+ * @param presenter The presenter that controls the fill UI window.
+ * @return Whether the UI was shown.
+ */
+ boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, int height,
+ @Nullable Rect virtualBounds, IAutofillWindowPresenter presenter);
+
+ /**
+ * Dispatch unhandled keyevent from Autofill window
+ * @param anchor The real view the UI needs to anchor to.
+ * @param keyEvent Unhandled KeyEvent from autofill window.
+ */
+ void autofillClientDispatchUnhandledKey(@NonNull View anchor, @NonNull KeyEvent keyEvent);
+
+ /**
+ * Request hiding the autofill UI.
+ *
+ * @return Whether the UI was hidden.
+ */
+ boolean autofillClientRequestHideFillUi();
+
+ /**
+ * Gets whether the fill UI is currenlty being shown.
+ *
+ * @return Whether the fill UI is currently being shown
+ */
+ boolean autofillClientIsFillUiShowing();
+
+ /**
+ * Checks if views are currently attached and visible.
+ *
+ * @return And array with {@code true} iff the view is attached or visible
+ */
+ @NonNull boolean[] autofillClientGetViewVisibility(@NonNull AutofillId[] autofillIds);
+
+ /**
+ * Checks is the client is currently visible as understood by autofill.
+ *
+ * @return {@code true} if the client is currently visible
+ */
+ boolean autofillClientIsVisibleForAutofill();
+
+ /**
+ * Client might disable enter/exit event e.g. when activity is paused.
+ */
+ boolean isDisablingEnterExitEventForAutofill();
+
+ /**
+ * Finds views by traversing the hierarchies of the client.
+ *
+ * @param autofillIds The autofill ids of the views to find
+ *
+ * @return And array containing the views (empty if no views found).
+ */
+ @NonNull View[] autofillClientFindViewsByAutofillIdTraversal(
+ @NonNull AutofillId[] autofillIds);
+
+ /**
+ * Finds a view by traversing the hierarchies of the client.
+ *
+ * @param autofillId The autofill id of the views to find
+ *
+ * @return The view, or {@code null} if not found
+ */
+ @Nullable View autofillClientFindViewByAutofillIdTraversal(@NonNull AutofillId autofillId);
+
+ /**
+ * Finds a view by a11y id in a given client window.
+ *
+ * @param viewId The accessibility id of the views to find
+ * @param windowId The accessibility window id where to search
+ *
+ * @return The view, or {@code null} if not found
+ */
+ @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId);
+
+ /**
+ * Runs the specified action on the UI thread.
+ */
+ void autofillClientRunOnUiThread(Runnable action);
+
+ /**
+ * Gets the complete component name of this client.
+ */
+ ComponentName autofillClientGetComponentName();
+
+ /**
+ * Gets the activity token
+ */
+ @Nullable IBinder autofillClientGetActivityToken();
+
+ /**
+ * @return Whether compatibility mode is enabled.
+ */
+ boolean autofillClientIsCompatibilityModeEnabled();
+
+ /**
+ * Gets the next unique autofill ID.
+ *
+ * <p>Typically used to manage views whose content is recycled - see
+ * {@link View#setAutofillId(AutofillId)} for more info.
+ *
+ * @return An ID that is unique in the activity.
+ */
+ @Nullable AutofillId autofillClientGetNextAutofillId();
+ }
+
+ /**
+ * @hide
+ */
+ public AutofillManager(Context context, IAutoFillManager service) {
+ mContext = Preconditions.checkNotNull(context, "context cannot be null");
+ mService = service;
+ mOptions = context.getAutofillOptions();
+
+ if (mOptions != null) {
+ sDebug = (mOptions.loggingLevel & FLAG_ADD_CLIENT_DEBUG) != 0;
+ sVerbose = (mOptions.loggingLevel & FLAG_ADD_CLIENT_VERBOSE) != 0;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void enableCompatibilityMode() {
+ synchronized (mLock) {
+ // The accessibility manager is a singleton so we may need to plug
+ // different bridge based on which activity is currently focused
+ // in the current process. Since compat would be rarely used, just
+ // create and register a new instance every time.
+ if (sDebug) {
+ Slog.d(TAG, "creating CompatibilityBridge for " + mContext);
+ }
+ mCompatibilityBridge = new CompatibilityBridge();
+ }
+ }
+
+ /**
+ * Restore state after activity lifecycle
+ *
+ * @param savedInstanceState The state to be restored
+ *
+ * {@hide}
+ */
+ public void onCreate(Bundle savedInstanceState) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ mLastAutofilledData = savedInstanceState.getParcelable(LAST_AUTOFILLED_DATA_TAG);
+
+ if (isActiveLocked()) {
+ Log.w(TAG, "New session was started before onCreate()");
+ return;
+ }
+
+ mSessionId = savedInstanceState.getInt(SESSION_ID_TAG, NO_SESSION);
+ mState = savedInstanceState.getInt(STATE_TAG, STATE_UNKNOWN);
+
+ if (mSessionId != NO_SESSION) {
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+
+ final AutofillClient client = getClient();
+ if (client != null) {
+ final SyncResultReceiver receiver = new SyncResultReceiver(
+ SYNC_CALLS_TIMEOUT_MS);
+ try {
+ boolean sessionWasRestored = false;
+ if (clientAdded) {
+ mService.restoreSession(mSessionId,
+ client.autofillClientGetActivityToken(),
+ mServiceClient.asBinder(), receiver);
+ sessionWasRestored = receiver.getIntResult() == 1;
+ } else {
+ Log.w(TAG, "No service client for session " + mSessionId);
+ }
+
+ if (!sessionWasRestored) {
+ Log.w(TAG, "Session " + mSessionId + " could not be restored");
+ mSessionId = NO_SESSION;
+ mState = STATE_UNKNOWN;
+ } else {
+ if (sDebug) {
+ Log.d(TAG, "session " + mSessionId + " was restored");
+ }
+
+ client.autofillClientResetableStateAvailable();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not figure out if there was an autofill session", e);
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.e(TAG, "Fail to get session restore status: " + e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Called once the client becomes visible.
+ *
+ * @see AutofillClient#autofillClientIsVisibleForAutofill()
+ *
+ * {@hide}
+ */
+ public void onVisibleForAutofill() {
+ // This gets called when the client just got visible at which point the visibility
+ // of the tracked views may not have been computed (due to a pending layout, etc).
+ // While generally we have no way to know when the UI has settled. We will evaluate
+ // the tracked views state at the end of next frame to guarantee that everything
+ // that may need to be laid out is laid out.
+ Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> {
+ synchronized (mLock) {
+ if (mEnabled && isActiveLocked() && mTrackedViews != null) {
+ mTrackedViews.onVisibleForAutofillChangedLocked();
+ }
+ }
+ }, null);
+ }
+
+ /**
+ * Called once the client becomes invisible.
+ *
+ * @see AutofillClient#autofillClientIsVisibleForAutofill()
+ *
+ * @param isExpiredResponse The response has expired or not
+ *
+ * {@hide}
+ */
+ public void onInvisibleForAutofill(boolean isExpiredResponse) {
+ synchronized (mLock) {
+ mOnInvisibleCalled = true;
+
+ if (isExpiredResponse) {
+ // Notify service the response has expired.
+ updateSessionLocked(/* id= */ null, /* bounds= */ null, /* value= */ null,
+ ACTION_RESPONSE_EXPIRED, /* flags= */ 0);
+ }
+ }
+ }
+
+ /**
+ * Save state before activity lifecycle
+ *
+ * @param outState Place to store the state
+ *
+ * {@hide}
+ */
+ public void onSaveInstanceState(Bundle outState) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mSessionId != NO_SESSION) {
+ outState.putInt(SESSION_ID_TAG, mSessionId);
+ }
+ if (mState != STATE_UNKNOWN) {
+ outState.putInt(STATE_TAG, mState);
+ }
+ if (mLastAutofilledData != null) {
+ outState.putParcelable(LAST_AUTOFILLED_DATA_TAG, mLastAutofilledData);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @GuardedBy("mLock")
+ public boolean isCompatibilityModeEnabledLocked() {
+ return mCompatibilityBridge != null;
+ }
+
+ /**
+ * Checks whether autofill is enabled for the current user.
+ *
+ * <p>Typically used to determine whether the option to explicitly request autofill should
+ * be offered - see {@link #requestAutofill(View)}.
+ *
+ * @return whether autofill is enabled for the current user.
+ */
+ public boolean isEnabled() {
+ if (!hasAutofillFeature()) {
+ return false;
+ }
+ synchronized (mLock) {
+ if (isDisabledByServiceLocked()) {
+ return false;
+ }
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+ return clientAdded ? mEnabled : false;
+ }
+ }
+
+ /**
+ * Should always be called from {@link AutofillService#getFillEventHistory()}.
+ *
+ * @hide
+ */
+ @Nullable public FillEventHistory getFillEventHistory() {
+ try {
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ mService.getFillEventHistory(receiver);
+ return receiver.getParcelableResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.e(TAG, "Fail to get fill event history: " + e);
+ return null;
+ }
+ }
+
+ /**
+ * Explicitly requests a new autofill context.
+ *
+ * <p>Normally, the autofill context is automatically started if necessary when
+ * {@link #notifyViewEntered(View)} is called, but this method should be used in the
+ * cases where it must be explicitly started. For example, when the view offers an AUTOFILL
+ * option on its contextual overflow menu, and the user selects it.
+ *
+ * @param view view requesting the new autofill context.
+ */
+ public void requestAutofill(@NonNull View view) {
+ int flags = FLAG_MANUAL_REQUEST;
+ if (!view.isFocused()) {
+ flags |= FLAG_VIEW_NOT_FOCUSED;
+ }
+ notifyViewEntered(view, flags);
+ }
+
+ /**
+ * Explicitly cancels the current session and requests a new autofill context.
+ *
+ * <p>Normally, the autofill context is automatically started if necessary when
+ * {@link #notifyViewEntered(View)} is called, but this method should be used in
+ * cases where it must be explicitly started or restarted. Currently, this method should only
+ * be called by
+ * {@link android.service.autofill.augmented.AugmentedAutofillService#requestAutofill(
+ * ComponentName, AutofillId)} to cancel the current session and trigger the autofill flow in
+ * a new session, giving the autofill service or the augmented autofill service a chance to
+ * send updated suggestions.
+ *
+ * @param view view requesting the new autofill context.
+ */
+ void requestAutofillFromNewSession(@NonNull View view) {
+ cancel();
+ notifyViewEntered(view);
+ }
+
+ /**
+ * Explicitly requests a new autofill context for virtual views.
+ *
+ * <p>Normally, the autofill context is automatically started if necessary when
+ * {@link #notifyViewEntered(View, int, Rect)} is called, but this method should be used in the
+ * cases where it must be explicitly started. For example, when the virtual view offers an
+ * AUTOFILL option on its contextual overflow menu, and the user selects it.
+ *
+ * <p>The virtual view boundaries must be absolute screen coordinates. For example, if the
+ * parent view uses {@code bounds} to draw the virtual view inside its Canvas,
+ * the absolute bounds could be calculated by:
+ *
+ * <pre class="prettyprint">
+ * int offset[] = new int[2];
+ * getLocationOnScreen(offset);
+ * Rect absBounds = new Rect(bounds.left + offset[0],
+ * bounds.top + offset[1],
+ * bounds.right + offset[0], bounds.bottom + offset[1]);
+ * </pre>
+ *
+ * @param view the virtual view parent.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ * @param absBounds absolute boundaries of the virtual view in the screen.
+ */
+ public void requestAutofill(@NonNull View view, int virtualId, @NonNull Rect absBounds) {
+ int flags = FLAG_MANUAL_REQUEST;
+ if (!view.isFocused()) {
+ flags |= FLAG_VIEW_NOT_FOCUSED;
+ }
+ notifyViewEntered(view, virtualId, absBounds, flags);
+ }
+
+ /**
+ * Called when a {@link View} that supports autofill is entered.
+ *
+ * @param view {@link View} that was entered.
+ */
+ public void notifyViewEntered(@NonNull View view) {
+ notifyViewEntered(view, 0);
+ }
+
+ @GuardedBy("mLock")
+ private boolean shouldIgnoreViewEnteredLocked(@NonNull AutofillId id, int flags) {
+ if (isDisabledByServiceLocked()) {
+ if (sVerbose) {
+ Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id
+ + ") on state " + getStateAsStringLocked() + " because disabled by svc");
+ }
+ return true;
+ }
+ if (isFinishedLocked()) {
+ // Session already finished: ignore if automatic request and view already entered
+ if ((flags & FLAG_MANUAL_REQUEST) == 0 && mEnteredIds != null
+ && mEnteredIds.contains(id)) {
+ if (sVerbose) {
+ Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id
+ + ") on state " + getStateAsStringLocked()
+ + " because view was already entered: " + mEnteredIds);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isClientVisibleForAutofillLocked() {
+ final AutofillClient client = getClient();
+ return client != null && client.autofillClientIsVisibleForAutofill();
+ }
+
+ private boolean isClientDisablingEnterExitEvent() {
+ final AutofillClient client = getClient();
+ return client != null && client.isDisablingEnterExitEventForAutofill();
+ }
+
+ private void notifyViewEntered(@NonNull View view, int flags) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ AutofillCallback callback;
+ synchronized (mLock) {
+ callback = notifyViewEnteredLocked(view, flags);
+ }
+
+ if (callback != null) {
+ mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+ }
+ }
+
+ /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
+ @GuardedBy("mLock")
+ private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) {
+ final AutofillId id = view.getAutofillId();
+ if (shouldIgnoreViewEnteredLocked(id, flags)) return null;
+
+ AutofillCallback callback = null;
+
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+
+ if (!clientAdded) {
+ if (sVerbose) Log.v(TAG, "ignoring notifyViewEntered(" + id + "): no service client");
+ return callback;
+ }
+
+ if (!mEnabled && !mEnabledForAugmentedAutofillOnly) {
+ if (sVerbose) Log.v(TAG, "ignoring notifyViewEntered(" + id + "): disabled");
+
+ if (mCallback != null) {
+ callback = mCallback;
+ }
+ } else {
+ // don't notify entered when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
+ final AutofillValue value = view.getAutofillValue();
+
+ if (view instanceof TextView && ((TextView) view).isAnyPasswordInputType()) {
+ flags |= FLAG_PASSWORD_INPUT_TYPE;
+ }
+
+ if (!isActiveLocked()) {
+ // Starts new session.
+ startSessionLocked(id, null, value, flags);
+ } else {
+ // Update focus on existing session.
+ if (mForAugmentedAutofillOnly && (flags & FLAG_MANUAL_REQUEST) != 0) {
+ if (sDebug) {
+ Log.d(TAG, "notifyViewEntered(" + id + "): resetting "
+ + "mForAugmentedAutofillOnly on manual request");
+ }
+ mForAugmentedAutofillOnly = false;
+ }
+ updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags);
+ }
+ addEnteredIdLocked(id);
+ }
+ }
+ return callback;
+ }
+
+ /**
+ * Called when a {@link View} that supports autofill is exited.
+ *
+ * @param view {@link View} that was exited.
+ */
+ public void notifyViewExited(@NonNull View view) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ notifyViewExitedLocked(view);
+ }
+ }
+
+ @GuardedBy("mLock")
+ void notifyViewExitedLocked(@NonNull View view) {
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+
+ if (clientAdded && (mEnabled || mEnabledForAugmentedAutofillOnly)
+ && isActiveLocked()) {
+ // dont notify exited when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
+ final AutofillId id = view.getAutofillId();
+
+ // Update focus on existing session.
+ updateSessionLocked(id, null, null, ACTION_VIEW_EXITED, 0);
+ }
+ }
+ }
+
+ /**
+ * Called when a {@link View view's} visibility changed.
+ *
+ * @param view {@link View} that was exited.
+ * @param isVisible visible if the view is visible in the view hierarchy.
+ */
+ public void notifyViewVisibilityChanged(@NonNull View view, boolean isVisible) {
+ notifyViewVisibilityChangedInternal(view, 0, isVisible, false);
+ }
+
+ /**
+ * Called when a virtual view's visibility changed.
+ *
+ * @param view {@link View} that was exited.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ * @param isVisible visible if the view is visible in the view hierarchy.
+ */
+ public void notifyViewVisibilityChanged(@NonNull View view, int virtualId, boolean isVisible) {
+ notifyViewVisibilityChangedInternal(view, virtualId, isVisible, true);
+ }
+
+ /**
+ * Called when a view/virtual view's visibility changed.
+ *
+ * @param view {@link View} that was exited.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ * @param isVisible visible if the view is visible in the view hierarchy.
+ * @param virtual Whether the view is virtual.
+ */
+ private void notifyViewVisibilityChangedInternal(@NonNull View view, int virtualId,
+ boolean isVisible, boolean virtual) {
+ synchronized (mLock) {
+ if (mForAugmentedAutofillOnly) {
+ if (sVerbose) {
+ Log.v(TAG, "notifyViewVisibilityChanged(): ignoring on augmented only mode");
+ }
+ return;
+ }
+ if (mEnabled && isActiveLocked()) {
+ final AutofillId id = virtual ? getAutofillId(view, virtualId)
+ : view.getAutofillId();
+ if (sVerbose) Log.v(TAG, "visibility changed for " + id + ": " + isVisible);
+ if (!isVisible && mFillableIds != null) {
+ if (mFillableIds.contains(id)) {
+ if (sDebug) Log.d(TAG, "Hidding UI when view " + id + " became invisible");
+ requestHideFillUi(id, view);
+ }
+ }
+ if (mTrackedViews != null) {
+ mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible);
+ } else if (sVerbose) {
+ Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views");
+ }
+ } else if (!virtual && isVisible) {
+ startAutofillIfNeededLocked(view);
+ }
+ }
+ }
+
+ /**
+ * Called when a virtual view that supports autofill is entered.
+ *
+ * <p>The virtual view boundaries must be absolute screen coordinates. For example, if the
+ * parent, non-virtual view uses {@code bounds} to draw the virtual view inside its Canvas,
+ * the absolute bounds could be calculated by:
+ *
+ * <pre class="prettyprint">
+ * int offset[] = new int[2];
+ * getLocationOnScreen(offset);
+ * Rect absBounds = new Rect(bounds.left + offset[0],
+ * bounds.top + offset[1],
+ * bounds.right + offset[0], bounds.bottom + offset[1]);
+ * </pre>
+ *
+ * @param view the virtual view parent.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ * @param absBounds absolute boundaries of the virtual view in the screen.
+ */
+ public void notifyViewEntered(@NonNull View view, int virtualId, @NonNull Rect absBounds) {
+ notifyViewEntered(view, virtualId, absBounds, 0);
+ }
+
+ private void notifyViewEntered(View view, int virtualId, Rect bounds, int flags) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ AutofillCallback callback;
+ synchronized (mLock) {
+ callback = notifyViewEnteredLocked(view, virtualId, bounds, flags);
+ }
+
+ if (callback != null) {
+ callback.onAutofillEvent(view, virtualId,
+ AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+ }
+ }
+
+ /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
+ @GuardedBy("mLock")
+ private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds,
+ int flags) {
+ final AutofillId id = getAutofillId(view, virtualId);
+ AutofillCallback callback = null;
+ if (shouldIgnoreViewEnteredLocked(id, flags)) return callback;
+
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+
+ if (!clientAdded) {
+ if (sVerbose) Log.v(TAG, "ignoring notifyViewEntered(" + id + "): no service client");
+ return callback;
+ }
+
+ if (!mEnabled && !mEnabledForAugmentedAutofillOnly) {
+ if (sVerbose) {
+ Log.v(TAG, "ignoring notifyViewEntered(" + id + "): disabled");
+ }
+ if (mCallback != null) {
+ callback = mCallback;
+ }
+ } else {
+ // don't notify entered when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
+ if (view instanceof TextView && ((TextView) view).isAnyPasswordInputType()) {
+ flags |= FLAG_PASSWORD_INPUT_TYPE;
+ }
+
+ if (!isActiveLocked()) {
+ // Starts new session.
+ startSessionLocked(id, bounds, null, flags);
+ } else {
+ // Update focus on existing session.
+ if (mForAugmentedAutofillOnly && (flags & FLAG_MANUAL_REQUEST) != 0) {
+ if (sDebug) {
+ Log.d(TAG, "notifyViewEntered(" + id + "): resetting "
+ + "mForAugmentedAutofillOnly on manual request");
+ }
+ mForAugmentedAutofillOnly = false;
+ }
+ updateSessionLocked(id, bounds, null, ACTION_VIEW_ENTERED, flags);
+ }
+ addEnteredIdLocked(id);
+ }
+ }
+ return callback;
+ }
+
+ @GuardedBy("mLock")
+ private void addEnteredIdLocked(@NonNull AutofillId id) {
+ if (mEnteredIds == null) {
+ mEnteredIds = new ArraySet<>(1);
+ }
+ id.resetSessionId();
+ mEnteredIds.add(id);
+ }
+
+ /**
+ * Called when a virtual view that supports autofill is exited.
+ *
+ * @param view the virtual view parent.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ */
+ public void notifyViewExited(@NonNull View view, int virtualId) {
+ if (sVerbose) Log.v(TAG, "notifyViewExited(" + view.getAutofillId() + ", " + virtualId);
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ notifyViewExitedLocked(view, virtualId);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void notifyViewExitedLocked(@NonNull View view, int virtualId) {
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+
+ if (clientAdded && (mEnabled || mEnabledForAugmentedAutofillOnly)
+ && isActiveLocked()) {
+ // don't notify exited when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
+ final AutofillId id = getAutofillId(view, virtualId);
+
+ // Update focus on existing session.
+ updateSessionLocked(id, null, null, ACTION_VIEW_EXITED, 0);
+ }
+ }
+ }
+
+ /**
+ * Called to indicate the value of an autofillable {@link View} changed.
+ *
+ * @param view view whose value changed.
+ */
+ public void notifyValueChanged(View view) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ AutofillId id = null;
+ boolean valueWasRead = false;
+ AutofillValue value = null;
+
+ synchronized (mLock) {
+ // If the session is gone some fields might still be highlighted, hence we have to
+ // remove the isAutofilled property even if no sessions are active.
+ if (mLastAutofilledData == null) {
+ view.setAutofilled(false, false);
+ } else {
+ id = view.getAutofillId();
+ if (mLastAutofilledData.containsKey(id)) {
+ value = view.getAutofillValue();
+ valueWasRead = true;
+ final boolean hideHighlight = mLastAutofilledData.keySet().size() == 1;
+
+ if (Objects.equals(mLastAutofilledData.get(id), value)) {
+ view.setAutofilled(true, hideHighlight);
+ } else {
+ view.setAutofilled(false, false);
+ mLastAutofilledData.remove(id);
+ }
+ } else {
+ view.setAutofilled(false, false);
+ }
+ }
+
+ if (!mEnabled || !isActiveLocked()) {
+ if (!startAutofillIfNeededLocked(view)) {
+ if (sVerbose) {
+ Log.v(TAG, "notifyValueChanged(" + view.getAutofillId()
+ + "): ignoring on state " + getStateAsStringLocked());
+ }
+ }
+ return;
+ }
+
+ if (id == null) {
+ id = view.getAutofillId();
+ }
+
+ if (!valueWasRead) {
+ value = view.getAutofillValue();
+ }
+
+ updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, 0);
+ }
+ }
+
+ /**
+ * Called to indicate the value of an autofillable virtual view has changed.
+ *
+ * @param view the virtual view parent.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ * @param value new value of the child.
+ */
+ public void notifyValueChanged(View view, int virtualId, AutofillValue value) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (!mEnabled || !isActiveLocked()) {
+ if (sVerbose) {
+ Log.v(TAG, "notifyValueChanged(" + view.getAutofillId() + ":" + virtualId
+ + "): ignoring on state " + getStateAsStringLocked());
+ }
+ return;
+ }
+
+ final AutofillId id = getAutofillId(view, virtualId);
+ updateSessionLocked(id, null, value, ACTION_VALUE_CHANGED, 0);
+ }
+ }
+
+ /**
+ * Called to indicate a {@link View} is clicked.
+ *
+ * @param view view that has been clicked.
+ */
+ public void notifyViewClicked(@NonNull View view) {
+ notifyViewClicked(view.getAutofillId());
+ }
+
+ /**
+ * Called to indicate a virtual view has been clicked.
+ *
+ * @param view the virtual view parent.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ */
+ public void notifyViewClicked(@NonNull View view, int virtualId) {
+ notifyViewClicked(getAutofillId(view, virtualId));
+ }
+
+ private void notifyViewClicked(AutofillId id) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId);
+
+ synchronized (mLock) {
+ if (!mEnabled || !isActiveLocked()) {
+ return;
+ }
+ if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) {
+ if (sDebug) Log.d(TAG, "triggering commit by click of " + id);
+ commitLocked();
+ mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_SAVE_EXPLICITLY_TRIGGERED));
+ }
+ }
+ }
+
+ /**
+ * Called by {@link android.app.Activity} to commit or cancel the session on finish.
+ *
+ * @hide
+ */
+ public void onActivityFinishing() {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mSaveOnFinish) {
+ if (sDebug) Log.d(TAG, "onActivityFinishing(): calling commitLocked()");
+ commitLocked();
+ } else {
+ if (sDebug) Log.d(TAG, "onActivityFinishing(): calling cancelLocked()");
+ cancelLocked();
+ }
+ }
+ }
+
+ /**
+ * Called to indicate the current autofill context should be commited.
+ *
+ * <p>This method is typically called by {@link View Views} that manage virtual views; for
+ * example, when the view is rendering an {@code HTML} page with a form and virtual views
+ * that represent the HTML elements, it should call this method after the form is submitted and
+ * another page is rendered.
+ *
+ * <p><b>Note:</b> This method does not need to be called on regular application lifecycle
+ * methods such as {@link android.app.Activity#finish()}.
+ */
+ public void commit() {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ if (sVerbose) Log.v(TAG, "commit() called by app");
+ synchronized (mLock) {
+ commitLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void commitLocked() {
+ if (!mEnabled && !isActiveLocked()) {
+ return;
+ }
+ finishSessionLocked();
+ }
+
+ /**
+ * Called to indicate the current autofill context should be cancelled.
+ *
+ * <p>This method is typically called by {@link View Views} that manage virtual views; for
+ * example, when the view is rendering an {@code HTML} page with a form and virtual views
+ * that represent the HTML elements, it should call this method if the user does not post the
+ * form but moves to another form in this page.
+ *
+ * <p><b>Note:</b> This method does not need to be called on regular application lifecycle
+ * methods such as {@link android.app.Activity#finish()}.
+ */
+ public void cancel() {
+ if (sVerbose) Log.v(TAG, "cancel() called by app or augmented autofill service");
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ cancelLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void cancelLocked() {
+ if (!mEnabled && !isActiveLocked()) {
+ return;
+ }
+ cancelSessionLocked();
+ }
+
+ /** @hide */
+ public void disableOwnedAutofillServices() {
+ disableAutofillServices();
+ }
+
+ /**
+ * If the app calling this API has enabled autofill services they
+ * will be disabled.
+ */
+ public void disableAutofillServices() {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ try {
+ mService.disableOwnedAutofillServices(mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns {@code true} if the calling application provides a {@link AutofillService} that is
+ * enabled for the current user, or {@code false} otherwise.
+ */
+ public boolean hasEnabledAutofillServices() {
+ if (mService == null) return false;
+
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.isServiceEnabled(mContext.getUserId(), mContext.getPackageName(),
+ receiver);
+ return receiver.getIntResult() == 1;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get enabled autofill services status.");
+ }
+ }
+
+ /**
+ * Returns the component name of the {@link AutofillService} that is enabled for the current
+ * user.
+ */
+ @Nullable
+ public ComponentName getAutofillServiceComponentName() {
+ if (mService == null) return null;
+
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.getAutofillServiceComponentName(receiver);
+ return receiver.getParcelableResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get autofill services component name.");
+ }
+ }
+
+ /**
+ * Gets the id of the {@link UserData} used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p>This method is useful when the service must check the status of the {@link UserData} in
+ * the device without fetching the whole object.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ *
+ * @return id of the {@link UserData} previously set by {@link #setUserData(UserData)}
+ * or {@code null} if it was reset or if the caller currently does not have an enabled autofill
+ * service for the user.
+ */
+ @Nullable public String getUserDataId() {
+ try {
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ mService.getUserDataId(receiver);
+ return receiver.getStringResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get user data id for field classification.");
+ }
+ }
+
+ /**
+ * Gets the user data used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ *
+ * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was
+ * reset or if the caller currently does not have an enabled autofill service for the user.
+ */
+ @Nullable public UserData getUserData() {
+ try {
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ mService.getUserData(receiver);
+ return receiver.getParcelableResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get user data for field classification.");
+ }
+ }
+
+ /**
+ * Sets the {@link UserData} used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ */
+ public void setUserData(@Nullable UserData userData) {
+ try {
+ mService.setUserData(userData);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks if <a href="AutofillService.html#FieldClassification">field classification</a> is
+ * enabled.
+ *
+ * <p>As field classification is an expensive operation, it could be disabled, either
+ * temporarily (for example, because the service exceeded a rate-limit threshold) or
+ * permanently (for example, because the device is a low-level device).
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ */
+ public boolean isFieldClassificationEnabled() {
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.isFieldClassificationEnabled(receiver);
+ return receiver.getIntResult() == 1;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get field classification enabled status.");
+ }
+ }
+
+ /**
+ * Gets the name of the default algorithm used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p>The default algorithm is used when the algorithm on {@link UserData} is invalid or not
+ * set.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ */
+ @Nullable
+ public String getDefaultFieldClassificationAlgorithm() {
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.getDefaultFieldClassificationAlgorithm(receiver);
+ return receiver.getStringResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get default field classification algorithm.");
+ }
+ }
+
+ /**
+ * Gets the name of all algorithms currently available for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it returns an empty list if the caller currently doesn't have an enabled autofill service
+ * for the user.
+ */
+ @NonNull
+ public List<String> getAvailableFieldClassificationAlgorithms() {
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.getAvailableFieldClassificationAlgorithms(receiver);
+ final String[] algorithms = receiver.getStringArrayResult();
+ return algorithms != null ? Arrays.asList(algorithms) : Collections.emptyList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get available field classification algorithms.");
+ }
+ }
+
+ /**
+ * Returns {@code true} if autofill is supported by the current device and
+ * is supported for this user.
+ *
+ * <p>Autofill is typically supported, but it could be unsupported in cases like:
+ * <ol>
+ * <li>Low-end devices.
+ * <li>Device policy rules that forbid its usage.
+ * </ol>
+ */
+ public boolean isAutofillSupported() {
+ if (mService == null) return false;
+
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.isServiceSupported(mContext.getUserId(), receiver);
+ return receiver.getIntResult() == 1;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get autofill supported status.");
+ }
+ }
+
+ // Note: don't need to use locked suffix because mContext is final.
+ private AutofillClient getClient() {
+ final AutofillClient client = mContext.getAutofillClient();
+ if (client == null && sVerbose) {
+ Log.v(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context "
+ + mContext);
+ }
+ return client;
+ }
+
+ /**
+ * Check if autofill ui is showing, must be called on UI thread.
+ * @hide
+ */
+ public boolean isAutofillUiShowing() {
+ final AutofillClient client = mContext.getAutofillClient();
+ return client != null && client.autofillClientIsFillUiShowing();
+ }
+
+ /** @hide */
+ public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ // TODO: the result code is being ignored, so this method is not reliably
+ // handling the cases where it's not RESULT_OK: it works fine if the service does not
+ // set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the
+ // service set the extra and returned RESULT_CANCELED...
+
+ if (sDebug) {
+ Log.d(TAG, "onAuthenticationResult(): id= " + authenticationId + ", data=" + data);
+ }
+
+ synchronized (mLock) {
+ if (!isActiveLocked()) {
+ return;
+ }
+ // If authenticate activity closes itself during onCreate(), there is no onStop/onStart
+ // of app activity. We enforce enter event to re-show fill ui in such case.
+ // CTS example:
+ // LoginActivityTest#testDatasetAuthTwoFieldsUserCancelsFirstAttempt
+ // LoginActivityTest#testFillResponseAuthBothFieldsUserCancelsFirstAttempt
+ if (!mOnInvisibleCalled && focusView != null
+ && focusView.canNotifyAutofillEnterExitEvent()) {
+ notifyViewExitedLocked(focusView);
+ notifyViewEnteredLocked(focusView, 0);
+ }
+ if (data == null) {
+ // data is set to null when result is not RESULT_OK
+ return;
+ }
+
+ final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
+ final Bundle responseData = new Bundle();
+ responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
+ final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE);
+ if (newClientState != null) {
+ responseData.putBundle(EXTRA_CLIENT_STATE, newClientState);
+ }
+ if (data.getExtras().containsKey(EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET)) {
+ responseData.putBoolean(EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET,
+ data.getBooleanExtra(EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET,
+ false));
+ }
+ try {
+ mService.setAuthenticationResult(responseData, mSessionId, authenticationId,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error delivering authentication result", e);
+ }
+ }
+ }
+
+ /**
+ * Gets the next unique autofill ID for the activity context.
+ *
+ * <p>Typically used to manage views whose content is recycled - see
+ * {@link View#setAutofillId(AutofillId)} for more info.
+ *
+ * @return An ID that is unique in the activity, or {@code null} if autofill is not supported in
+ * the {@link Context} associated with this {@link AutofillManager}.
+ */
+ @Nullable
+ public AutofillId getNextAutofillId() {
+ final AutofillClient client = getClient();
+ if (client == null) return null;
+
+ final AutofillId id = client.autofillClientGetNextAutofillId();
+
+ if (id == null && sDebug) {
+ Log.d(TAG, "getNextAutofillId(): client " + client + " returned null");
+ }
+
+ return id;
+ }
+
+ private static AutofillId getAutofillId(View parent, int virtualId) {
+ return new AutofillId(parent.getAutofillViewId(), virtualId);
+ }
+
+ @GuardedBy("mLock")
+ private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds,
+ @NonNull AutofillValue value, int flags) {
+ if (mEnteredForAugmentedAutofillIds != null
+ && mEnteredForAugmentedAutofillIds.contains(id)
+ || mEnabledForAugmentedAutofillOnly) {
+ if (sVerbose) Log.v(TAG, "Starting session for augmented autofill on " + id);
+ flags |= FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY;
+ }
+ if (sVerbose) {
+ Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value
+ + ", flags=" + flags + ", state=" + getStateAsStringLocked()
+ + ", compatMode=" + isCompatibilityModeEnabledLocked()
+ + ", augmentedOnly=" + mForAugmentedAutofillOnly
+ + ", enabledAugmentedOnly=" + mEnabledForAugmentedAutofillOnly
+ + ", enteredIds=" + mEnteredIds);
+ }
+ // We need to reset the augmented-only state when a manual request is made, as it's possible
+ // that the service returned null for the first request and now the user is manually
+ // requesting autofill to trigger a custom UI provided by the service.
+ if (mForAugmentedAutofillOnly && !mEnabledForAugmentedAutofillOnly
+ && (flags & FLAG_MANUAL_REQUEST) != 0) {
+ if (sVerbose) {
+ Log.v(TAG, "resetting mForAugmentedAutofillOnly on manual autofill request");
+ }
+ mForAugmentedAutofillOnly = false;
+ }
+ if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) {
+ if (sVerbose) {
+ Log.v(TAG, "not automatically starting session for " + id
+ + " on state " + getStateAsStringLocked() + " and flags " + flags);
+ }
+ return;
+ }
+ try {
+ final AutofillClient client = getClient();
+ if (client == null) return; // NOTE: getClient() already logged it..
+
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ final ComponentName clientActivity = client.autofillClientGetComponentName();
+
+ if (!mEnabledForAugmentedAutofillOnly && mOptions != null
+ && mOptions.isAutofillDisabledLocked(clientActivity)) {
+ if (mOptions.isAugmentedAutofillEnabled(mContext)) {
+ if (sDebug) {
+ Log.d(TAG, "startSession(" + clientActivity + "): disabled by service but "
+ + "allowlisted for augmented autofill");
+ flags |= FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY;
+ }
+ } else {
+ if (sDebug) {
+ Log.d(TAG, "startSession(" + clientActivity + "): ignored because "
+ + "disabled by service and not allowlisted for augmented autofill");
+ }
+ setSessionFinished(AutofillManager.STATE_DISABLED_BY_SERVICE, null);
+ client.autofillClientResetableStateAvailable();
+ return;
+ }
+ }
+
+ mService.startSession(client.autofillClientGetActivityToken(),
+ mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
+ mCallback != null, flags, clientActivity,
+ isCompatibilityModeEnabledLocked(), receiver);
+ mSessionId = receiver.getIntResult();
+ if (mSessionId != NO_SESSION) {
+ mState = STATE_ACTIVE;
+ }
+ final int extraFlags = receiver.getOptionalExtraIntResult(0);
+ if ((extraFlags & RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) {
+ if (sDebug) Log.d(TAG, "startSession(" + clientActivity + "): for augmented only");
+ mForAugmentedAutofillOnly = true;
+ }
+ client.autofillClientResetableStateAvailable();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ // no-op, just log the error message.
+ Log.w(TAG, "Exception getting result from SyncResultReceiver: " + e);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void finishSessionLocked() {
+ if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + getStateAsStringLocked());
+
+ if (!isActiveLocked()) return;
+
+ try {
+ mService.finishSession(mSessionId, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ resetSessionLocked(/* resetEnteredIds= */ true);
+ }
+
+ @GuardedBy("mLock")
+ private void cancelSessionLocked() {
+ if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + getStateAsStringLocked());
+
+ if (!isActiveLocked()) return;
+
+ try {
+ mService.cancelSession(mSessionId, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ resetSessionLocked(/* resetEnteredIds= */ true);
+ }
+
+ @GuardedBy("mLock")
+ private void resetSessionLocked(boolean resetEnteredIds) {
+ mSessionId = NO_SESSION;
+ mState = STATE_UNKNOWN;
+ mTrackedViews = null;
+ mFillableIds = null;
+ mSaveTriggerId = null;
+ mIdShownFillUi = null;
+ if (resetEnteredIds) {
+ mEnteredIds = null;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action,
+ int flags) {
+ if (sVerbose) {
+ Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds
+ + ", value=" + value + ", action=" + action + ", flags=" + flags);
+ }
+ try {
+ mService.updateSession(mSessionId, id, bounds, value, action, flags,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Tries to add AutofillManagerClient to service if it does not been added. Returns {@code true}
+ * if the AutofillManagerClient is added successfully or is already added. Otherwise,
+ * returns {@code false}.
+ */
+ @GuardedBy("mLock")
+ private boolean tryAddServiceClientIfNeededLocked() {
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return false;
+ }
+ if (mService == null) {
+ Log.w(TAG, "Autofill service is null!");
+ return false;
+ }
+ if (mServiceClient == null) {
+ mServiceClient = new AutofillManagerClient(this);
+ try {
+ final int userId = mContext.getUserId();
+ final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ mService.addClient(mServiceClient, client.autofillClientGetComponentName(),
+ userId, receiver);
+ int flags = 0;
+ try {
+ flags = receiver.getIntResult();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.w(TAG, "Failed to initialize autofill: " + e);
+ // Reset the states initialized above.
+ mService.removeClient(mServiceClient, userId);
+ mServiceClient = null;
+ return false;
+ }
+ mEnabled = (flags & FLAG_ADD_CLIENT_ENABLED) != 0;
+ sDebug = (flags & FLAG_ADD_CLIENT_DEBUG) != 0;
+ sVerbose = (flags & FLAG_ADD_CLIENT_VERBOSE) != 0;
+ mEnabledForAugmentedAutofillOnly = (flags
+ & FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY) != 0;
+ if (sVerbose) {
+ Log.v(TAG, "receiver results: flags=" + flags + " enabled=" + mEnabled
+ + ", enabledForAugmentedOnly: " + mEnabledForAugmentedAutofillOnly);
+ }
+ final IAutoFillManager service = mService;
+ final IAutoFillManagerClient serviceClient = mServiceClient;
+ mServiceClientCleaner = Cleaner.create(this, () -> {
+ // TODO(b/123100811): call service to also remove reference to
+ // mAugmentedAutofillServiceClient
+ try {
+ service.removeClient(serviceClient, userId);
+ } catch (RemoteException e) {
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ @GuardedBy("mLock")
+ private boolean startAutofillIfNeededLocked(View view) {
+ if (mState == STATE_UNKNOWN
+ && mSessionId == NO_SESSION
+ && view instanceof EditText
+ && !TextUtils.isEmpty(((EditText) view).getText())
+ && !view.isFocused()
+ && view.isImportantForAutofill()
+ && view.isLaidOut()
+ && view.isVisibleToUser()) {
+
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+
+ if (sVerbose) {
+ Log.v(TAG, "startAutofillIfNeededLocked(): enabled=" + mEnabled + " mServiceClient="
+ + mServiceClient);
+ }
+ if (clientAdded && mEnabled && !isClientDisablingEnterExitEvent()) {
+ final AutofillId id = view.getAutofillId();
+ final AutofillValue value = view.getAutofillValue();
+ // Starts new session.
+ startSessionLocked(id, /* bounds= */ null, /* value= */ null, /* flags= */ 0);
+ // Updates value.
+ updateSessionLocked(id, /* bounds= */ null, value, ACTION_VALUE_CHANGED,
+ /* flags= */ 0);
+ addEnteredIdLocked(id);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Registers a {@link AutofillCallback} to receive autofill events.
+ *
+ * @param callback callback to receive events.
+ */
+ public void registerCallback(@Nullable AutofillCallback callback) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (callback == null) return;
+
+ final boolean hadCallback = mCallback != null;
+ mCallback = callback;
+
+ if (!hadCallback) {
+ try {
+ mService.setHasCallback(mSessionId, mContext.getUserId(), true);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ }
+
+ /**
+ * Unregisters a {@link AutofillCallback} to receive autofill events.
+ *
+ * @param callback callback to stop receiving events.
+ */
+ public void unregisterCallback(@Nullable AutofillCallback callback) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (callback == null || mCallback == null || callback != mCallback) return;
+
+ mCallback = null;
+
+ try {
+ mService.setHasCallback(mSessionId, mContext.getUserId(), false);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Explicitly limits augmented autofill to the given packages and activities.
+ *
+ * <p>To reset the allowlist, call it passing {@code null} to both arguments.
+ *
+ * <p>Useful when the service wants to restrict augmented autofill to a category of apps, like
+ * apps that uses addresses. For example, if the service wants to support augmented autofill on
+ * all activities of app {@code AddressApp1} and just activities {@code act1} and {@code act2}
+ * of {@code AddressApp2}, it would call:
+ * {@code setAugmentedAutofillWhitelist(Arrays.asList("AddressApp1"),
+ * Arrays.asList(new ComponentName("AddressApp2", "act1"),
+ * new ComponentName("AddressApp2", "act2")));}
+ *
+ * <p><b>Note:</b> This method should only be called by the app providing the augmented autofill
+ * service, and it's ignored if the caller isn't it.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void setAugmentedAutofillWhitelist(@Nullable Set<String> packages,
+ @Nullable Set<ComponentName> activities) {
+ if (!hasAutofillFeature()) {
+ return;
+ }
+
+ final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ int resultCode;
+ try {
+ mService.setAugmentedAutofillWhitelist(toList(packages), toList(activities),
+ resultReceiver);
+ resultCode = resultReceiver.getIntResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.e(TAG, "Fail to get the result of set AugmentedAutofill whitelist. " + e);
+ return;
+ }
+ switch (resultCode) {
+ case RESULT_OK:
+ return;
+ case RESULT_CODE_NOT_SERVICE:
+ throw new SecurityException("caller is not user's Augmented Autofill Service");
+ default:
+ Log.wtf(TAG, "setAugmentedAutofillWhitelist(): received invalid result: "
+ + resultCode);
+ }
+ }
+
+ /**
+ * Notifies that a non-autofillable view was entered because the activity is allowlisted for
+ * augmented autofill.
+ *
+ * <p>This method is necessary to set the right flag on start, so the server-side session
+ * doesn't trigger the standard autofill workflow, but the augmented's instead.
+ *
+ * @hide
+ */
+ public void notifyViewEnteredForAugmentedAutofill(@NonNull View view) {
+ final AutofillId id = view.getAutofillId();
+ synchronized (mLock) {
+ if (mEnteredForAugmentedAutofillIds == null) {
+ mEnteredForAugmentedAutofillIds = new ArraySet<>(1);
+ }
+ mEnteredForAugmentedAutofillIds.add(id);
+ }
+ }
+
+ private void requestShowFillUi(int sessionId, AutofillId id, int width, int height,
+ Rect anchorBounds, IAutofillWindowPresenter presenter) {
+ final View anchor = findView(id);
+ if (anchor == null) {
+ return;
+ }
+
+ AutofillCallback callback = null;
+ synchronized (mLock) {
+ if (mSessionId == sessionId) {
+ AutofillClient client = getClient();
+
+ if (client != null) {
+ if (client.autofillClientRequestShowFillUi(anchor, width, height,
+ anchorBounds, presenter)) {
+ callback = mCallback;
+ mIdShownFillUi = id;
+ }
+ }
+ }
+ }
+
+ if (callback != null) {
+ if (id.isVirtualInt()) {
+ callback.onAutofillEvent(anchor, id.getVirtualChildIntId(),
+ AutofillCallback.EVENT_INPUT_SHOWN);
+ } else {
+ callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_SHOWN);
+ }
+ }
+ }
+
+ private void authenticate(int sessionId, int authenticationId, IntentSender intent,
+ Intent fillInIntent, boolean authenticateInline) {
+ synchronized (mLock) {
+ if (sessionId == mSessionId) {
+ final AutofillClient client = getClient();
+ if (client != null) {
+ // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
+ // before onAuthenticationResult()
+ mOnInvisibleCalled = false;
+ client.autofillClientAuthenticate(authenticationId, intent, fillInIntent,
+ authenticateInline);
+ }
+ }
+ }
+ }
+
+ private void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent keyEvent) {
+ final View anchor = findView(id);
+ if (anchor == null) {
+ return;
+ }
+
+ synchronized (mLock) {
+ if (mSessionId == sessionId) {
+ AutofillClient client = getClient();
+
+ if (client != null) {
+ client.autofillClientDispatchUnhandledKey(anchor, keyEvent);
+ }
+ }
+ }
+ }
+
+ /** @hide */
+ public static final int SET_STATE_FLAG_ENABLED = 0x01;
+ /** @hide */
+ public static final int SET_STATE_FLAG_RESET_SESSION = 0x02;
+ /** @hide */
+ public static final int SET_STATE_FLAG_RESET_CLIENT = 0x04;
+ /** @hide */
+ public static final int SET_STATE_FLAG_DEBUG = 0x08;
+ /** @hide */
+ public static final int SET_STATE_FLAG_VERBOSE = 0x10;
+ /** @hide */
+ public static final int SET_STATE_FLAG_FOR_AUTOFILL_ONLY = 0x20;
+
+ private void setState(int flags) {
+ if (sVerbose) {
+ Log.v(TAG, "setState(" + flags + ": " + DebugUtils.flagsToString(AutofillManager.class,
+ "SET_STATE_FLAG_", flags) + ")");
+ }
+ synchronized (mLock) {
+ if ((flags & SET_STATE_FLAG_FOR_AUTOFILL_ONLY) != 0) {
+ mForAugmentedAutofillOnly = true;
+ // NOTE: returning right away as this is the only flag set, at least currently...
+ return;
+ }
+ mEnabled = (flags & SET_STATE_FLAG_ENABLED) != 0;
+ if (!mEnabled || (flags & SET_STATE_FLAG_RESET_SESSION) != 0) {
+ // Reset the session state
+ resetSessionLocked(/* resetEnteredIds= */ true);
+ }
+ if ((flags & SET_STATE_FLAG_RESET_CLIENT) != 0) {
+ // Reset connection to system
+ mServiceClient = null;
+ mAugmentedAutofillServiceClient = null;
+ if (mServiceClientCleaner != null) {
+ mServiceClientCleaner.clean();
+ mServiceClientCleaner = null;
+ }
+ notifyReenableAutofill();
+ }
+ }
+ sDebug = (flags & SET_STATE_FLAG_DEBUG) != 0;
+ sVerbose = (flags & SET_STATE_FLAG_VERBOSE) != 0;
+ }
+
+ /**
+ * Sets a view as autofilled if the current value is the {code targetValue}.
+ *
+ * @param view The view that is to be autofilled
+ * @param targetValue The value we want to fill into view
+ */
+ private void setAutofilledIfValuesIs(@NonNull View view, @Nullable AutofillValue targetValue,
+ boolean hideHighlight) {
+ AutofillValue currentValue = view.getAutofillValue();
+ if (Objects.equals(currentValue, targetValue)) {
+ synchronized (mLock) {
+ if (mLastAutofilledData == null) {
+ mLastAutofilledData = new ParcelableMap(1);
+ }
+ mLastAutofilledData.put(view.getAutofillId(), targetValue);
+ }
+ view.setAutofilled(true, hideHighlight);
+ }
+ }
+
+ private void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values,
+ boolean hideHighlight) {
+ synchronized (mLock) {
+ if (sessionId != mSessionId) {
+ return;
+ }
+
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return;
+ }
+
+ final int itemCount = ids.size();
+ int numApplied = 0;
+ ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
+ final View[] views = client.autofillClientFindViewsByAutofillIdTraversal(
+ Helper.toArray(ids));
+
+ ArrayList<AutofillId> failedIds = null;
+
+ for (int i = 0; i < itemCount; i++) {
+ final AutofillId id = ids.get(i);
+ final AutofillValue value = values.get(i);
+ final View view = views[i];
+ if (view == null) {
+ // Most likely view has been removed after the initial request was sent to the
+ // the service; this is fine, but we need to update the view status in the
+ // server side so it can be triggered again.
+ Log.d(TAG, "autofill(): no View with id " + id);
+ if (failedIds == null) {
+ failedIds = new ArrayList<>();
+ }
+ failedIds.add(id);
+ continue;
+ }
+ if (id.isVirtualInt()) {
+ if (virtualValues == null) {
+ // Most likely there will be just one view with virtual children.
+ virtualValues = new ArrayMap<>(1);
+ }
+ SparseArray<AutofillValue> valuesByParent = virtualValues.get(view);
+ if (valuesByParent == null) {
+ // We don't know the size yet, but usually it will be just a few fields...
+ valuesByParent = new SparseArray<>(5);
+ virtualValues.put(view, valuesByParent);
+ }
+ valuesByParent.put(id.getVirtualChildIntId(), value);
+ } else {
+ // Mark the view as to be autofilled with 'value'
+ if (mLastAutofilledData == null) {
+ mLastAutofilledData = new ParcelableMap(itemCount - i);
+ }
+ mLastAutofilledData.put(id, value);
+
+ view.autofill(value);
+
+ // Set as autofilled if the values match now, e.g. when the value was updated
+ // synchronously.
+ // If autofill happens async, the view is set to autofilled in
+ // notifyValueChanged.
+ setAutofilledIfValuesIs(view, value, hideHighlight);
+
+ numApplied++;
+ }
+ }
+
+ if (failedIds != null) {
+ if (sVerbose) {
+ Log.v(TAG, "autofill(): total failed views: " + failedIds);
+ }
+ try {
+ mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId());
+ } catch (RemoteException e) {
+ // In theory, we could ignore this error since it's not a big deal, but
+ // in reality, we rather crash the app anyways, as the failure could be
+ // a consequence of something going wrong on the server side...
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ if (virtualValues != null) {
+ for (int i = 0; i < virtualValues.size(); i++) {
+ final View parent = virtualValues.keyAt(i);
+ final SparseArray<AutofillValue> childrenValues = virtualValues.valueAt(i);
+ parent.autofill(childrenValues);
+ numApplied += childrenValues.size();
+ // TODO: we should provide a callback so the parent can call failures; something
+ // like notifyAutofillFailed(View view, int[] childrenIds);
+ }
+ }
+
+ mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_DATASET_APPLIED)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied));
+ }
+ }
+
+ private void autofillContent(int sessionId, AutofillId id, ClipData clip) {
+ synchronized (mLock) {
+ if (sessionId != mSessionId) {
+ return;
+ }
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return;
+ }
+ final View view = client.autofillClientFindViewByAutofillIdTraversal(id);
+ if (view == null) {
+ // Most likely view has been removed after the initial request was sent to the
+ // the service; this is fine, but we need to update the view status in the
+ // server side so it can be triggered again.
+ Log.d(TAG, "autofillContent(): no view with id " + id);
+ reportAutofillContentFailure(id);
+ return;
+ }
+ ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_AUTOFILL).build();
+ ContentInfo result = view.performReceiveContent(payload);
+ if (result != null) {
+ Log.w(TAG, "autofillContent(): receiver could not insert content: id=" + id
+ + ", view=" + view + ", clip=" + clip);
+ reportAutofillContentFailure(id);
+ return;
+ }
+ mMetricsLogger.write(newLog(MetricsEvent.AUTOFILL_DATASET_APPLIED)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, 1)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, 1));
+ }
+ }
+
+ private void reportAutofillContentFailure(AutofillId id) {
+ try {
+ mService.setAutofillFailure(mSessionId, Collections.singletonList(id),
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private LogMaker newLog(int category) {
+ final LogMaker log = new LogMaker(category)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, mSessionId);
+
+ if (isCompatibilityModeEnabledLocked()) {
+ log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE, 1);
+ }
+ final AutofillClient client = getClient();
+ if (client == null) {
+ // Client should never be null here, but it doesn't hurt to check...
+ log.setPackageName(mContext.getPackageName());
+ } else {
+ // Remove activity name from logging
+ final ComponentName sanitizedComponentName =
+ new ComponentName(client.autofillClientGetComponentName().getPackageName(), "");
+ log.setComponentName(sanitizedComponentName);
+ }
+ return log;
+ }
+
+ /**
+ * Set the tracked views.
+ *
+ * @param trackedIds The views to be tracked.
+ * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible.
+ * @param saveOnFinish Finish the session once the activity is finished.
+ * @param fillableIds Views that might anchor FillUI.
+ * @param saveTriggerId View that when clicked triggers commit().
+ */
+ private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds,
+ boolean saveOnAllViewsInvisible, boolean saveOnFinish,
+ @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) {
+ if (saveTriggerId != null) {
+ saveTriggerId.resetSessionId();
+ }
+ synchronized (mLock) {
+ if (sVerbose) {
+ Log.v(TAG, "setTrackedViews(): sessionId=" + sessionId
+ + ", trackedIds=" + Arrays.toString(trackedIds)
+ + ", saveOnAllViewsInvisible=" + saveOnAllViewsInvisible
+ + ", saveOnFinish=" + saveOnFinish
+ + ", fillableIds=" + Arrays.toString(fillableIds)
+ + ", saveTrigerId=" + saveTriggerId
+ + ", mFillableIds=" + mFillableIds
+ + ", mEnabled=" + mEnabled
+ + ", mSessionId=" + mSessionId);
+
+ }
+ if (mEnabled && mSessionId == sessionId) {
+ if (saveOnAllViewsInvisible) {
+ mTrackedViews = new TrackedViews(trackedIds);
+ } else {
+ mTrackedViews = null;
+ }
+ mSaveOnFinish = saveOnFinish;
+ if (fillableIds != null) {
+ if (mFillableIds == null) {
+ mFillableIds = new ArraySet<>(fillableIds.length);
+ }
+ for (AutofillId id : fillableIds) {
+ id.resetSessionId();
+ mFillableIds.add(id);
+ }
+ }
+
+ if (mSaveTriggerId != null && !mSaveTriggerId.equals(saveTriggerId)) {
+ // Turn off trigger on previous view id.
+ setNotifyOnClickLocked(mSaveTriggerId, false);
+ }
+
+ if (saveTriggerId != null && !saveTriggerId.equals(mSaveTriggerId)) {
+ // Turn on trigger on new view id.
+ mSaveTriggerId = saveTriggerId;
+ setNotifyOnClickLocked(mSaveTriggerId, true);
+ }
+ }
+ }
+ }
+
+ private void setNotifyOnClickLocked(@NonNull AutofillId id, boolean notify) {
+ final View view = findView(id);
+ if (view == null) {
+ Log.w(TAG, "setNotifyOnClick(): invalid id: " + id);
+ return;
+ }
+ view.setNotifyAutofillManagerOnClick(notify);
+ }
+
+ private void setSaveUiState(int sessionId, boolean shown) {
+ if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown);
+ synchronized (mLock) {
+ if (mSessionId != NO_SESSION) {
+ // Race condition: app triggered a new session after the previous session was
+ // finished but before server called setSaveUiState() - need to cancel the new
+ // session to avoid further inconsistent behavior.
+ Log.w(TAG, "setSaveUiState(" + sessionId + ", " + shown
+ + ") called on existing session " + mSessionId + "; cancelling it");
+ cancelSessionLocked();
+ }
+ if (shown) {
+ mSessionId = sessionId;
+ mState = STATE_SHOWING_SAVE_UI;
+ } else {
+ mSessionId = NO_SESSION;
+ mState = STATE_UNKNOWN;
+ }
+ }
+ }
+
+ /**
+ * Marks the state of the session as finished.
+ *
+ * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null}
+ * FillResponse), {@link #STATE_UNKNOWN} (because the session was removed),
+ * {@link #STATE_UNKNOWN_COMPAT_MODE} (beucase the session was finished when the URL bar
+ * changed on compat mode), {@link #STATE_UNKNOWN_FAILED} (because the session was finished
+ * when the service failed to fullfil the request, or {@link #STATE_DISABLED_BY_SERVICE}
+ * (because the autofill service disabled further autofill requests for the activity).
+ * @param autofillableIds list of ids that could trigger autofill, use to not handle a new
+ * session when they're entered.
+ */
+ private void setSessionFinished(int newState, @Nullable List<AutofillId> autofillableIds) {
+ if (autofillableIds != null) {
+ for (int i = 0; i < autofillableIds.size(); i++) {
+ autofillableIds.get(i).resetSessionId();
+ }
+ }
+ synchronized (mLock) {
+ if (sVerbose) {
+ Log.v(TAG, "setSessionFinished(): from " + getStateAsStringLocked() + " to "
+ + getStateAsString(newState) + "; autofillableIds=" + autofillableIds);
+ }
+ if (autofillableIds != null) {
+ mEnteredIds = new ArraySet<>(autofillableIds);
+ }
+ if (newState == STATE_UNKNOWN_COMPAT_MODE || newState == STATE_UNKNOWN_FAILED) {
+ resetSessionLocked(/* resetEnteredIds= */ true);
+ mState = STATE_UNKNOWN;
+ } else {
+ resetSessionLocked(/* resetEnteredIds= */ false);
+ mState = newState;
+ }
+ }
+ }
+
+ /**
+ * Gets a {@link AugmentedAutofillManagerClient} for this {@link AutofillManagerClient}.
+ *
+ * <p>These are 2 distinct objects because we need to restrict what the Augmented Autofill
+ * service can do (which is defined by {@code IAugmentedAutofillManagerClient.aidl}).
+ */
+ private void getAugmentedAutofillClient(@NonNull IResultReceiver result) {
+ synchronized (mLock) {
+ if (mAugmentedAutofillServiceClient == null) {
+ mAugmentedAutofillServiceClient = new AugmentedAutofillManagerClient(this);
+ }
+ final Bundle resultData = new Bundle();
+ resultData.putBinder(EXTRA_AUGMENTED_AUTOFILL_CLIENT,
+ mAugmentedAutofillServiceClient.asBinder());
+
+ try {
+ result.send(0, resultData);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Could not send AugmentedAutofillClient back: " + e);
+ }
+ }
+ }
+
+ private void requestShowSoftInput(@NonNull AutofillId id) {
+ if (sVerbose) Log.v(TAG, "requestShowSoftInput(" + id + ")");
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return;
+ }
+ final View view = client.autofillClientFindViewByAutofillIdTraversal(id);
+ if (view == null) {
+ if (sVerbose) Log.v(TAG, "View is not found");
+ return;
+ }
+ final Handler handler = view.getHandler();
+ if (handler == null) {
+ if (sVerbose) Log.v(TAG, "Ignoring requestShowSoftInput due to no handler in view");
+ return;
+ }
+ if (handler.getLooper() != Looper.myLooper()) {
+ // The view is running on a different thread than our own, so we need to reschedule
+ // our work for over there.
+ if (sVerbose) Log.v(TAG, "Scheduling showSoftInput() on the view UI thread");
+ handler.post(() -> requestShowSoftInputInViewThread(view));
+ } else {
+ requestShowSoftInputInViewThread(view);
+ }
+ }
+
+ // This method must be called from within the View thread.
+ private static void requestShowSoftInputInViewThread(@NonNull View view) {
+ if (!view.isFocused()) {
+ Log.w(TAG, "Ignoring requestShowSoftInput() due to non-focused view");
+ return;
+ }
+ final InputMethodManager inputMethodManager = view.getContext().getSystemService(
+ InputMethodManager.class);
+ boolean ret = inputMethodManager.showSoftInput(view, /*flags=*/ 0);
+ if (sVerbose) Log.v(TAG, " InputMethodManager.showSoftInput returns " + ret);
+ }
+
+ /** @hide */
+ public void requestHideFillUi() {
+ requestHideFillUi(mIdShownFillUi, true);
+ }
+
+ private void requestHideFillUi(AutofillId id, boolean force) {
+ final View anchor = id == null ? null : findView(id);
+ if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor);
+ if (anchor == null) {
+ if (force) {
+ // When user taps outside autofill window, force to close fill ui even id does
+ // not match.
+ AutofillClient client = getClient();
+ if (client != null) {
+ client.autofillClientRequestHideFillUi();
+ }
+ }
+ return;
+ }
+ requestHideFillUi(id, anchor);
+ }
+
+ private void requestHideFillUi(AutofillId id, View anchor) {
+
+ AutofillCallback callback = null;
+ synchronized (mLock) {
+ // We do not check the session id for two reasons:
+ // 1. If local and remote session id are off sync the UI would be stuck shown
+ // 2. There is a race between the user state being destroyed due the fill
+ // service being uninstalled and the UI being dismissed.
+ AutofillClient client = getClient();
+ if (client != null) {
+ if (client.autofillClientRequestHideFillUi()) {
+ mIdShownFillUi = null;
+ callback = mCallback;
+ }
+ }
+ }
+
+ if (callback != null) {
+ if (id.isVirtualInt()) {
+ callback.onAutofillEvent(anchor, id.getVirtualChildIntId(),
+ AutofillCallback.EVENT_INPUT_HIDDEN);
+ } else {
+ callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_HIDDEN);
+ }
+ }
+ }
+
+ private void notifyDisableAutofill(long disableDuration, ComponentName componentName) {
+ synchronized (mLock) {
+ if (mOptions == null) {
+ return;
+ }
+ long expiration = SystemClock.elapsedRealtime() + disableDuration;
+ // Protect it against overflow
+ if (expiration < 0) {
+ expiration = Long.MAX_VALUE;
+ }
+ if (componentName != null) {
+ if (mOptions.disabledActivities == null) {
+ mOptions.disabledActivities = new ArrayMap<>();
+ }
+ mOptions.disabledActivities.put(componentName.flattenToString(), expiration);
+ } else {
+ mOptions.appDisabledExpiration = expiration;
+ }
+ }
+ }
+
+ void notifyReenableAutofill() {
+ synchronized (mLock) {
+ if (mOptions == null) {
+ return;
+ }
+ mOptions.appDisabledExpiration = 0;
+ mOptions.disabledActivities = null;
+ }
+ }
+
+ private void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) {
+ if (sVerbose) {
+ Log.v(TAG, "notifyNoFillUi(): sessionFinishedState=" + sessionFinishedState);
+ }
+ final View anchor = findView(id);
+ if (anchor == null) {
+ return;
+ }
+
+ notifyCallback(sessionId, id, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+
+ if (sessionFinishedState != STATE_UNKNOWN) {
+ // Callback call was "hijacked" to also update the session state.
+ setSessionFinished(sessionFinishedState, /* autofillableIds= */ null);
+ }
+ }
+
+ private void notifyCallback(
+ int sessionId, AutofillId id, @AutofillCallback.AutofillEventType int event) {
+ if (sVerbose) {
+ Log.v(TAG, "notifyCallback(): sessionId=" + sessionId + ", autofillId=" + id
+ + ", event=" + event);
+ }
+ final View anchor = findView(id);
+ if (anchor == null) {
+ return;
+ }
+
+ AutofillCallback callback = null;
+ synchronized (mLock) {
+ if (mSessionId == sessionId && getClient() != null) {
+ callback = mCallback;
+ }
+ }
+
+ if (callback != null) {
+ if (id.isVirtualInt()) {
+ callback.onAutofillEvent(
+ anchor, id.getVirtualChildIntId(), event);
+ } else {
+ callback.onAutofillEvent(anchor, event);
+ }
+ }
+ }
+
+ /**
+ * Find a single view by its id.
+ *
+ * @param autofillId The autofill id of the view
+ *
+ * @return The view or {@code null} if view was not found
+ */
+ private View findView(@NonNull AutofillId autofillId) {
+ final AutofillClient client = getClient();
+ if (client != null) {
+ return client.autofillClientFindViewByAutofillIdTraversal(autofillId);
+ }
+ return null;
+ }
+
+ /** @hide */
+ public boolean hasAutofillFeature() {
+ return mService != null;
+ }
+
+ /** @hide */
+ public void onPendingSaveUi(int operation, IBinder token) {
+ if (sVerbose) Log.v(TAG, "onPendingSaveUi(" + operation + "): " + token);
+
+ synchronized (mLock) {
+ try {
+ mService.onPendingSaveUi(operation, token);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in onPendingSaveUi: ", e);
+ }
+ }
+ }
+
+ /** @hide */
+ public void dump(String outerPrefix, PrintWriter pw) {
+ 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(getStateAsStringLocked());
+ pw.print(pfx); pw.print("context: "); pw.println(mContext);
+ pw.print(pfx); pw.print("service client: "); pw.println(mServiceClient);
+ final AutofillClient client = getClient();
+ if (client != null) {
+ pw.print(pfx); pw.print("client: "); pw.print(client);
+ pw.print(" ("); pw.print(client.autofillClientGetActivityToken()); pw.println(')');
+ }
+ pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled);
+ pw.print(pfx); pw.print("enabledAugmentedOnly: "); pw.println(mForAugmentedAutofillOnly);
+ pw.print(pfx); pw.print("hasService: "); pw.println(mService != null);
+ pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null);
+ pw.print(pfx); pw.print("onInvisibleCalled "); pw.println(mOnInvisibleCalled);
+ pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData);
+ pw.print(pfx); pw.print("id of last fill UI shown: "); pw.println(mIdShownFillUi);
+ pw.print(pfx); pw.print("tracked views: ");
+ if (mTrackedViews == null) {
+ pw.println("null");
+ } else {
+ final String pfx2 = pfx + " ";
+ pw.println();
+ pw.print(pfx2); pw.print("visible:"); pw.println(mTrackedViews.mVisibleTrackedIds);
+ pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds);
+ }
+ pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds);
+ pw.print(pfx); pw.print("entered ids: "); pw.println(mEnteredIds);
+ if (mEnteredForAugmentedAutofillIds != null) {
+ pw.print(pfx); pw.print("entered ids for augmented autofill: ");
+ pw.println(mEnteredForAugmentedAutofillIds);
+ }
+ if (mForAugmentedAutofillOnly) {
+ pw.print(pfx); pw.println("For Augmented Autofill Only");
+ }
+ pw.print(pfx); pw.print("save trigger id: "); pw.println(mSaveTriggerId);
+ pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish);
+ if (mOptions != null) {
+ pw.print(pfx); pw.print("options: "); mOptions.dumpShort(pw); pw.println();
+ }
+ pw.print(pfx); pw.print("compat mode enabled: ");
+ synchronized (mLock) {
+ if (mCompatibilityBridge != null) {
+ final String pfx2 = pfx + " ";
+ pw.println("true");
+ pw.print(pfx2); pw.print("windowId: ");
+ pw.println(mCompatibilityBridge.mFocusedWindowId);
+ pw.print(pfx2); pw.print("nodeId: ");
+ pw.println(mCompatibilityBridge.mFocusedNodeId);
+ pw.print(pfx2); pw.print("virtualId: ");
+ pw.println(AccessibilityNodeInfo
+ .getVirtualDescendantId(mCompatibilityBridge.mFocusedNodeId));
+ pw.print(pfx2); pw.print("focusedBounds: ");
+ pw.println(mCompatibilityBridge.mFocusedBounds);
+ } else {
+ pw.println("false");
+ }
+ }
+ pw.print(pfx); pw.print("debug: "); pw.print(sDebug);
+ pw.print(" verbose: "); pw.println(sVerbose);
+ }
+
+ @GuardedBy("mLock")
+ private String getStateAsStringLocked() {
+ return getStateAsString(mState);
+ }
+
+ @NonNull
+ private static String getStateAsString(int state) {
+ switch (state) {
+ case STATE_UNKNOWN:
+ return "UNKNOWN";
+ case STATE_ACTIVE:
+ return "ACTIVE";
+ case STATE_FINISHED:
+ return "FINISHED";
+ case STATE_SHOWING_SAVE_UI:
+ return "SHOWING_SAVE_UI";
+ case STATE_DISABLED_BY_SERVICE:
+ return "DISABLED_BY_SERVICE";
+ case STATE_UNKNOWN_COMPAT_MODE:
+ return "UNKNOWN_COMPAT_MODE";
+ case STATE_UNKNOWN_FAILED:
+ return "UNKNOWN_FAILED";
+ default:
+ return "INVALID:" + state;
+ }
+ }
+
+ /** @hide */
+ public static String getSmartSuggestionModeToString(@SmartSuggestionMode int flags) {
+ switch (flags) {
+ case FLAG_SMART_SUGGESTION_OFF:
+ return "OFF";
+ case FLAG_SMART_SUGGESTION_SYSTEM:
+ return "SYSTEM";
+ default:
+ return "INVALID:" + flags;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean isActiveLocked() {
+ return mState == STATE_ACTIVE;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isDisabledByServiceLocked() {
+ return mState == STATE_DISABLED_BY_SERVICE;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isFinishedLocked() {
+ return mState == STATE_FINISHED;
+ }
+
+ private void post(Runnable runnable) {
+ final AutofillClient client = getClient();
+ if (client == null) {
+ if (sVerbose) Log.v(TAG, "ignoring post() because client is null");
+ return;
+ }
+ client.autofillClientRunOnUiThread(runnable);
+ }
+
+ /**
+ * Implementation of the accessibility based compatibility.
+ */
+ private final class CompatibilityBridge implements AccessibilityManager.AccessibilityPolicy {
+ @GuardedBy("mLock")
+ private final Rect mFocusedBounds = new Rect();
+ @GuardedBy("mLock")
+ private final Rect mTempBounds = new Rect();
+
+ @GuardedBy("mLock")
+ private int mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ @GuardedBy("mLock")
+ private long mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+
+ // Need to report a fake service in case a11y clients check the service list
+ @NonNull
+ @GuardedBy("mLock")
+ AccessibilityServiceInfo mCompatServiceInfo;
+
+ CompatibilityBridge() {
+ final AccessibilityManager am = AccessibilityManager.getInstance(mContext);
+ am.setAccessibilityPolicy(this);
+ }
+
+ private AccessibilityServiceInfo getCompatServiceInfo() {
+ synchronized (mLock) {
+ if (mCompatServiceInfo != null) {
+ return mCompatServiceInfo;
+ }
+ final Intent intent = new Intent();
+ intent.setComponent(new ComponentName("android",
+ "com.android.server.autofill.AutofillCompatAccessibilityService"));
+ final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(
+ intent, PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
+ try {
+ mCompatServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Cannot find compat autofill service:" + intent);
+ throw new IllegalStateException("Cannot find compat autofill service");
+ }
+ return mCompatServiceInfo;
+ }
+ }
+
+ @Override
+ public boolean isEnabled(boolean accessibilityEnabled) {
+ return true;
+ }
+
+ @Override
+ public int getRelevantEventTypes(int relevantEventTypes) {
+ return relevantEventTypes | AccessibilityEvent.TYPE_VIEW_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_CLICKED
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
+ }
+
+ @Override
+ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
+ List<AccessibilityServiceInfo> installedServices) {
+ if (installedServices == null) {
+ installedServices = new ArrayList<>();
+ }
+ installedServices.add(getCompatServiceInfo());
+ return installedServices;
+ }
+
+ @Override
+ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
+ int feedbackTypeFlags, List<AccessibilityServiceInfo> enabledService) {
+ if (enabledService == null) {
+ enabledService = new ArrayList<>();
+ }
+ enabledService.add(getCompatServiceInfo());
+ return enabledService;
+ }
+
+ @Override
+ public AccessibilityEvent onAccessibilityEvent(AccessibilityEvent event,
+ boolean accessibilityEnabled, int relevantEventTypes) {
+ final int type = event.getEventType();
+ if (sVerbose) {
+ // NOTE: this is waaay spammy, but that's life.
+ Log.v(TAG, "onAccessibilityEvent(" + AccessibilityEvent.eventTypeToString(type)
+ + "): virtualId="
+ + AccessibilityNodeInfo.getVirtualDescendantId(event.getSourceNodeId())
+ + ", client=" + getClient());
+ }
+ switch (type) {
+ case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
+ synchronized (mLock) {
+ if (mFocusedWindowId == event.getWindowId()
+ && mFocusedNodeId == event.getSourceNodeId()) {
+ return event;
+ }
+ if (mFocusedWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
+ && mFocusedNodeId != AccessibilityNodeInfo.UNDEFINED_NODE_ID) {
+ notifyViewExited(mFocusedWindowId, mFocusedNodeId);
+ mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+ mFocusedBounds.set(0, 0, 0, 0);
+ }
+ final int windowId = event.getWindowId();
+ final long nodeId = event.getSourceNodeId();
+ if (notifyViewEntered(windowId, nodeId, mFocusedBounds)) {
+ mFocusedWindowId = windowId;
+ mFocusedNodeId = nodeId;
+ }
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: {
+ synchronized (mLock) {
+ if (mFocusedWindowId == event.getWindowId()
+ && mFocusedNodeId == event.getSourceNodeId()) {
+ notifyValueChanged(event.getWindowId(), event.getSourceNodeId());
+ }
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_CLICKED: {
+ synchronized (mLock) {
+ notifyViewClicked(event.getWindowId(), event.getSourceNodeId());
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
+ final AutofillClient client = getClient();
+ if (client != null) {
+ synchronized (mLock) {
+ if (client.autofillClientIsFillUiShowing()) {
+ notifyViewEntered(mFocusedWindowId, mFocusedNodeId, mFocusedBounds);
+ }
+ updateTrackedViewsLocked();
+ }
+ }
+ } break;
+ }
+
+ return accessibilityEnabled ? event : null;
+ }
+
+ private boolean notifyViewEntered(int windowId, long nodeId, Rect focusedBounds) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return false;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return false;
+ }
+ final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
+ if (node == null) {
+ return false;
+ }
+ if (!node.isEditable()) {
+ return false;
+ }
+ final Rect newBounds = mTempBounds;
+ node.getBoundsInScreen(newBounds);
+ if (newBounds.equals(focusedBounds)) {
+ return false;
+ }
+ focusedBounds.set(newBounds);
+ AutofillManager.this.notifyViewEntered(view, virtualId, newBounds);
+ return true;
+ }
+
+ private void notifyViewExited(int windowId, long nodeId) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return;
+ }
+ AutofillManager.this.notifyViewExited(view, virtualId);
+ }
+
+ private void notifyValueChanged(int windowId, long nodeId) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return;
+ }
+ final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
+ if (node == null) {
+ return;
+ }
+ AutofillManager.this.notifyValueChanged(view, virtualId,
+ AutofillValue.forText(node.getText()));
+ }
+
+ private void notifyViewClicked(int windowId, long nodeId) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return;
+ }
+ final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
+ if (node == null) {
+ return;
+ }
+ AutofillManager.this.notifyViewClicked(view, virtualId);
+ }
+
+ @GuardedBy("mLock")
+ private void updateTrackedViewsLocked() {
+ if (mTrackedViews != null) {
+ mTrackedViews.onVisibleForAutofillChangedLocked();
+ }
+ }
+
+ private View findViewByAccessibilityId(int windowId, long nodeId) {
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return null;
+ }
+ final int viewId = AccessibilityNodeInfo.getAccessibilityViewId(nodeId);
+ return client.autofillClientFindViewByAccessibilityIdTraversal(viewId, windowId);
+ }
+
+ private AccessibilityNodeInfo findVirtualNodeByAccessibilityId(View view, int virtualId) {
+ final AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
+ if (provider == null) {
+ return null;
+ }
+ return provider.createAccessibilityNodeInfo(virtualId);
+ }
+
+ private boolean isVirtualNode(int nodeId) {
+ return nodeId != AccessibilityNodeProvider.HOST_VIEW_ID
+ && nodeId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ }
+ }
+
+ /**
+ * View tracking information. Once all tracked views become invisible the session is finished.
+ */
+ private class TrackedViews {
+ /** Visible tracked views */
+ @Nullable private ArraySet<AutofillId> mVisibleTrackedIds;
+
+ /** Invisible tracked views */
+ @Nullable private ArraySet<AutofillId> mInvisibleTrackedIds;
+
+ /**
+ * Check if set is null or value is in set.
+ *
+ * @param set The set or null (== empty set)
+ * @param value The value that might be in the set
+ *
+ * @return {@code true} iff set is not empty and value is in set
+ */
+ // TODO: move to Helper as static method
+ private <T> boolean isInSet(@Nullable ArraySet<T> set, T value) {
+ return set != null && set.contains(value);
+ }
+
+ /**
+ * Add a value to a set. If set is null, create a new set.
+ *
+ * @param set The set or null (== empty set)
+ * @param valueToAdd The value to add
+ *
+ * @return The set including the new value. If set was {@code null}, a set containing only
+ * the new value.
+ */
+ // TODO: move to Helper as static method
+ @NonNull
+ private <T> ArraySet<T> addToSet(@Nullable ArraySet<T> set, T valueToAdd) {
+ if (set == null) {
+ set = new ArraySet<>(1);
+ }
+
+ set.add(valueToAdd);
+
+ return set;
+ }
+
+ /**
+ * Remove a value from a set.
+ *
+ * @param set The set or null (== empty set)
+ * @param valueToRemove The value to remove
+ *
+ * @return The set without the removed value. {@code null} if set was null, or is empty
+ * after removal.
+ */
+ // TODO: move to Helper as static method
+ @Nullable
+ private <T> ArraySet<T> removeFromSet(@Nullable ArraySet<T> set, T valueToRemove) {
+ if (set == null) {
+ return null;
+ }
+
+ set.remove(valueToRemove);
+
+ if (set.isEmpty()) {
+ return null;
+ }
+
+ return set;
+ }
+
+ /**
+ * Set the tracked views.
+ *
+ * @param trackedIds The views to be tracked
+ */
+ TrackedViews(@Nullable AutofillId[] trackedIds) {
+ final AutofillClient client = getClient();
+ if (!ArrayUtils.isEmpty(trackedIds) && client != null) {
+ final boolean[] isVisible;
+
+ if (client.autofillClientIsVisibleForAutofill()) {
+ if (sVerbose) Log.v(TAG, "client is visible, check tracked ids");
+ isVisible = client.autofillClientGetViewVisibility(trackedIds);
+ } else {
+ // All false
+ isVisible = new boolean[trackedIds.length];
+ }
+
+ final int numIds = trackedIds.length;
+ for (int i = 0; i < numIds; i++) {
+ final AutofillId id = trackedIds[i];
+ id.resetSessionId();
+
+ if (isVisible[i]) {
+ mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
+ } else {
+ mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+ }
+ }
+ }
+
+ if (sVerbose) {
+ Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(trackedIds) + "): "
+ + " mVisibleTrackedIds=" + mVisibleTrackedIds
+ + " mInvisibleTrackedIds=" + mInvisibleTrackedIds);
+ }
+
+ if (mVisibleTrackedIds == null) {
+ finishSessionLocked();
+ }
+ }
+
+ /**
+ * Called when a {@link View view's} visibility changes.
+ *
+ * @param id the id of the view/virtual view whose visibility changed.
+ * @param isVisible visible if the view is visible in the view hierarchy.
+ */
+ @GuardedBy("mLock")
+ void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) {
+ if (sDebug) {
+ Log.d(TAG, "notifyViewVisibilityChangedLocked(): id=" + id + " isVisible="
+ + isVisible);
+ }
+
+ if (isClientVisibleForAutofillLocked()) {
+ if (isVisible) {
+ if (isInSet(mInvisibleTrackedIds, id)) {
+ mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
+ mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
+ }
+ } else {
+ if (isInSet(mVisibleTrackedIds, id)) {
+ mVisibleTrackedIds = removeFromSet(mVisibleTrackedIds, id);
+ mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+ }
+ }
+ }
+
+ if (mVisibleTrackedIds == null) {
+ if (sVerbose) {
+ Log.v(TAG, "No more visible ids. Invisibile = " + mInvisibleTrackedIds);
+ }
+ finishSessionLocked();
+ }
+ }
+
+ /**
+ * Called once the client becomes visible.
+ *
+ * @see AutofillClient#autofillClientIsVisibleForAutofill()
+ */
+ @GuardedBy("mLock")
+ void onVisibleForAutofillChangedLocked() {
+ // The visibility of the views might have changed while the client was not be visible,
+ // hence update the visibility state for all views.
+ AutofillClient client = getClient();
+ ArraySet<AutofillId> updatedVisibleTrackedIds = null;
+ ArraySet<AutofillId> updatedInvisibleTrackedIds = null;
+ if (client != null) {
+ if (sVerbose) {
+ Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds
+ + " vis=" + mVisibleTrackedIds);
+ }
+ if (mInvisibleTrackedIds != null) {
+ final ArrayList<AutofillId> orderedInvisibleIds =
+ new ArrayList<>(mInvisibleTrackedIds);
+ final boolean[] isVisible = client.autofillClientGetViewVisibility(
+ Helper.toArray(orderedInvisibleIds));
+
+ final int numInvisibleTrackedIds = orderedInvisibleIds.size();
+ for (int i = 0; i < numInvisibleTrackedIds; i++) {
+ final AutofillId id = orderedInvisibleIds.get(i);
+ if (isVisible[i]) {
+ updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
+
+ if (sDebug) {
+ Log.d(TAG, "onVisibleForAutofill() " + id + " became visible");
+ }
+ } else {
+ updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
+ }
+ }
+ }
+
+ if (mVisibleTrackedIds != null) {
+ final ArrayList<AutofillId> orderedVisibleIds =
+ new ArrayList<>(mVisibleTrackedIds);
+ final boolean[] isVisible = client.autofillClientGetViewVisibility(
+ Helper.toArray(orderedVisibleIds));
+
+ final int numVisibleTrackedIds = orderedVisibleIds.size();
+ for (int i = 0; i < numVisibleTrackedIds; i++) {
+ final AutofillId id = orderedVisibleIds.get(i);
+
+ if (isVisible[i]) {
+ updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
+ } else {
+ updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
+
+ if (sDebug) {
+ Log.d(TAG, "onVisibleForAutofill() " + id + " became invisible");
+ }
+ }
+ }
+ }
+
+ mInvisibleTrackedIds = updatedInvisibleTrackedIds;
+ mVisibleTrackedIds = updatedVisibleTrackedIds;
+ }
+
+ if (mVisibleTrackedIds == null) {
+ if (sVerbose) {
+ Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids");
+ }
+ finishSessionLocked();
+ }
+ }
+ }
+
+ /**
+ * Callback for autofill related events.
+ *
+ * <p>Typically used for applications that display their own "auto-complete" views, so they can
+ * enable / disable such views when the autofill UI is shown / hidden.
+ */
+ public abstract static class AutofillCallback {
+
+ /** @hide */
+ @IntDef(prefix = { "EVENT_INPUT_" }, value = {
+ EVENT_INPUT_SHOWN,
+ EVENT_INPUT_HIDDEN,
+ EVENT_INPUT_UNAVAILABLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AutofillEventType {}
+
+ /**
+ * The autofill input UI associated with the view was shown.
+ *
+ * <p>If the view provides its own auto-complete UI and its currently shown, it
+ * should be hidden upon receiving this event.
+ */
+ public static final int EVENT_INPUT_SHOWN = 1;
+
+ /**
+ * The autofill input UI associated with the view was hidden.
+ *
+ * <p>If the view provides its own auto-complete UI that was hidden upon a
+ * {@link #EVENT_INPUT_SHOWN} event, it could be shown again now.
+ */
+ public static final int EVENT_INPUT_HIDDEN = 2;
+
+ /**
+ * The autofill input UI associated with the view isn't shown because
+ * autofill is not available.
+ *
+ * <p>If the view provides its own auto-complete UI but was not displaying it
+ * to avoid flickering, it could shown it upon receiving this event.
+ */
+ public static final int EVENT_INPUT_UNAVAILABLE = 3;
+
+ /**
+ * Called after a change in the autofill state associated with a view.
+ *
+ * @param view view associated with the change.
+ *
+ * @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}.
+ */
+ public void onAutofillEvent(@NonNull View view, @AutofillEventType int event) {
+ }
+
+ /**
+ * Called after a change in the autofill state associated with a virtual view.
+ *
+ * @param view parent view associated with the change.
+ * @param virtualId id identifying the virtual child inside the parent view.
+ *
+ * @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}.
+ */
+ public void onAutofillEvent(@NonNull View view, int virtualId,
+ @AutofillEventType int event) {
+ }
+ }
+
+ private static final class AutofillManagerClient extends IAutoFillManagerClient.Stub {
+ private final WeakReference<AutofillManager> mAfm;
+
+ private AutofillManagerClient(AutofillManager autofillManager) {
+ mAfm = new WeakReference<>(autofillManager);
+ }
+
+ @Override
+ public void setState(int flags) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.setState(flags));
+ }
+ }
+
+ @Override
+ public void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values,
+ boolean hideHighlight) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.autofill(sessionId, ids, values, hideHighlight));
+ }
+ }
+
+ @Override
+ public void autofillContent(int sessionId, AutofillId id, ClipData content) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.autofillContent(sessionId, id, content));
+ }
+ }
+
+ @Override
+ public void authenticate(int sessionId, int authenticationId, IntentSender intent,
+ Intent fillInIntent, boolean authenticateInline) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent,
+ authenticateInline));
+ }
+ }
+
+ @Override
+ public void requestShowFillUi(int sessionId, AutofillId id, int width, int height,
+ Rect anchorBounds, IAutofillWindowPresenter presenter) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestShowFillUi(sessionId, id, width, height, anchorBounds,
+ presenter));
+ }
+ }
+
+ @Override
+ public void requestHideFillUi(int sessionId, AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestHideFillUi(id, false));
+ }
+ }
+
+ @Override
+ public void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinishedState));
+ }
+ }
+
+ @Override
+ public void notifyFillUiShown(int sessionId, AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(
+ () -> afm.notifyCallback(
+ sessionId, id, AutofillCallback.EVENT_INPUT_SHOWN));
+ }
+ }
+
+ @Override
+ public void notifyFillUiHidden(int sessionId, AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(
+ () -> afm.notifyCallback(
+ sessionId, id, AutofillCallback.EVENT_INPUT_HIDDEN));
+ }
+ }
+
+ @Override
+ public void notifyDisableAutofill(long disableDuration, ComponentName componentName)
+ throws RemoteException {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.notifyDisableAutofill(disableDuration, componentName));
+ }
+ }
+
+ @Override
+ public void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent fullScreen) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.dispatchUnhandledKey(sessionId, id, fullScreen));
+ }
+ }
+
+ @Override
+ public void startIntentSender(IntentSender intentSender, Intent intent) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> {
+ try {
+ afm.mContext.startIntentSender(intentSender, intent, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ Log.e(TAG, "startIntentSender() failed for intent:" + intentSender, e);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setTrackedViews(int sessionId, AutofillId[] ids,
+ boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds,
+ AutofillId saveTriggerId) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible,
+ saveOnFinish, fillableIds, saveTriggerId));
+ }
+ }
+
+ @Override
+ public void setSaveUiState(int sessionId, boolean shown) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.setSaveUiState(sessionId, shown));
+ }
+ }
+
+ @Override
+ public void setSessionFinished(int newState, List<AutofillId> autofillableIds) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.setSessionFinished(newState, autofillableIds));
+ }
+ }
+
+ @Override
+ public void getAugmentedAutofillClient(IResultReceiver result) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.getAugmentedAutofillClient(result));
+ }
+ }
+
+ @Override
+ public void requestShowSoftInput(@NonNull AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestShowSoftInput(id));
+ }
+ }
+ }
+
+ private static final class AugmentedAutofillManagerClient
+ extends IAugmentedAutofillManagerClient.Stub {
+ private final WeakReference<AutofillManager> mAfm;
+
+ private AugmentedAutofillManagerClient(AutofillManager autofillManager) {
+ mAfm = new WeakReference<>(autofillManager);
+ }
+
+ @Nullable
+ @Override
+ public ViewNodeParcelable getViewNodeParcelable(@NonNull AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm == null) return null;
+
+ final View view = getView(afm, id);
+ if (view == null) {
+ Log.w(TAG, "getViewNodeParcelable(" + id + "): could not find view");
+ return null;
+ }
+ final ViewRootImpl root = view.getViewRootImpl();
+ if (root != null
+ && (root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) == 0) {
+ ViewNodeBuilder viewStructure = new ViewNodeBuilder();
+ viewStructure.setAutofillId(view.getAutofillId());
+ view.onProvideAutofillStructure(viewStructure, /* flags= */ 0);
+ // TODO(b/141703532): We don't call View#onProvideAutofillVirtualStructure for
+ // efficiency reason. But this also means we will return null for virtual views
+ // for now. We will add a new API to fetch the view node info of the virtual
+ // child view.
+ ViewNode viewNode = viewStructure.getViewNode();
+ if (viewNode != null && id.equals(viewNode.getAutofillId())) {
+ return new ViewNodeParcelable(viewNode);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Rect getViewCoordinates(@NonNull AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm == null) return null;
+
+ final View view = getView(afm, id);
+ if (view == null) {
+ return null;
+ }
+ final Rect windowVisibleDisplayFrame = new Rect();
+ view.getWindowVisibleDisplayFrame(windowVisibleDisplayFrame);
+ final int[] location = new int[2];
+ view.getLocationOnScreen(location);
+ final Rect rect = new Rect(location[0], location[1] - windowVisibleDisplayFrame.top,
+ location[0] + view.getWidth(),
+ location[1] - windowVisibleDisplayFrame.top + view.getHeight());
+ if (sVerbose) {
+ Log.v(TAG, "Coordinates for " + id + ": " + rect);
+ }
+ return rect;
+ }
+
+ @Override
+ public void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values,
+ boolean hideHighlight) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.autofill(sessionId, ids, values, hideHighlight));
+ }
+ }
+
+ @Override
+ public void requestShowFillUi(int sessionId, AutofillId id, int width, int height,
+ Rect anchorBounds, IAutofillWindowPresenter presenter) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestShowFillUi(sessionId, id, width, height, anchorBounds,
+ presenter));
+ }
+ }
+
+ @Override
+ public void requestHideFillUi(int sessionId, AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestHideFillUi(id, false));
+ }
+ }
+
+ @Override
+ public boolean requestAutofill(int sessionId, AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm == null || afm.mSessionId != sessionId) {
+ if (sDebug) {
+ Slog.d(TAG, "Autofill not available or sessionId doesn't match");
+ }
+ return false;
+ }
+ final View view = getView(afm, id);
+ if (view == null || !view.isFocused()) {
+ if (sDebug) {
+ Slog.d(TAG, "View not available or is not on focus");
+ }
+ return false;
+ }
+ if (sVerbose) {
+ Log.v(TAG, "requestAutofill() by AugmentedAutofillService.");
+ }
+ afm.post(() -> afm.requestAutofillFromNewSession(view));
+ return true;
+ }
+
+ @Nullable
+ private View getView(@NonNull AutofillManager afm, @NonNull AutofillId id) {
+ final AutofillClient client = afm.getClient();
+ if (client == null) {
+ Log.w(TAG, "getView(" + id + "): no autofill client");
+ return null;
+ }
+ View view = client.autofillClientFindViewByAutofillIdTraversal(id);
+ if (view == null) {
+ Log.w(TAG, "getView(" + id + "): could not find view");
+ }
+ return view;
+ }
+ }
+}
diff --git a/android/view/autofill/AutofillManagerInternal.java b/android/view/autofill/AutofillManagerInternal.java
new file mode 100644
index 0000000..a07d46d
--- /dev/null
+++ b/android/view/autofill/AutofillManagerInternal.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.view.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.AutofillOptions;
+
+/**
+ * Autofill Manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class AutofillManagerInternal {
+
+ /**
+ * Notifies the manager that the back key was pressed.
+ */
+ public abstract void onBackKeyPressed();
+
+ /**
+ * Gets autofill options for a package.
+ *
+ * <p><b>NOTE: </b>this method is called by the {@code ActivityManager} service and hence cannot
+ * hold the main service lock.
+ *
+ * @param packageName The package for which to query.
+ * @param versionCode The package version code.
+ * @param userId The user id for which to query.
+ */
+ @Nullable
+ public abstract AutofillOptions getAutofillOptions(@NonNull String packageName,
+ long versionCode, @UserIdInt int userId);
+
+ /**
+ * Checks whether the given {@code uid} owns the
+ * {@link android.service.autofill.augmented.AugmentedAutofillService} implementation associated
+ * with the given {@code userId}.
+ */
+ public abstract boolean isAugmentedAutofillServiceForUser(@NonNull int callingUid,
+ @UserIdInt int userId);
+}
diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java
new file mode 100644
index 0000000..3161c3c
--- /dev/null
+++ b/android/view/autofill/AutofillPopupWindow.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import static android.view.autofill.Helper.sVerbose;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.transition.Transition;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.PopupWindow;
+import android.window.WindowMetricsHelper;
+
+/**
+ * Custom {@link PopupWindow} used to isolate its content from the autofilled app - the
+ * UI is rendered in a framework process, but it's controlled by the app.
+ *
+ * TODO(b/34943932): use an app surface control solution.
+ *
+ * @hide
+ */
+public class AutofillPopupWindow extends PopupWindow {
+
+ private static final String TAG = "AutofillPopupWindow";
+
+ private final WindowPresenter mWindowPresenter;
+ private WindowManager.LayoutParams mWindowLayoutParams;
+ private boolean mFullScreen;
+
+ 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
+ * being shown by another process while the popup logic is in the process hosting
+ * the anchor view.
+ * <p>
+ * Using this constructor means that the presenter completely owns the content of
+ * the window and the following methods manipulating the window content shouldn't
+ * be used: {@link #getEnterTransition()}, {@link #setEnterTransition(Transition)},
+ * {@link #getExitTransition()}, {@link #setExitTransition(Transition)},
+ * {@link #getContentView()}, {@link #setContentView(View)}, {@link #getBackground()},
+ * {@link #setBackgroundDrawable(Drawable)}, {@link #getElevation()},
+ * {@link #setElevation(float)}, ({@link #getAnimationStyle()},
+ * {@link #setAnimationStyle(int)}, {@link #setTouchInterceptor(OnTouchListener)}.</p>
+ */
+ public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) {
+ mWindowPresenter = new WindowPresenter(presenter);
+
+ setTouchModal(false);
+ setOutsideTouchable(true);
+ setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
+ setFocusable(true);
+ }
+
+ @Override
+ protected boolean hasContentView() {
+ return true;
+ }
+
+ @Override
+ protected boolean hasDecorView() {
+ return true;
+ }
+
+ @Override
+ protected LayoutParams getDecorViewLayoutParams() {
+ return mWindowLayoutParams;
+ }
+
+ /**
+ * The effective {@code update} method that should be called by its clients.
+ */
+ public void update(View anchor, int offsetX, int offsetY, int width, int height,
+ Rect virtualBounds) {
+ mFullScreen = width == LayoutParams.MATCH_PARENT;
+ // For no fullscreen autofill window, we want to show the window as system controlled one
+ // so it covers app windows, but it has to be an application type (so it's contained inside
+ // the application area). Hence, we set it to the application type with the highest z-order,
+ // which currently is TYPE_APPLICATION_ABOVE_SUB_PANEL.
+ // For fullscreen mode, autofill window is at the bottom of screen, it should not be
+ // clipped by app activity window. Fullscreen autofill window does not need to follow app
+ // anchor view position.
+ setWindowLayoutType(mFullScreen ? WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG
+ : WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
+ // If we are showing the popup for a virtual view we use a fake view which
+ // delegates to the anchor but present itself with the same bounds as the
+ // virtual view. This ensures that the location logic in popup works
+ // symmetrically when the dropdown is below and above the anchor.
+ final View actualAnchor;
+ if (mFullScreen) {
+ offsetX = 0;
+ offsetY = 0;
+ // If it is not fullscreen height, put window at bottom. Computes absolute position.
+ // Note that we cannot easily change default gravity from Gravity.TOP to
+ // Gravity.BOTTOM because PopupWindow base class does not expose computeGravity().
+ final WindowManager windowManager = anchor.getContext()
+ .getSystemService(WindowManager.class);
+ final Rect windowBounds = WindowMetricsHelper.getBoundsExcludingNavigationBarAndCutout(
+ windowManager.getCurrentWindowMetrics());
+ width = windowBounds.width();
+ if (height != LayoutParams.MATCH_PARENT) {
+ offsetY = windowBounds.height() - height;
+ }
+ actualAnchor = anchor;
+ } else if (virtualBounds != null) {
+ final int[] mLocationOnScreen = new int[] {virtualBounds.left, virtualBounds.top};
+ actualAnchor = new View(anchor.getContext()) {
+ @Override
+ public void getLocationOnScreen(int[] location) {
+ location[0] = mLocationOnScreen[0];
+ location[1] = mLocationOnScreen[1];
+ }
+
+ @Override
+ public int getAccessibilityViewId() {
+ return anchor.getAccessibilityViewId();
+ }
+
+ @Override
+ public ViewTreeObserver getViewTreeObserver() {
+ return anchor.getViewTreeObserver();
+ }
+
+ @Override
+ public IBinder getApplicationWindowToken() {
+ return anchor.getApplicationWindowToken();
+ }
+
+ @Override
+ public View getRootView() {
+ return anchor.getRootView();
+ }
+
+ @Override
+ public int getLayoutDirection() {
+ return anchor.getLayoutDirection();
+ }
+
+ @Override
+ public void getWindowDisplayFrame(Rect outRect) {
+ anchor.getWindowDisplayFrame(outRect);
+ }
+
+ @Override
+ public void addOnAttachStateChangeListener(
+ OnAttachStateChangeListener listener) {
+ anchor.addOnAttachStateChangeListener(listener);
+ }
+
+ @Override
+ public void removeOnAttachStateChangeListener(
+ OnAttachStateChangeListener listener) {
+ anchor.removeOnAttachStateChangeListener(listener);
+ }
+
+ @Override
+ public boolean isAttachedToWindow() {
+ return anchor.isAttachedToWindow();
+ }
+
+ @Override
+ public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
+ return anchor.requestRectangleOnScreen(rectangle, immediate);
+ }
+
+ @Override
+ public IBinder getWindowToken() {
+ return anchor.getWindowToken();
+ }
+ };
+
+ actualAnchor.setLeftTopRightBottom(
+ virtualBounds.left, virtualBounds.top,
+ virtualBounds.right, virtualBounds.bottom);
+ actualAnchor.setScrollX(anchor.getScrollX());
+ actualAnchor.setScrollY(anchor.getScrollY());
+
+ anchor.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+ mLocationOnScreen[0] = mLocationOnScreen[0] - (scrollX - oldScrollX);
+ mLocationOnScreen[1] = mLocationOnScreen[1] - (scrollY - oldScrollY);
+ });
+ actualAnchor.setWillNotDraw(true);
+ } else {
+ actualAnchor = anchor;
+ }
+
+ if (!mFullScreen) {
+ // No fullscreen window animation is controlled by PopupWindow.
+ setAnimationStyle(-1);
+ } else if (height == LayoutParams.MATCH_PARENT) {
+ // Complete fullscreen autofill window has no animation.
+ setAnimationStyle(0);
+ } else {
+ // Slide half screen height autofill window from bottom.
+ setAnimationStyle(com.android.internal.R.style.AutofillHalfScreenAnimation);
+ }
+ if (!isShowing()) {
+ setWidth(width);
+ setHeight(height);
+ showAsDropDown(actualAnchor, offsetX, offsetY);
+ } else {
+ update(actualAnchor, offsetX, offsetY, width, height);
+ }
+ }
+
+ @Override
+ protected void update(View anchor, WindowManager.LayoutParams params) {
+ final int layoutDirection = anchor != null ? anchor.getLayoutDirection()
+ : View.LAYOUT_DIRECTION_LOCALE;
+ mWindowPresenter.show(params, getTransitionEpicenter(), isLayoutInsetDecor(),
+ layoutDirection);
+ }
+
+ @Override
+ protected boolean findDropDownPosition(View anchor, LayoutParams outParams,
+ int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) {
+ if (mFullScreen) {
+ // In fullscreen mode, don't need consider the anchor view.
+ outParams.x = xOffset;
+ outParams.y = yOffset;
+ outParams.width = width;
+ outParams.height = height;
+ outParams.gravity = gravity;
+ return false;
+ }
+ return super.findDropDownPosition(anchor, outParams, xOffset, yOffset,
+ width, height, gravity, allowScroll);
+ }
+
+ @Override
+ public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
+ if (sVerbose) {
+ Log.v(TAG, "showAsDropDown(): anchor=" + anchor + ", xoff=" + xoff + ", yoff=" + yoff
+ + ", isShowing(): " + isShowing());
+ }
+ if (isShowing()) {
+ return;
+ }
+
+ setShowing(true);
+ setDropDown(true);
+ attachToAnchor(anchor, xoff, yoff, gravity);
+ final WindowManager.LayoutParams p = mWindowLayoutParams = createPopupLayoutParams(
+ anchor.getWindowToken());
+ final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
+ p.width, p.height, gravity, getAllowScrollingAnchorParent());
+ updateAboveAnchor(aboveAnchor);
+ p.accessibilityIdOfAnchor = anchor.getAccessibilityViewId();
+ p.packageName = anchor.getContext().getPackageName();
+ mWindowPresenter.show(p, getTransitionEpicenter(), isLayoutInsetDecor(),
+ anchor.getLayoutDirection());
+ }
+
+ @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
+ public void dismiss() {
+ if (!isShowing() || isTransitioningToDismiss()) {
+ return;
+ }
+
+ setShowing(false);
+ setTransitioningToDismiss(true);
+
+ mWindowPresenter.hide(getTransitionEpicenter());
+ detachFromAnchor();
+ if (getOnDismissListener() != null) {
+ getOnDismissListener().onDismiss();
+ }
+ }
+
+ @Override
+ public int getAnimationStyle() {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public Drawable getBackground() {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public View getContentView() {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public float getElevation() {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public Transition getEnterTransition() {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public Transition getExitTransition() {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable background) {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public void setContentView(View contentView) {
+ if (contentView != null) {
+ throw new IllegalStateException("You can't call this!");
+ }
+ }
+
+ @Override
+ public void setElevation(float elevation) {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public void setEnterTransition(Transition enterTransition) {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public void setExitTransition(Transition exitTransition) {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ @Override
+ public void setTouchInterceptor(OnTouchListener l) {
+ throw new IllegalStateException("You can't call this!");
+ }
+
+ /**
+ * Contract between the popup window and a presenter that is responsible for
+ * showing/hiding/updating the actual window.
+ *
+ * <p>This can be useful if the anchor is in one process and the backing window is owned by
+ * another process.
+ */
+ private class WindowPresenter {
+ final IAutofillWindowPresenter mPresenter;
+
+ WindowPresenter(IAutofillWindowPresenter presenter) {
+ mPresenter = presenter;
+ }
+
+ /**
+ * Shows the backing window.
+ *
+ * @param p The window layout params.
+ * @param transitionEpicenter The transition epicenter if animating.
+ * @param fitsSystemWindows Whether the content view should account for system decorations.
+ * @param layoutDirection The content layout direction to be consistent with the anchor.
+ */
+ void show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows,
+ int layoutDirection) {
+ try {
+ mPresenter.show(p, transitionEpicenter, fitsSystemWindows, layoutDirection);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error showing fill window", e);
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Hides the backing window.
+ *
+ * @param transitionEpicenter The transition epicenter if animating.
+ */
+ void hide(Rect transitionEpicenter) {
+ try {
+ mPresenter.hide(transitionEpicenter);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error hiding fill window", e);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+}
diff --git a/android/view/autofill/AutofillTestHelper.java b/android/view/autofill/AutofillTestHelper.java
new file mode 100644
index 0000000..0763729
--- /dev/null
+++ b/android/view/autofill/AutofillTestHelper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.WindowNode;
+import android.service.autofill.FillContext;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Helper for common funcionalities.
+ */
+public class AutofillTestHelper {
+ private static final String TAG = "AutofillTestHelper";
+
+ /**
+ * Gets a node given its Android resource id, or {@code null} if not found.
+ */
+ public static ViewNode findNodeByResourceId(List<FillContext> contexts, String resourceId) {
+ for (FillContext context : contexts) {
+ ViewNode node = findNodeByResourceId(context.getStructure(), resourceId);
+ if (node != null) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets a node if it matches the filter criteria for the given id.
+ */
+ private static ViewNode findNodeByResourceId(AssistStructure structure, String id) {
+ Log.v(TAG, "Parsing request for activity " + structure.getActivityComponent());
+ final int nodes = structure.getWindowNodeCount();
+ for (int i = 0; i < nodes; i++) {
+ final WindowNode windowNode = structure.getWindowNodeAt(i);
+ final ViewNode rootNode = windowNode.getRootViewNode();
+ final ViewNode node = findNodeByResourceId(rootNode, id);
+ if (node != null) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets a node if it matches the filter criteria for the given id.
+ */
+ private static ViewNode findNodeByResourceId(ViewNode node, String id) {
+ if (id.equals(node.getIdEntry())) {
+ return node;
+ }
+ final int childrenSize = node.getChildCount();
+ if (childrenSize > 0) {
+ for (int i = 0; i < childrenSize; i++) {
+ final ViewNode found = findNodeByResourceId(node.getChildAt(i), id);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/android/view/autofill/AutofillTestWatcher.java b/android/view/autofill/AutofillTestWatcher.java
new file mode 100644
index 0000000..f65067f
--- /dev/null
+++ b/android/view/autofill/AutofillTestWatcher.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.perftests.utils.SettingsHelper;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.Timeout;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Custom {@link TestWatcher} that does the setup and reset tasks for the tests.
+ */
+final class AutofillTestWatcher extends TestWatcher {
+
+ private static final String TAG = "AutofillTestWatcher";
+ private static final long GENERIC_TIMEOUT_MS = 10_000;
+
+ private static ServiceWatcher sServiceWatcher;
+
+ private String mOriginalLogLevel;
+
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ final String testName = description.getDisplayName();
+ Log.i(TAG, "Starting " + testName);
+
+ prepareDevice();
+ enableVerboseLog();
+ // Prepare the service before each test.
+ // Disable the current AutofillService.
+ resetAutofillService();
+ // Set MyAutofillService status enable, it can start to accept the calls.
+ enableMyAutofillService();
+ setServiceWatcher();
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ final String testName = description.getDisplayName();
+ Log.i(TAG, "Finished " + testName);
+ restoreLogLevel();
+ // Set MyAutofillService status disable, so the calls are ignored.
+ disableMyAutofillService();
+ clearServiceWatcher();
+ }
+
+ void waitServiceConnect() throws InterruptedException {
+ if (sServiceWatcher != null) {
+ Log.d(TAG, "waitServiceConnect()");
+ sServiceWatcher.waitOnConnected();
+ }
+ }
+
+ /**
+ * Uses the {@code settings} binary to set the autofill service.
+ */
+ void setAutofillService() {
+ String serviceName = MyAutofillService.COMPONENT_NAME;
+ SettingsHelper.syncSet(InstrumentationRegistry.getTargetContext(),
+ SettingsHelper.NAMESPACE_SECURE,
+ Settings.Secure.AUTOFILL_SERVICE,
+ serviceName);
+ // Waits until the service is actually enabled.
+ Timeout timeout = new Timeout("CONNECTION_TIMEOUT", GENERIC_TIMEOUT_MS, 2F,
+ GENERIC_TIMEOUT_MS);
+ try {
+ timeout.run("Enabling Autofill service", () -> {
+ return isAutofillServiceEnabled(serviceName) ? serviceName : null;
+ });
+ } catch (Exception e) {
+ throw new AssertionError("Enabling Autofill service failed.");
+ }
+ }
+
+ /**
+ * Uses the {@code settings} binary to reset the autofill service.
+ */
+ void resetAutofillService() {
+ SettingsHelper.syncDelete(InstrumentationRegistry.getTargetContext(),
+ SettingsHelper.NAMESPACE_SECURE,
+ Settings.Secure.AUTOFILL_SERVICE);
+ }
+
+ /**
+ * Checks whether the given service is set as the autofill service for the default user.
+ */
+ private boolean isAutofillServiceEnabled(String serviceName) {
+ String actualName = SettingsHelper.get(SettingsHelper.NAMESPACE_SECURE,
+ Settings.Secure.AUTOFILL_SERVICE);
+ return serviceName.equals(actualName);
+ }
+
+ private void prepareDevice() {
+ // Unlock screen.
+ runShellCommand("input keyevent KEYCODE_WAKEUP");
+
+ // Dismiss keyguard, in case it's set as "Swipe to unlock".
+ runShellCommand("wm dismiss-keyguard");
+
+ // Collapse notifications.
+ runShellCommand("cmd statusbar collapse");
+ }
+
+ private void enableMyAutofillService() {
+ MyAutofillService.resetStaticState();
+ MyAutofillService.setEnabled(true);
+ }
+
+ private void disableMyAutofillService() {
+ // Must disable service so calls are ignored in case of errors during the test case;
+ // otherwise, other tests will fail because these calls are made in the UI thread (as both
+ // the service, the tests, and the app run in the same process).
+ MyAutofillService.setEnabled(false);
+ }
+
+ private void enableVerboseLog() {
+ mOriginalLogLevel = runShellCommand("cmd autofill get log_level");
+ Log.d(TAG, "enableVerboseLog(), mOriginalLogLevel=" + mOriginalLogLevel);
+ if (!mOriginalLogLevel.equals("verbose")) {
+ runShellCommand("cmd autofill set log_level verbose");
+ }
+ }
+
+ private void restoreLogLevel() {
+ Log.d(TAG, "restoreLogLevel to " + mOriginalLogLevel);
+ if (!mOriginalLogLevel.equals("verbose")) {
+ runShellCommand("cmd autofill set log_level %s", mOriginalLogLevel);
+ }
+ }
+
+ private static void setServiceWatcher() {
+ if (sServiceWatcher == null) {
+ sServiceWatcher = new ServiceWatcher();
+ }
+ }
+
+ private static void clearServiceWatcher() {
+ if (sServiceWatcher != null) {
+ sServiceWatcher = null;
+ }
+ }
+
+ public static final class ServiceWatcher {
+ private final CountDownLatch mConnected = new CountDownLatch(1);
+
+ public static void onConnected() {
+ Log.i(TAG, "onConnected: sServiceWatcher=" + sServiceWatcher);
+
+ sServiceWatcher.mConnected.countDown();
+ }
+
+ @NonNull
+ public void waitOnConnected() throws InterruptedException {
+ await(mConnected, "not connected");
+ }
+
+ private void await(@NonNull CountDownLatch latch, @NonNull String fmt,
+ @Nullable Object... args)
+ throws InterruptedException {
+ final boolean called = latch.await(GENERIC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (!called) {
+ throw new IllegalStateException(String.format(fmt, args)
+ + " in " + GENERIC_TIMEOUT_MS + "ms");
+ }
+ }
+ }
+}
diff --git a/android/view/autofill/AutofillValue.java b/android/view/autofill/AutofillValue.java
new file mode 100644
index 0000000..f5eef35
--- /dev/null
+++ b/android/view/autofill/AutofillValue.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.autofill;
+
+import static android.view.View.AUTOFILL_TYPE_DATE;
+import static android.view.View.AUTOFILL_TYPE_LIST;
+import static android.view.View.AUTOFILL_TYPE_TEXT;
+import static android.view.View.AUTOFILL_TYPE_TOGGLE;
+import static android.view.autofill.Helper.sDebug;
+import static android.view.autofill.Helper.sVerbose;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Abstracts how a {@link View} can be autofilled by an
+ * {@link android.service.autofill.AutofillService}.
+ *
+ * <p>Each {@link AutofillValue} is associated with a {@code type}, as defined by
+ * {@link View#getAutofillType()}.
+ */
+public final class AutofillValue implements Parcelable {
+
+ private static final String TAG = "AutofillValue";
+
+ private final @View.AutofillType int mType;
+ private final @NonNull Object mValue;
+
+ private AutofillValue(@View.AutofillType int type, @NonNull Object value) {
+ mType = type;
+ mValue = value;
+ }
+
+ /**
+ * Gets the value to autofill a text field.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_TEXT} for more info.</p>
+ *
+ * @throws IllegalStateException if the value is not a text value
+ */
+ @NonNull public CharSequence getTextValue() {
+ Preconditions.checkState(isText(), "value must be a text value, not type=%d", mType);
+ return (CharSequence) mValue;
+ }
+
+ /**
+ * Checks if this is a text value.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_TEXT} for more info.</p>
+ */
+ public boolean isText() {
+ return mType == AUTOFILL_TYPE_TEXT;
+ }
+
+ /**
+ * Gets the value to autofill a toggable field.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_TOGGLE} for more info.</p>
+ *
+ * @throws IllegalStateException if the value is not a toggle value
+ */
+ public boolean getToggleValue() {
+ Preconditions.checkState(isToggle(), "value must be a toggle value, not type=%d", mType);
+ return (Boolean) mValue;
+ }
+
+ /**
+ * Checks if this is a toggle value.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_TOGGLE} for more info.</p>
+ */
+ public boolean isToggle() {
+ return mType == AUTOFILL_TYPE_TOGGLE;
+ }
+
+ /**
+ * Gets the value to autofill a selection list field.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_LIST} for more info.</p>
+ *
+ * @throws IllegalStateException if the value is not a list value
+ */
+ public int getListValue() {
+ Preconditions.checkState(isList(), "value must be a list value, not type=%d", mType);
+ return (Integer) mValue;
+ }
+
+ /**
+ * Checks if this is a list value.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_LIST} for more info.</p>
+ */
+ public boolean isList() {
+ return mType == AUTOFILL_TYPE_LIST;
+ }
+
+ /**
+ * Gets the value to autofill a date field.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_DATE} for more info.</p>
+ *
+ * @throws IllegalStateException if the value is not a date value
+ */
+ public long getDateValue() {
+ Preconditions.checkState(isDate(), "value must be a date value, not type=%d", mType);
+ return (Long) mValue;
+ }
+
+ /**
+ * Checks if this is a date value.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_DATE} for more info.</p>
+ */
+ public boolean isDate() {
+ return mType == AUTOFILL_TYPE_DATE;
+ }
+
+ /**
+ * Used to define whether a field is empty so it's not sent to service on save.
+ *
+ * <p>Only applies to some types, like text.
+ *
+ * @hide
+ */
+ public boolean isEmpty() {
+ return isText() && ((CharSequence) mValue).length() == 0;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int hashCode() {
+ return mType + mValue.hashCode();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final AutofillValue other = (AutofillValue) obj;
+
+ if (mType != other.mType) return false;
+
+ if (isText()) {
+ return mValue.toString().equals(other.mValue.toString());
+ } else {
+ return Objects.equals(mValue, other.mValue);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder string = new StringBuilder()
+ .append("[type=").append(mType)
+ .append(", value=");
+ if (isText()) {
+ Helper.appendRedacted(string, (CharSequence) mValue);
+ } else {
+ string.append(mValue);
+ }
+ return string.append(']').toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mType);
+
+ switch (mType) {
+ case AUTOFILL_TYPE_TEXT:
+ parcel.writeCharSequence((CharSequence) mValue);
+ break;
+ case AUTOFILL_TYPE_TOGGLE:
+ parcel.writeInt((Boolean) mValue ? 1 : 0);
+ break;
+ case AUTOFILL_TYPE_LIST:
+ parcel.writeInt((Integer) mValue);
+ break;
+ case AUTOFILL_TYPE_DATE:
+ parcel.writeLong((Long) mValue);
+ break;
+ }
+ }
+
+ private AutofillValue(@NonNull Parcel parcel) {
+ mType = parcel.readInt();
+
+ switch (mType) {
+ case AUTOFILL_TYPE_TEXT:
+ mValue = parcel.readCharSequence();
+ break;
+ case AUTOFILL_TYPE_TOGGLE:
+ int rawValue = parcel.readInt();
+ mValue = rawValue != 0;
+ break;
+ case AUTOFILL_TYPE_LIST:
+ mValue = parcel.readInt();
+ break;
+ case AUTOFILL_TYPE_DATE:
+ mValue = parcel.readLong();
+ break;
+ default:
+ throw new IllegalArgumentException("type=" + mType + " not valid");
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AutofillValue> CREATOR =
+ new Parcelable.Creator<AutofillValue>() {
+ @Override
+ public AutofillValue createFromParcel(Parcel source) {
+ return new AutofillValue(source);
+ }
+
+ @Override
+ public AutofillValue[] newArray(int size) {
+ return new AutofillValue[size];
+ }
+ };
+
+ ////////////////////
+ // Factory methods //
+ ////////////////////
+
+ /**
+ * Creates a new {@link AutofillValue} to autofill a {@link View} representing a text field.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_TEXT} for more info.
+ *
+ * <p><b>Note:</b> This method is not thread safe and can throw an exception if the
+ * {@code value} is modified by a different thread before it returns.
+ */
+ public static AutofillValue forText(@Nullable CharSequence value) {
+ if (sVerbose && !Looper.getMainLooper().isCurrentThread()) {
+ Log.v(TAG, "forText() not called on main thread: " + Thread.currentThread());
+ }
+
+ return value == null ? null : new AutofillValue(AUTOFILL_TYPE_TEXT,
+ TextUtils.trimNoCopySpans(value));
+ }
+
+ /**
+ * Creates a new {@link AutofillValue} to autofill a {@link View} representing a toggable
+ * field.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_TOGGLE} for more info.
+ */
+ public static AutofillValue forToggle(boolean value) {
+ return new AutofillValue(AUTOFILL_TYPE_TOGGLE, value);
+ }
+
+ /**
+ * Creates a new {@link AutofillValue} to autofill a {@link View} representing a selection
+ * list.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_LIST} for more info.
+ */
+ public static AutofillValue forList(int value) {
+ return new AutofillValue(AUTOFILL_TYPE_LIST, value);
+ }
+
+ /**
+ * Creates a new {@link AutofillValue} to autofill a {@link View} representing a date.
+ *
+ * <p>See {@link View#AUTOFILL_TYPE_DATE} for more info.
+ */
+ public static AutofillValue forDate(long value) {
+ return new AutofillValue(AUTOFILL_TYPE_DATE, value);
+ }
+}
diff --git a/android/view/autofill/Helper.java b/android/view/autofill/Helper.java
new file mode 100644
index 0000000..2f12bb2
--- /dev/null
+++ b/android/view/autofill/Helper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.view.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+
+/** @hide */
+public final class Helper {
+
+ // Debug-level flags are defined when service is bound.
+ public static boolean sDebug = false;
+ public static boolean sVerbose = false;
+
+ /**
+ * Appends {@code value} to the {@code builder} redacting its contents.
+ */
+ public static void appendRedacted(@NonNull StringBuilder builder,
+ @Nullable CharSequence value) {
+ builder.append(getRedacted(value));
+ }
+
+ /**
+ * Gets the redacted version of a value.
+ */
+ @NonNull
+ public static String getRedacted(@Nullable CharSequence value) {
+ return (value == null) ? "null" : value.length() + "_chars";
+ }
+
+ /**
+ * Appends {@code values} to the {@code builder} redacting its contents.
+ */
+ public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) {
+ if (values == null) {
+ builder.append("N/A");
+ return;
+ }
+ builder.append("[");
+ for (String value : values) {
+ builder.append(" '");
+ appendRedacted(builder, value);
+ builder.append("'");
+ }
+ builder.append(" ]");
+ }
+
+ /**
+ * Converts a collaction of {@link AutofillId AutofillIds} to an array.
+ *
+ * @param collection The collection.
+ * @return The array.
+ */
+ public static @NonNull AutofillId[] toArray(Collection<AutofillId> collection) {
+ if (collection == null) {
+ return new AutofillId[0];
+ }
+ final AutofillId[] array = new AutofillId[collection.size()];
+ collection.toArray(array);
+ return array;
+ }
+
+ /**
+ * Converts a Set to a List.
+ */
+ @Nullable
+ public static <T> ArrayList<T> toList(@Nullable Set<T> set) {
+ return set == null ? null : new ArrayList<T>(set);
+ }
+
+ private Helper() {
+ throw new UnsupportedOperationException("contains static members only");
+ }
+}
diff --git a/android/view/autofill/LoginTest.java b/android/view/autofill/LoginTest.java
new file mode 100644
index 0000000..a5d1e00
--- /dev/null
+++ b/android/view/autofill/LoginTest.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_HIDDEN;
+import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_SHOWN;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfTestActivity;
+import android.view.View;
+import android.widget.EditText;
+
+import androidx.test.filters.LargeTest;
+
+import com.android.perftests.autofill.R;
+
+import org.junit.Test;
+
+@LargeTest
+public class LoginTest extends AbstractAutofillPerfTestCase {
+
+ public static final String ID_USERNAME = "username";
+ public static final String ID_PASSWORD = "password";
+
+ private EditText mUsername;
+ private EditText mPassword;
+ private AutofillManager mAfm;
+
+ public LoginTest() {
+ super(R.layout.test_autofill_login);
+ }
+
+ @Override
+ protected void onCreate(PerfTestActivity activity) {
+ View root = activity.getWindow().getDecorView();
+ mUsername = root.findViewById(R.id.username);
+ mPassword = root.findViewById(R.id.password);
+ mAfm = activity.getSystemService(AutofillManager.class);
+ }
+
+ /**
+ * This is the baseline test for focusing the 2 views when autofill is disabled.
+ */
+ @Test
+ public void testFocus_noService() throws Throwable {
+ mTestWatcher.resetAutofillService();
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mActivityRule.runOnUiThread(() -> {
+ mUsername.requestFocus();
+ mPassword.requestFocus();
+ });
+ }
+ }
+
+ /**
+ * This time the service is called, but it returns a {@code null} response so the UI behaves
+ * as if autofill was disabled.
+ */
+ @Test
+ public void testFocus_serviceDoesNotAutofill() throws Throwable {
+ MyAutofillService.newCannedResponse().reply();
+ mTestWatcher.setAutofillService();
+
+ // Must first focus in a field to trigger autofill and wait for service response
+ // outside the loop
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ mTestWatcher.waitServiceConnect();
+ MyAutofillService.getLastFillRequest();
+ // Then focus on password so loop start with focus away from username
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mActivityRule.runOnUiThread(() -> {
+ mUsername.requestFocus();
+ mPassword.requestFocus();
+ });
+ }
+ }
+
+ /**
+ * Now the service returns autofill data, for both username and password.
+ */
+ @Test
+ public void testFocus_autofillBothFields() throws Throwable {
+ MyAutofillService.newCannedResponse()
+ .setUsername(ID_USERNAME, "user")
+ .setPassword(ID_PASSWORD, "pass")
+ .reply();
+ mTestWatcher.setAutofillService();
+
+ // Callback is used to slow down the calls made to the autofill server so the
+ // app is not crashed due to binder exhaustion. But the time spent waiting for the callbacks
+ // is not measured here...
+ MyAutofillCallback callback = new MyAutofillCallback();
+ mAfm.registerCallback(callback);
+
+ // Must first trigger autofill and wait for service response outside the loop
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ mTestWatcher.waitServiceConnect();
+ MyAutofillService.getLastFillRequest();
+ callback.expectEvent(mUsername, EVENT_INPUT_SHOWN);
+
+ // Then focus on password so loop start with focus away from username
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+ callback.expectEvent(mUsername, EVENT_INPUT_HIDDEN);
+ callback.expectEvent(mPassword, EVENT_INPUT_SHOWN);
+
+
+ // NOTE: we cannot run the whole loop inside the UI thread, because the autofill callback
+ // is called on it, which would cause a deadlock on expectEvent().
+ try {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ state.pauseTiming(); // Ignore time spent waiting for callbacks
+ callback.expectEvent(mPassword, EVENT_INPUT_HIDDEN);
+ callback.expectEvent(mUsername, EVENT_INPUT_SHOWN);
+ state.resumeTiming();
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+ state.pauseTiming(); // Ignore time spent waiting for callbacks
+ callback.expectEvent(mUsername, EVENT_INPUT_HIDDEN);
+ callback.expectEvent(mPassword, EVENT_INPUT_SHOWN);
+ state.resumeTiming();
+ }
+
+ // Sanity check
+ callback.assertNoAsyncErrors();
+ } finally {
+ mAfm.unregisterCallback(callback);
+ }
+ }
+
+ /**
+ * Now the service returns autofill data, but just for username.
+ */
+ @Test
+ public void testFocus_autofillUsernameOnly() throws Throwable {
+ // Must set ignored ids so focus on password does not trigger new requests
+ MyAutofillService.newCannedResponse()
+ .setUsername(ID_USERNAME, "user")
+ .setIgnored(ID_PASSWORD)
+ .reply();
+ mTestWatcher.setAutofillService();
+
+ // Callback is used to slow down the calls made to the autofill server so the
+ // app is not crashed due to binder exhaustion. But the time spent waiting for the callbacks
+ // is not measured here...
+ MyAutofillCallback callback = new MyAutofillCallback();
+ mAfm.registerCallback(callback);
+
+ // Must first trigger autofill and wait for service response outside the loop
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ mTestWatcher.waitServiceConnect();
+ MyAutofillService.getLastFillRequest();
+ callback.expectEvent(mUsername, EVENT_INPUT_SHOWN);
+
+ // Then focus on password so loop start with focus away from username
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+ callback.expectEvent(mUsername, EVENT_INPUT_HIDDEN);
+
+ // NOTE: we cannot run the whole loop inside the UI thread, because the autofill callback
+ // is called on it, which would cause a deadlock on expectEvent().
+ try {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ state.pauseTiming(); // Ignore time spent waiting for callbacks
+ callback.expectEvent(mUsername, EVENT_INPUT_SHOWN);
+ state.resumeTiming();
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+ state.pauseTiming(); // Ignore time spent waiting for callbacks
+ callback.expectEvent(mUsername, EVENT_INPUT_HIDDEN);
+ state.resumeTiming();
+ }
+
+ // Sanity check
+ callback.assertNoAsyncErrors();
+ } finally {
+ mAfm.unregisterCallback(callback);
+ }
+ }
+
+ /**
+ * This is the baseline test for changing the 2 views when autofill is disabled.
+ */
+ @Test
+ public void testChange_noService() throws Throwable {
+ mTestWatcher.resetAutofillService();
+
+ changeTest(false);
+ }
+
+ /**
+ * This time the service is called, but it returns a {@code null} response so the UI behaves
+ * as if autofill was disabled.
+ */
+ @Test
+ public void testChange_serviceDoesNotAutofill() throws Throwable {
+ MyAutofillService.newCannedResponse().reply();
+ mTestWatcher.setAutofillService();
+
+ changeTest(true);
+ }
+
+ /**
+ * Now the service returns autofill data, for both username and password.
+ */
+ @Test
+ public void testChange_autofillBothFields() throws Throwable {
+ MyAutofillService.newCannedResponse()
+ .setUsername(ID_USERNAME, "user")
+ .setPassword(ID_PASSWORD, "pass")
+ .reply();
+ mTestWatcher.setAutofillService();
+
+ changeTest(true);
+ }
+
+ /**
+ * Now the service returns autofill data, but just for username.
+ */
+ @Test
+ public void testChange_autofillUsernameOnly() throws Throwable {
+ // Must set ignored ids so focus on password does not trigger new requests
+ MyAutofillService.newCannedResponse()
+ .setUsername(ID_USERNAME, "user")
+ .setIgnored(ID_PASSWORD)
+ .reply();
+ mTestWatcher.setAutofillService();
+
+ changeTest(true);
+ }
+
+ private void changeTest(boolean waitForService) throws Throwable {
+ // Must first focus in a field to trigger autofill and wait for service response
+ // outside the loop
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ if (waitForService) {
+ mTestWatcher.waitServiceConnect();
+ MyAutofillService.getLastFillRequest();
+ }
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mActivityRule.runOnUiThread(() -> {
+ mUsername.setText("");
+ mUsername.setText("a");
+ mPassword.setText("");
+ mPassword.setText("x");
+ });
+ }
+ }
+
+ @Test
+ public void testCallbacks() throws Throwable {
+ MyAutofillService.newCannedResponse()
+ .setUsername(ID_USERNAME, "user")
+ .setPassword(ID_PASSWORD, "pass")
+ .reply();
+ mTestWatcher.setAutofillService();
+
+ MyAutofillCallback callback = new MyAutofillCallback();
+ mAfm.registerCallback(callback);
+
+ // Must first focus in a field to trigger autofill and wait for service response
+ // outside the loop
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ mTestWatcher.waitServiceConnect();
+ MyAutofillService.getLastFillRequest();
+ callback.expectEvent(mUsername, EVENT_INPUT_SHOWN);
+
+ // Now focus on password to prepare loop state
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+ callback.expectEvent(mUsername, EVENT_INPUT_HIDDEN);
+ callback.expectEvent(mPassword, EVENT_INPUT_SHOWN);
+
+ // NOTE: we cannot run the whole loop inside the UI thread, because the autofill callback
+ // is called on it, which would cause a deadlock on expectEvent().
+ try {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+ callback.expectEvent(mPassword, EVENT_INPUT_HIDDEN);
+ callback.expectEvent(mUsername, EVENT_INPUT_SHOWN);
+ mActivityRule.runOnUiThread(() -> mPassword.requestFocus());
+ callback.expectEvent(mUsername, EVENT_INPUT_HIDDEN);
+ callback.expectEvent(mPassword, EVENT_INPUT_SHOWN);
+ }
+
+ // Sanity check
+ callback.assertNoAsyncErrors();
+ } finally {
+ mAfm.unregisterCallback(callback);
+ }
+ }
+}
diff --git a/android/view/autofill/MyAutofillCallback.java b/android/view/autofill/MyAutofillCallback.java
new file mode 100644
index 0000000..208d632
--- /dev/null
+++ b/android/view/autofill/MyAutofillCallback.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import android.view.View;
+import android.view.autofill.AutofillManager.AutofillCallback;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_HIDDEN;
+import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_SHOWN;
+import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_UNAVAILABLE;
+
+import android.os.CancellationSignal;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.util.Log;
+
+/**
+ * Custom {@link AutofillCallback} used to recover events during tests.
+ */
+public final class MyAutofillCallback extends AutofillCallback {
+
+ private static final String TAG = "MyAutofillCallback";
+ private static final int TIMEOUT_MS = 5000;
+
+ private final BlockingQueue<MyEvent> mEvents = new LinkedBlockingQueue<>(2);
+ private final List<String> mAsyncErrors = new ArrayList<>();
+
+ @Override
+ public void onAutofillEvent(View view, int event) {
+ boolean offered = false;
+ try {
+ offered = mEvents.offer(new MyEvent(view, event), TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ if (!offered) {
+ String error = "could not offer " + toString(view, event) + " in " + TIMEOUT_MS + "ms";
+ Log.e(TAG, error);
+ mAsyncErrors.add(error);
+ }
+ }
+
+ /**
+ * Asserts the callback is called for the given view and event, or fail if it times out.
+ */
+ public void expectEvent(@NonNull View view, int event) {
+ MyEvent myEvent;
+ try {
+ myEvent = mEvents.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (myEvent == null) {
+ throw new IllegalStateException("no event received in " + TIMEOUT_MS
+ + "ms while waiting for " + toString(view, event));
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("interrupted waiting for " + toString(view, event));
+ }
+ if (!myEvent.view.equals(view) || myEvent.event != event) {
+ throw new AssertionError("Invalid event: expected " + myEvent + ", got "
+ + toString(view, event));
+ }
+ }
+
+ /**
+ * Throws an exception if an error happened asynchronously while handing
+ * {@link #onAutofillEvent(View, int)}.
+ */
+ public void assertNoAsyncErrors() {
+ if (!mAsyncErrors.isEmpty()) {
+ throw new IllegalStateException(mAsyncErrors.size() + " errors: " + mAsyncErrors);
+ }
+ }
+
+ private static String eventToString(int event) {
+ switch (event) {
+ case EVENT_INPUT_HIDDEN:
+ return "HIDDEN";
+ case EVENT_INPUT_SHOWN:
+ return "SHOWN";
+ case EVENT_INPUT_UNAVAILABLE:
+ return "UNAVAILABLE";
+ default:
+ throw new IllegalArgumentException("invalid event: " + event);
+ }
+ }
+
+ private static String toString(View view, int event) {
+ return eventToString(event) + ": " + view + ")";
+ }
+
+ private static final class MyEvent {
+ public final View view;
+ public final int event;
+
+ MyEvent(View view, int event) {
+ this.view = view;
+ this.event = event;
+ }
+
+ @Override
+ public String toString() {
+ return MyAutofillCallback.toString(view, event);
+ }
+ }
+}
diff --git a/android/view/autofill/MyAutofillService.java b/android/view/autofill/MyAutofillService.java
new file mode 100644
index 0000000..5db6597
--- /dev/null
+++ b/android/view/autofill/MyAutofillService.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveRequest;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+
+import com.android.perftests.autofill.R;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An {@link AutofillService} implementation whose replies can be programmed by the test case.
+ */
+public class MyAutofillService extends AutofillService {
+
+ private static final String TAG = "MyAutofillService";
+ private static final int TIMEOUT_MS = 5_000;
+
+ private static final String PACKAGE_NAME = "com.android.perftests.autofill";
+ static final String COMPONENT_NAME = PACKAGE_NAME + "/android.view.autofill.MyAutofillService";
+
+ private static final BlockingQueue<FillRequest> sFillRequests = new LinkedBlockingQueue<>();
+ private static final BlockingQueue<CannedResponse> sCannedResponses =
+ new LinkedBlockingQueue<>();
+
+ private static boolean sEnabled;
+
+ /**
+ * Returns the TestWatcher that was used for the testing.
+ */
+ @NonNull
+ public static AutofillTestWatcher getTestWatcher() {
+ return new AutofillTestWatcher();
+ }
+
+ /**
+ * Resets the static state associated with the service.
+ */
+ static void resetStaticState() {
+ sFillRequests.clear();
+ sCannedResponses.clear();
+ sEnabled = false;
+ }
+
+ /**
+ * Sets whether the service is enabled or not - when disabled, calls to
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} will be ignored.
+ */
+ static void setEnabled(boolean enabled) {
+ sEnabled = enabled;
+ }
+
+ /**
+ * Gets the the last {@link FillRequest} passed to
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} or throws an
+ * exception if that method was not called.
+ */
+ @NonNull
+ static FillRequest getLastFillRequest() {
+ FillRequest request = null;
+ try {
+ request = sFillRequests.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("onFillRequest() interrupted");
+ }
+ if (request == null) {
+ throw new IllegalStateException("onFillRequest() not called in " + TIMEOUT_MS + "ms");
+ }
+ return request;
+ }
+
+ @Override
+ public void onConnected() {
+ AutofillTestWatcher.ServiceWatcher.onConnected();
+ }
+
+ @Override
+ public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+ FillCallback callback) {
+ try {
+ handleRequest(request, callback);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ onError("onFillRequest() interrupted", e, callback);
+ } catch (Exception e) {
+ onError("exception on onFillRequest()", e, callback);
+ }
+ }
+
+
+ private void handleRequest(FillRequest request, FillCallback callback) throws Exception {
+ if (!sEnabled) {
+ onError("ignoring onFillRequest(): service is disabled", callback);
+ return;
+ }
+ CannedResponse response = sCannedResponses.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (response == null) {
+ onError("ignoring onFillRequest(): response not set", callback);
+ return;
+ }
+ Dataset.Builder dataset = new Dataset.Builder(newDatasetPresentation("dataset"));
+ boolean hasData = false;
+ if (response.mUsername != null) {
+ hasData = true;
+ AutofillId autofillId = getAutofillIdByResourceId(request, response.mUsername.first);
+ AutofillValue value = AutofillValue.forText(response.mUsername.second);
+ dataset.setValue(autofillId, value, newDatasetPresentation("dataset"));
+ }
+ if (response.mPassword != null) {
+ hasData = true;
+ AutofillId autofillId = getAutofillIdByResourceId(request, response.mPassword.first);
+ AutofillValue value = AutofillValue.forText(response.mPassword.second);
+ dataset.setValue(autofillId, value, newDatasetPresentation("dataset"));
+ }
+ if (hasData) {
+ FillResponse.Builder fillResponse = new FillResponse.Builder();
+ if (response.mIgnoredIds != null) {
+ int length = response.mIgnoredIds.length;
+ AutofillId[] requiredIds = new AutofillId[length];
+ for (int i = 0; i < length; i++) {
+ String resourceId = response.mIgnoredIds[i];
+ requiredIds[i] = getAutofillIdByResourceId(request, resourceId);
+ }
+ fillResponse.setIgnoredIds(requiredIds);
+ }
+ callback.onSuccess(fillResponse.addDataset(dataset.build()).build());
+ } else {
+ callback.onSuccess(null);
+ }
+ if (!sFillRequests.offer(request, TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ Log.w(TAG, "could not offer request in " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ private AutofillId getAutofillIdByResourceId(FillRequest request, String resourceId)
+ throws Exception {
+ ViewNode node = AutofillTestHelper.findNodeByResourceId(request.getFillContexts(),
+ resourceId);
+ if (node == null) {
+ throw new AssertionError("No node with resource id " + resourceId);
+ }
+ return node.getAutofillId();
+ }
+
+ @Override
+ public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+ // No current test should have triggered it...
+ Log.e(TAG, "onSaveRequest() should not have been called");
+ callback.onFailure("onSaveRequest() should not have been called");
+ }
+
+ static final class CannedResponse {
+ private final Pair<String, String> mUsername;
+ private final Pair<String, String> mPassword;
+ private final String[] mIgnoredIds;
+
+ private CannedResponse(@NonNull Builder builder) {
+ mUsername = builder.mUsername;
+ mPassword = builder.mPassword;
+ mIgnoredIds = builder.mIgnoredIds;
+ }
+
+ static class Builder {
+ private Pair<String, String> mUsername;
+ private Pair<String, String> mPassword;
+ private String[] mIgnoredIds;
+
+ @NonNull
+ Builder setUsername(@NonNull String id, @NonNull String value) {
+ mUsername = new Pair<>(id, value);
+ return this;
+ }
+
+ @NonNull
+ Builder setPassword(@NonNull String id, @NonNull String value) {
+ mPassword = new Pair<>(id, value);
+ return this;
+ }
+
+ @NonNull
+ Builder setIgnored(String... ids) {
+ mIgnoredIds = ids;
+ return this;
+ }
+
+ void reply() {
+ sCannedResponses.add(new CannedResponse(this));
+ }
+ }
+ }
+
+ /**
+ * Sets the expected canned {@link FillResponse} for the next
+ * {@link AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+ */
+ static CannedResponse.Builder newCannedResponse() {
+ return new CannedResponse.Builder();
+ }
+
+ private void onError(@NonNull String msg, @NonNull FillCallback callback) {
+ Log.e(TAG, msg);
+ callback.onFailure(msg);
+ }
+
+ private void onError(@NonNull String msg, @NonNull Exception e,
+ @NonNull FillCallback callback) {
+ Log.e(TAG, msg, e);
+ callback.onFailure(msg);
+ }
+
+ @NonNull
+ private static RemoteViews newDatasetPresentation(@NonNull CharSequence text) {
+ RemoteViews presentation =
+ new RemoteViews(PACKAGE_NAME, R.layout.autofill_dataset_picker_text_only);
+ presentation.setTextViewText(R.id.text, text);
+ return presentation;
+ }
+}
diff --git a/android/view/autofill/ParcelableMap.java b/android/view/autofill/ParcelableMap.java
new file mode 100644
index 0000000..d8459aa
--- /dev/null
+++ b/android/view/autofill/ParcelableMap.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.autofill;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A parcelable HashMap for {@link AutofillId} and {@link AutofillValue}
+ *
+ * {@hide}
+ */
+class ParcelableMap extends HashMap<AutofillId, AutofillValue> implements Parcelable {
+ ParcelableMap(int size) {
+ super(size);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(size());
+
+ for (Map.Entry<AutofillId, AutofillValue> entry : entrySet()) {
+ dest.writeParcelable(entry.getKey(), 0);
+ dest.writeParcelable(entry.getValue(), 0);
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ParcelableMap> CREATOR =
+ new Parcelable.Creator<ParcelableMap>() {
+ @Override
+ public ParcelableMap createFromParcel(Parcel source) {
+ int size = source.readInt();
+
+ ParcelableMap map = new ParcelableMap(size);
+
+ for (int i = 0; i < size; i++) {
+ AutofillId key = source.readParcelable(null);
+ AutofillValue value = source.readParcelable(null);
+
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ @Override
+ public ParcelableMap[] newArray(int size) {
+ return new ParcelableMap[size];
+ }
+ };
+}
diff --git a/android/view/contentcapture/AbstractContentCapturePerfTestCase.java b/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
new file mode 100644
index 0000000..9b853fe
--- /dev/null
+++ b/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.CustomTestActivity.INTENT_EXTRA_CUSTOM_VIEWS;
+import static android.view.contentcapture.CustomTestActivity.INTENT_EXTRA_LAYOUT_ID;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.app.Application;
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.os.BatteryManager;
+import android.os.UserHandle;
+import android.perftests.utils.PerfStatusReporter;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.ActivitiesWatcher;
+import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
+import com.android.perftests.contentcapture.R;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runners.model.Statement;
+
+/**
+ * Base class for all content capture tests.
+ */
+public abstract class AbstractContentCapturePerfTestCase {
+
+ private static final String TAG = AbstractContentCapturePerfTestCase.class.getSimpleName();
+ private static final long GENERIC_TIMEOUT_MS = 10_000;
+
+ private static int sOriginalStayOnWhilePluggedIn;
+ private static Context sContext = getInstrumentation().getTargetContext();
+
+ protected ActivitiesWatcher mActivitiesWatcher;
+
+ private MyContentCaptureService.ServiceWatcher mServiceWatcher;
+
+ @Rule
+ public ActivityTestRule<CustomTestActivity> mActivityRule =
+ new ActivityTestRule<>(CustomTestActivity.class, false, false);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Rule
+ public TestRule mServiceDisablerRule = (base, description) -> {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ base.evaluate();
+ } finally {
+ Log.v(TAG, "@mServiceDisablerRule: safelyDisableService()");
+ safelyDisableService();
+ }
+ }
+ };
+ };
+
+ private void safelyDisableService() {
+ try {
+ resetService();
+ MyContentCaptureService.resetStaticState();
+
+ if (mServiceWatcher != null) {
+ mServiceWatcher.waitOnDestroy();
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "error disabling service", t);
+ }
+ }
+
+ /**
+ * Sets the content capture service.
+ */
+ private static void setService(@NonNull String service) {
+ final int userId = getCurrentUserId();
+ Log.d(TAG, "Setting service for user " + userId + " to " + service);
+ // TODO(b/123540602): use @TestingAPI to get max duration constant
+ runShellCommand("cmd content_capture set temporary-service %d %s 119000", userId, service);
+ }
+
+ /**
+ * Resets the content capture service.
+ */
+ private static void resetService() {
+ final int userId = getCurrentUserId();
+ Log.d(TAG, "Resetting back user " + userId + " to default service");
+ runShellCommand("cmd content_capture set temporary-service %d", userId);
+ }
+
+ private static int getCurrentUserId() {
+ return UserHandle.myUserId();
+ }
+
+ @BeforeClass
+ public static void setStayAwake() {
+ Log.v(TAG, "@BeforeClass: setStayAwake()");
+ // Some test cases will restart the activity, and stay awake is necessary to ensure that
+ // the test will not screen off during the test.
+ // Keeping the activity screen on is not enough, screen off may occur between the activity
+ // finished and the next start
+ final int stayOnWhilePluggedIn = Settings.Global.getInt(sContext.getContentResolver(),
+ Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0);
+ sOriginalStayOnWhilePluggedIn = -1;
+ if (stayOnWhilePluggedIn != BatteryManager.BATTERY_PLUGGED_ANY) {
+ sOriginalStayOnWhilePluggedIn = stayOnWhilePluggedIn;
+ // Keep the device awake during testing.
+ setStayOnWhilePluggedIn(BatteryManager.BATTERY_PLUGGED_ANY);
+ }
+ }
+
+ @AfterClass
+ public static void resetStayAwake() {
+ Log.v(TAG, "@AfterClass: resetStayAwake()");
+ if (sOriginalStayOnWhilePluggedIn != -1) {
+ setStayOnWhilePluggedIn(sOriginalStayOnWhilePluggedIn);
+ }
+ }
+
+ private static void setStayOnWhilePluggedIn(int value) {
+ runShellCommand(String.format("settings put global %s %d",
+ Settings.Global.STAY_ON_WHILE_PLUGGED_IN, value));
+ }
+
+ @BeforeClass
+ public static void setAllowSelf() {
+ final ContentCaptureOptions options = new ContentCaptureOptions(null);
+ Log.v(TAG, "@BeforeClass: setAllowSelf(): options=" + options);
+ sContext.getApplicationContext().setContentCaptureOptions(options);
+ }
+
+ @AfterClass
+ public static void unsetAllowSelf() {
+ Log.v(TAG, "@AfterClass: unsetAllowSelf()");
+ clearOptions();
+ }
+
+ protected static void clearOptions() {
+ sContext.getApplicationContext().setContentCaptureOptions(null);
+ }
+
+ @BeforeClass
+ public static void disableDefaultService() {
+ Log.v(TAG, "@BeforeClass: disableDefaultService()");
+ setDefaultServiceEnabled(false);
+ }
+
+ @AfterClass
+ public static void enableDefaultService() {
+ Log.v(TAG, "@AfterClass: enableDefaultService()");
+ setDefaultServiceEnabled(true);
+ }
+
+ /**
+ * Enables / disables the default service.
+ */
+ private static void setDefaultServiceEnabled(boolean enabled) {
+ final int userId = getCurrentUserId();
+ Log.d(TAG, "setDefaultServiceEnabled(user=" + userId + ", enabled= " + enabled + ")");
+ runShellCommand("cmd content_capture set default-service-enabled %d %s", userId,
+ Boolean.toString(enabled));
+ }
+
+ @Before
+ public void prepareDevice() throws Exception {
+ Log.v(TAG, "@Before: prepareDevice()");
+
+ // Unlock screen.
+ runShellCommand("input keyevent KEYCODE_WAKEUP");
+
+ // Dismiss keyguard, in case it's set as "Swipe to unlock".
+ runShellCommand("wm dismiss-keyguard");
+
+ // Collapse notifications.
+ runShellCommand("cmd statusbar collapse");
+ }
+
+ @Before
+ public void registerLifecycleCallback() {
+ Log.v(TAG, "@Before: Registering lifecycle callback");
+ final Application app = (Application) sContext.getApplicationContext();
+ mActivitiesWatcher = new ActivitiesWatcher(GENERIC_TIMEOUT_MS);
+ app.registerActivityLifecycleCallbacks(mActivitiesWatcher);
+ }
+
+ @After
+ public void unregisterLifecycleCallback() {
+ Log.d(TAG, "@After: Unregistering lifecycle callback: " + mActivitiesWatcher);
+ if (mActivitiesWatcher != null) {
+ final Application app = (Application) sContext.getApplicationContext();
+ app.unregisterActivityLifecycleCallbacks(mActivitiesWatcher);
+ }
+ }
+
+ /**
+ * Sets {@link MyContentCaptureService} as the service for the current user and waits until
+ * its created, then add the perf test package into allow list.
+ */
+ public MyContentCaptureService enableService() throws InterruptedException {
+ if (mServiceWatcher != null) {
+ throw new IllegalStateException("There Can Be Only One!");
+ }
+
+ mServiceWatcher = MyContentCaptureService.setServiceWatcher();
+ setService(MyContentCaptureService.SERVICE_NAME);
+ mServiceWatcher.setAllowSelf();
+ return mServiceWatcher.waitOnCreate();
+ }
+
+ @NonNull
+ protected ActivityWatcher startWatcher() {
+ return mActivitiesWatcher.watch(CustomTestActivity.class);
+ }
+
+ /**
+ * Launch test activity with default login layout
+ */
+ protected CustomTestActivity launchActivity() {
+ return launchActivity(R.layout.test_login_activity, 0);
+ }
+
+ /**
+ * Launch test activity with give layout and parameter
+ */
+ protected CustomTestActivity launchActivity(int layoutId, int numViews) {
+ final Intent intent = new Intent(sContext, CustomTestActivity.class);
+ intent.putExtra(INTENT_EXTRA_LAYOUT_ID, layoutId);
+ intent.putExtra(INTENT_EXTRA_CUSTOM_VIEWS, numViews);
+ return mActivityRule.launchActivity(intent);
+ }
+
+ protected void finishActivity() {
+ try {
+ mActivityRule.finishActivity();
+ } catch (IllegalStateException e) {
+ // no op
+ }
+ }
+}
diff --git a/android/view/contentcapture/ChildContentCaptureSession.java b/android/view/contentcapture/ChildContentCaptureSession.java
new file mode 100644
index 0000000..44b4353
--- /dev/null
+++ b/android/view/contentcapture/ChildContentCaptureSession.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+/**
+ * A session that is explicitly created by the app (and hence is a descendant of
+ * {@link MainContentCaptureSession}).
+ *
+ * @hide
+ */
+final class ChildContentCaptureSession extends ContentCaptureSession {
+
+ @NonNull
+ private final ContentCaptureSession mParent;
+
+ /** @hide */
+ protected ChildContentCaptureSession(@NonNull ContentCaptureSession parent,
+ @NonNull ContentCaptureContext clientContext) {
+ super(clientContext);
+ mParent = parent;
+ }
+
+ @Override
+ MainContentCaptureSession getMainCaptureSession() {
+ if (mParent instanceof MainContentCaptureSession) {
+ return (MainContentCaptureSession) mParent;
+ }
+ return mParent.getMainCaptureSession();
+ }
+
+ @Override
+ ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
+ final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
+ getMainCaptureSession().notifyChildSessionStarted(mId, child.mId, clientContext);
+ return child;
+ }
+
+ @Override
+ void flush(@FlushReason int reason) {
+ mParent.flush(reason);
+ }
+
+ @Override
+ public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
+ getMainCaptureSession().notifyContextUpdated(mId, context);
+ }
+
+ @Override
+ void onDestroy() {
+ getMainCaptureSession().notifyChildSessionFinished(mParent.mId, mId);
+ }
+
+ @Override
+ void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
+ getMainCaptureSession().notifyViewAppeared(mId, node);
+ }
+
+ @Override
+ void internalNotifyViewDisappeared(@NonNull AutofillId id) {
+ getMainCaptureSession().notifyViewDisappeared(mId, id);
+ }
+
+ @Override
+ void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
+ getMainCaptureSession().notifyViewTextChanged(mId, id, text);
+ }
+
+ @Override
+ void internalNotifyViewInsetsChanged(@NonNull Insets viewInsets) {
+ getMainCaptureSession().notifyViewInsetsChanged(mId, viewInsets);
+ }
+
+ @Override
+ public void internalNotifyViewTreeEvent(boolean started) {
+ getMainCaptureSession().notifyViewTreeEvent(mId, started);
+ }
+
+ @Override
+ void internalNotifySessionResumed() {
+ getMainCaptureSession().notifySessionResumed();
+ }
+
+ @Override
+ void internalNotifySessionPaused() {
+ getMainCaptureSession().notifySessionPaused();
+ }
+
+ @Override
+ boolean isContentCaptureEnabled() {
+ return getMainCaptureSession().isContentCaptureEnabled();
+ }
+}
diff --git a/android/view/contentcapture/ContentCaptureCondition.java b/android/view/contentcapture/ContentCaptureCondition.java
new file mode 100644
index 0000000..1adef94
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureCondition.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.LocusId;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.DebugUtils;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Defines a condition for when content capture should be allowed.
+ *
+ * <p>See {@link ContentCaptureManager#getContentCaptureConditions()} for more.
+ */
+public final class ContentCaptureCondition implements Parcelable {
+
+ /**
+ * When set, package should use the {@link LocusId#getId()} as a regular expression (using the
+ * {@link java.util.regex.Pattern} format).
+ */
+ public static final int FLAG_IS_REGEX = 0x2;
+
+ /** @hide */
+ @IntDef(prefix = { "FLAG" }, flag = true, value = {
+ FLAG_IS_REGEX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Flags {}
+
+ private final @NonNull LocusId mLocusId;
+ private final @Flags int mFlags;
+
+ /**
+ * Default constructor.
+ *
+ * @param locusId id of the condition, as defined by
+ * {@link ContentCaptureContext#getLocusId()}.
+ * @param flags either {@link ContentCaptureCondition#FLAG_IS_REGEX} (to use a regular
+ * expression match) or {@code 0} (in which case the {@code LocusId} must be an exact match of
+ * the {@code LocusId} used in the {@link ContentCaptureContext}).
+ */
+ public ContentCaptureCondition(@NonNull LocusId locusId, @Flags int flags) {
+ this.mLocusId = Preconditions.checkNotNull(locusId);
+ this.mFlags = flags;
+ }
+
+ /**
+ * Gets the {@code LocusId} per se.
+ */
+ @NonNull
+ public LocusId getLocusId() {
+ return mLocusId;
+ }
+
+ /**
+ * Gets the flags associates with this condition.
+ *
+ * @return either {@link ContentCaptureCondition#FLAG_IS_REGEX} or {@code 0}.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mFlags;
+ result = prime * result + ((mLocusId == null) ? 0 : mLocusId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final ContentCaptureCondition other = (ContentCaptureCondition) obj;
+ if (mFlags != other.mFlags) return false;
+ if (mLocusId == null) {
+ if (other.mLocusId != null) return false;
+ } else {
+ if (!mLocusId.equals(other.mLocusId)) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder string = new StringBuilder(mLocusId.toString()); // LocusID is PII safe
+ if (mFlags != 0) {
+ string
+ .append(" (")
+ .append(DebugUtils.flagsToString(ContentCaptureCondition.class, "FLAG_", mFlags))
+ .append(')');
+ }
+ return string.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ parcel.writeParcelable(mLocusId, flags);
+ parcel.writeInt(mFlags);
+ }
+
+ public static final @NonNull Parcelable.Creator<ContentCaptureCondition> CREATOR =
+ new Parcelable.Creator<ContentCaptureCondition>() {
+
+ @Override
+ public ContentCaptureCondition createFromParcel(@NonNull Parcel parcel) {
+ return new ContentCaptureCondition(parcel.readParcelable(null),
+ parcel.readInt());
+ }
+
+ @Override
+ public ContentCaptureCondition[] newArray(int size) {
+ return new ContentCaptureCondition[size];
+ }
+ };
+}
diff --git a/android/view/contentcapture/ContentCaptureContext.java b/android/view/contentcapture/ContentCaptureContext.java
new file mode 100644
index 0000000..9998fbc
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureContext.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.TaskInfo;
+import android.app.assist.ActivityId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.LocusId;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.Display;
+import android.view.View;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Context associated with a {@link ContentCaptureSession} - see {@link ContentCaptureManager} for
+ * more info.
+ */
+public final class ContentCaptureContext implements Parcelable {
+
+ /*
+ * IMPLEMENTATION NOTICE:
+ *
+ * This object contains both the info that's explicitly added by apps (hence it's public), but
+ * it also contains info injected by the server (and are accessible through @SystemApi methods).
+ */
+
+ /**
+ * Flag used to indicate that the app explicitly disabled content capture for the activity
+ * (using {@link ContentCaptureManager#setContentCaptureEnabled(boolean)}),
+ * in which case the service will just receive activity-level events.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_DISABLED_BY_APP = 0x1;
+
+ /**
+ * Flag used to indicate that the activity's window is tagged with
+ * {@link android.view.Display#FLAG_SECURE}, in which case the service will just receive
+ * activity-level events.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_DISABLED_BY_FLAG_SECURE = 0x2;
+
+ /**
+ * Flag used when the event is sent because the Android System reconnected to the service (for
+ * example, after its process died).
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_RECONNECTED = 0x4;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_DISABLED_BY_APP,
+ FLAG_DISABLED_BY_FLAG_SECURE,
+ FLAG_RECONNECTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ContextCreationFlags{}
+
+ /**
+ * Flag indicating if this object has the app-provided context (which is set on
+ * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}).
+ */
+ private final boolean mHasClientContext;
+
+ // Fields below are set by app on Builder
+ private final @Nullable Bundle mExtras;
+ private final @Nullable LocusId mId;
+
+ // Fields below are set by server when the session starts
+ private final @Nullable ComponentName mComponentName;
+ private final int mFlags;
+ private final int mDisplayId;
+ private final ActivityId mActivityId;
+
+ // Fields below are set by the service upon "delivery" and are not marshalled in the parcel
+ private int mParentSessionId = NO_SESSION_ID;
+
+ /** @hide */
+ public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
+ @NonNull ActivityId activityId, @NonNull ComponentName componentName, int displayId,
+ int flags) {
+ if (clientContext != null) {
+ mHasClientContext = true;
+ mExtras = clientContext.mExtras;
+ mId = clientContext.mId;
+ } else {
+ mHasClientContext = false;
+ mExtras = null;
+ mId = null;
+ }
+ mComponentName = Objects.requireNonNull(componentName);
+ mFlags = flags;
+ mDisplayId = displayId;
+ mActivityId = activityId;
+ }
+
+ private ContentCaptureContext(@NonNull Builder builder) {
+ mHasClientContext = true;
+ mExtras = builder.mExtras;
+ mId = builder.mId;
+
+ mComponentName = null;
+ mFlags = 0;
+ mDisplayId = Display.INVALID_DISPLAY;
+ mActivityId = null;
+ }
+
+ /** @hide */
+ public ContentCaptureContext(@Nullable ContentCaptureContext original, int extraFlags) {
+ mHasClientContext = original.mHasClientContext;
+ mExtras = original.mExtras;
+ mId = original.mId;
+ mComponentName = original.mComponentName;
+ mFlags = original.mFlags | extraFlags;
+ mDisplayId = original.mDisplayId;
+ mActivityId = original.mActivityId;
+ }
+
+ /**
+ * Gets the (optional) extras set by the app (through {@link Builder#setExtras(Bundle)}).
+ *
+ * <p>It can be used to provide vendor-specific data that can be modified and examined.
+ */
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Gets the context id.
+ */
+ @Nullable
+ public LocusId getLocusId() {
+ return mId;
+ }
+
+ /**
+ * Gets the id of the {@link TaskInfo task} associated with this context.
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getTaskId() {
+ return mHasClientContext ? 0 : mActivityId.getTaskId();
+ }
+
+ /**
+ * Gets the activity associated with this context, or {@code null} when it is a child session.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @Nullable ComponentName getActivityComponent() {
+ return mComponentName;
+ }
+
+ /**
+ * Gets the Activity id information associated with this context, or {@code null} when it is a
+ * child session.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public ActivityId getActivityId() {
+ return mHasClientContext ? null : mActivityId;
+ }
+
+ /**
+ * Gets the id of the session that originated this session (through
+ * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}),
+ * or {@code null} if this is the main session associated with the Activity's {@link Context}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @Nullable ContentCaptureSessionId getParentSessionId() {
+ return mParentSessionId == NO_SESSION_ID ? null
+ : new ContentCaptureSessionId(mParentSessionId);
+ }
+
+ /** @hide */
+ public void setParentSessionId(int parentSessionId) {
+ mParentSessionId = parentSessionId;
+ }
+
+ /**
+ * Gets the ID of the display associated with this context, as defined by
+ * {G android.hardware.display.DisplayManager#getDisplay(int) DisplayManager.getDisplay()}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Gets the flags associated with this context.
+ *
+ * @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE},
+ * {@link #FLAG_DISABLED_BY_APP} and {@link #FLAG_RECONNECTED}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @ContextCreationFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Helper that creates a {@link ContentCaptureContext} associated with the given {@code id}.
+ */
+ @NonNull
+ public static ContentCaptureContext forLocusId(@NonNull String id) {
+ return new Builder(new LocusId(id)).build();
+ }
+
+ /**
+ * Builder for {@link ContentCaptureContext} objects.
+ */
+ public static final class Builder {
+ private Bundle mExtras;
+ private final LocusId mId;
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder.
+ *
+ * <p>The context must have an id, which is usually one of the following:
+ *
+ * <ul>
+ * <li>A URL representing a web page (or {@code IFRAME}) that's being rendered by the
+ * activity (See {@link View#setContentCaptureSession(ContentCaptureSession)} for an
+ * example).
+ * <li>A unique identifier of the application state (for example, a conversation between
+ * 2 users in a chat app).
+ * </ul>
+ *
+ * <p>See {@link ContentCaptureManager} for more info about the content capture context.
+ *
+ * @param id id associated with this context.
+ */
+ public Builder(@NonNull LocusId id) {
+ mId = Preconditions.checkNotNull(id);
+ }
+
+ /**
+ * Sets extra options associated with this context.
+ *
+ * <p>It can be used to provide vendor-specific data that can be modified and examined.
+ *
+ * @param extras extra options.
+ * @return this builder.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = Preconditions.checkNotNull(extras);
+ throwIfDestroyed();
+ return this;
+ }
+
+ /**
+ * Builds the {@link ContentCaptureContext}.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return the built {@code ContentCaptureContext}
+ */
+ @NonNull
+ public ContentCaptureContext build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new ContentCaptureContext(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called #build()");
+ }
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(b/111276913): dump to proto as well
+ public void dump(PrintWriter pw) {
+ if (mComponentName != null) {
+ pw.print("activity="); pw.print(mComponentName.flattenToShortString());
+ }
+ if (mId != null) {
+ pw.print(", id="); mId.dump(pw);
+ }
+ pw.print(", activityId="); pw.print(mActivityId);
+ pw.print(", displayId="); pw.print(mDisplayId);
+ if (mParentSessionId != NO_SESSION_ID) {
+ pw.print(", parentId="); pw.print(mParentSessionId);
+ }
+ if (mFlags > 0) {
+ pw.print(", flags="); pw.print(mFlags);
+ }
+ if (mExtras != null) {
+ // NOTE: cannot dump because it could contain PII
+ pw.print(", hasExtras");
+ }
+ }
+
+ private boolean fromServer() {
+ return mComponentName != null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("Context[");
+
+ if (fromServer()) {
+ builder.append("act=").append(ComponentName.flattenToShortString(mComponentName))
+ .append(", activityId=").append(mActivityId)
+ .append(", displayId=").append(mDisplayId)
+ .append(", flags=").append(mFlags);
+ } else {
+ builder.append("id=").append(mId);
+ if (mExtras != null) {
+ // NOTE: cannot print because it could contain PII
+ builder.append(", hasExtras");
+ }
+ }
+ if (mParentSessionId != NO_SESSION_ID) {
+ builder.append(", parentId=").append(mParentSessionId);
+ }
+ return builder.append(']').toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mHasClientContext ? 1 : 0);
+ if (mHasClientContext) {
+ parcel.writeParcelable(mId, flags);
+ parcel.writeBundle(mExtras);
+ }
+ parcel.writeParcelable(mComponentName, flags);
+ if (fromServer()) {
+ parcel.writeInt(mDisplayId);
+ parcel.writeInt(mFlags);
+ mActivityId.writeToParcel(parcel, flags);
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ContentCaptureContext> CREATOR =
+ new Parcelable.Creator<ContentCaptureContext>() {
+
+ @Override
+ @NonNull
+ public ContentCaptureContext createFromParcel(Parcel parcel) {
+ final boolean hasClientContext = parcel.readInt() == 1;
+
+ final ContentCaptureContext clientContext;
+ if (hasClientContext) {
+ // Must reconstruct the client context using the Builder API
+ final LocusId id = parcel.readParcelable(null);
+ final Bundle extras = parcel.readBundle();
+ final Builder builder = new Builder(id);
+ if (extras != null) builder.setExtras(extras);
+ clientContext = new ContentCaptureContext(builder);
+ } else {
+ clientContext = null;
+ }
+ final ComponentName componentName = parcel.readParcelable(null);
+ if (componentName == null) {
+ // Client-state only
+ return clientContext;
+ } else {
+ final int displayId = parcel.readInt();
+ final int flags = parcel.readInt();
+ final ActivityId activityId = new ActivityId(parcel);
+
+ return new ContentCaptureContext(clientContext, activityId, componentName,
+ displayId, flags);
+ }
+ }
+
+ @Override
+ @NonNull
+ public ContentCaptureContext[] newArray(int size) {
+ return new ContentCaptureContext[size];
+ }
+ };
+}
diff --git a/android/view/contentcapture/ContentCaptureEvent.java b/android/view/contentcapture/ContentCaptureEvent.java
new file mode 100644
index 0000000..ce6d034
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureEvent.java
@@ -0,0 +1,656 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
+import static android.view.contentcapture.ContentCaptureManager.DEBUG;
+import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.graphics.Insets;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.inputmethod.BaseInputConnection;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/** @hide */
+@SystemApi
+public final class ContentCaptureEvent implements Parcelable {
+
+ private static final String TAG = ContentCaptureEvent.class.getSimpleName();
+
+ /** @hide */
+ public static final int TYPE_SESSION_FINISHED = -2;
+ /** @hide */
+ public static final int TYPE_SESSION_STARTED = -1;
+
+ /**
+ * Called when a node has been added to the screen and is visible to the user.
+ *
+ * <p>The metadata of the node is available through {@link #getViewNode()}.
+ */
+ public static final int TYPE_VIEW_APPEARED = 1;
+
+ /**
+ * Called when one or more nodes have been removed from the screen and is not visible to the
+ * user anymore.
+ *
+ * <p>To get the id(s), first call {@link #getIds()} - if it returns {@code null}, then call
+ * {@link #getId()}.
+ */
+ public static final int TYPE_VIEW_DISAPPEARED = 2;
+
+ /**
+ * Called when the text of a node has been changed.
+ *
+ * <p>The id of the node is available through {@link #getId()}, and the new text is
+ * available through {@link #getText()}.
+ */
+ public static final int TYPE_VIEW_TEXT_CHANGED = 3;
+
+ /**
+ * Called before events (such as {@link #TYPE_VIEW_APPEARED} and/or
+ * {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy are sent.
+ *
+ * <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent
+ * if the initial view hierarchy doesn't initially have any view that's important for content
+ * capture.
+ */
+ public static final int TYPE_VIEW_TREE_APPEARING = 4;
+
+ /**
+ * Called after events (such as {@link #TYPE_VIEW_APPEARED} and/or
+ * {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy were sent.
+ *
+ * <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent
+ * if the initial view hierarchy doesn't initially have any view that's important for content
+ * capture.
+ */
+ public static final int TYPE_VIEW_TREE_APPEARED = 5;
+
+ /**
+ * Called after a call to
+ * {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}.
+ *
+ * <p>The passed context is available through {@link #getContentCaptureContext()}.
+ */
+ public static final int TYPE_CONTEXT_UPDATED = 6;
+
+ /**
+ * Called after the session is ready, typically after the activity resumed and the
+ * initial views appeared
+ */
+ public static final int TYPE_SESSION_RESUMED = 7;
+
+ /**
+ * Called after the session is paused, typically after the activity paused and the
+ * views disappeared.
+ */
+ public static final int TYPE_SESSION_PAUSED = 8;
+
+ /**
+ * Called when the view's insets are changed. The new insets associated with the
+ * event may then be retrieved by calling {@link #getInsets()}
+ */
+ public static final int TYPE_VIEW_INSETS_CHANGED = 9;
+
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_VIEW_APPEARED,
+ TYPE_VIEW_DISAPPEARED,
+ TYPE_VIEW_TEXT_CHANGED,
+ TYPE_VIEW_TREE_APPEARING,
+ TYPE_VIEW_TREE_APPEARED,
+ TYPE_CONTEXT_UPDATED,
+ TYPE_SESSION_PAUSED,
+ TYPE_SESSION_RESUMED,
+ TYPE_VIEW_INSETS_CHANGED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EventType{}
+
+ /** @hide */
+ public static final int MAX_INVALID_VALUE = -1;
+
+ private final int mSessionId;
+ private final int mType;
+ private final long mEventTime;
+ private @Nullable AutofillId mId;
+ private @Nullable ArrayList<AutofillId> mIds;
+ private @Nullable ViewNode mNode;
+ private @Nullable CharSequence mText;
+ private int mParentSessionId = NO_SESSION_ID;
+ private @Nullable ContentCaptureContext mClientContext;
+ private @Nullable Insets mInsets;
+
+ private int mComposingStart = MAX_INVALID_VALUE;
+ private int mComposingEnd = MAX_INVALID_VALUE;
+ private int mSelectionStartIndex = MAX_INVALID_VALUE;
+ private int mSelectionEndIndex = MAX_INVALID_VALUE;
+
+ /** Only used in the main Content Capture session, no need to parcel */
+ private boolean mTextHasComposingSpan;
+
+ /** @hide */
+ public ContentCaptureEvent(int sessionId, int type, long eventTime) {
+ mSessionId = sessionId;
+ mType = type;
+ mEventTime = eventTime;
+ }
+
+ /** @hide */
+ public ContentCaptureEvent(int sessionId, int type) {
+ this(sessionId, type, System.currentTimeMillis());
+ }
+
+ /** @hide */
+ public ContentCaptureEvent setAutofillId(@NonNull AutofillId id) {
+ mId = Preconditions.checkNotNull(id);
+ return this;
+ }
+
+ /** @hide */
+ public ContentCaptureEvent setAutofillIds(@NonNull ArrayList<AutofillId> ids) {
+ mIds = Preconditions.checkNotNull(ids);
+ return this;
+ }
+
+ /**
+ * Adds an autofill id to the this event, merging the single id into a list if necessary.
+ *
+ * @hide
+ */
+ public ContentCaptureEvent addAutofillId(@NonNull AutofillId id) {
+ Preconditions.checkNotNull(id);
+ if (mIds == null) {
+ mIds = new ArrayList<>();
+ if (mId == null) {
+ Log.w(TAG, "addAutofillId(" + id + ") called without an initial id");
+ } else {
+ mIds.add(mId);
+ mId = null;
+ }
+ }
+ mIds.add(id);
+ return this;
+ }
+
+ /**
+ * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
+ *
+ * @hide
+ */
+ public ContentCaptureEvent setParentSessionId(int parentSessionId) {
+ mParentSessionId = parentSessionId;
+ return this;
+ }
+
+ /**
+ * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
+ *
+ * @hide
+ */
+ public ContentCaptureEvent setClientContext(@NonNull ContentCaptureContext clientContext) {
+ mClientContext = clientContext;
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public int getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
+ *
+ * @hide
+ */
+ @Nullable
+ public int getParentSessionId() {
+ return mParentSessionId;
+ }
+
+ /**
+ * Gets the {@link ContentCaptureContext} set calls to
+ * {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}.
+ *
+ * <p>Only set on {@link #TYPE_CONTEXT_UPDATED} events.
+ */
+ @Nullable
+ public ContentCaptureContext getContentCaptureContext() {
+ return mClientContext;
+ }
+
+ /** @hide */
+ @NonNull
+ public ContentCaptureEvent setViewNode(@NonNull ViewNode node) {
+ mNode = Preconditions.checkNotNull(node);
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public ContentCaptureEvent setText(@Nullable CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public ContentCaptureEvent setComposingIndex(int start, int end) {
+ mComposingStart = start;
+ mComposingEnd = end;
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public boolean hasComposingSpan() {
+ return mComposingStart > MAX_INVALID_VALUE;
+ }
+
+ /** @hide */
+ @NonNull
+ public ContentCaptureEvent setSelectionIndex(int start, int end) {
+ mSelectionStartIndex = start;
+ mSelectionEndIndex = end;
+ return this;
+ }
+
+ boolean hasSameComposingSpan(@NonNull ContentCaptureEvent other) {
+ return mComposingStart == other.mComposingStart && mComposingEnd == other.mComposingEnd;
+ }
+
+ boolean hasSameSelectionSpan(@NonNull ContentCaptureEvent other) {
+ return mSelectionStartIndex == other.mSelectionStartIndex
+ && mSelectionEndIndex == other.mSelectionEndIndex;
+ }
+
+ private int getComposingStart() {
+ return mComposingStart;
+ }
+
+ private int getComposingEnd() {
+ return mComposingEnd;
+ }
+
+ private int getSelectionStart() {
+ return mSelectionStartIndex;
+ }
+
+ private int getSelectionEnd() {
+ return mSelectionEndIndex;
+ }
+
+ private void restoreComposingSpan() {
+ if (mComposingStart <= MAX_INVALID_VALUE
+ || mComposingEnd <= MAX_INVALID_VALUE) {
+ return;
+ }
+ if (mText instanceof Spannable) {
+ BaseInputConnection.setComposingSpans((Spannable) mText, mComposingStart,
+ mComposingEnd);
+ } else {
+ Log.w(TAG, "Text is not a Spannable.");
+ }
+ }
+
+ private void restoreSelectionSpans() {
+ if (mSelectionStartIndex <= MAX_INVALID_VALUE
+ || mSelectionEndIndex <= MAX_INVALID_VALUE) {
+ return;
+ }
+
+ if (mText instanceof SpannableString) {
+ SpannableString ss = (SpannableString) mText;
+ ss.setSpan(Selection.SELECTION_START, mSelectionStartIndex, mSelectionStartIndex, 0);
+ ss.setSpan(Selection.SELECTION_END, mSelectionEndIndex, mSelectionEndIndex, 0);
+ } else {
+ Log.w(TAG, "Text is not a SpannableString.");
+ }
+ }
+
+ /** @hide */
+ @NonNull
+ public ContentCaptureEvent setInsets(@NonNull Insets insets) {
+ mInsets = insets;
+ return this;
+ }
+
+ /**
+ * Gets the type of the event.
+ *
+ * @return one of {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED},
+ * {@link #TYPE_VIEW_TEXT_CHANGED}, {@link #TYPE_VIEW_TREE_APPEARING},
+ * {@link #TYPE_VIEW_TREE_APPEARED}, {@link #TYPE_CONTEXT_UPDATED},
+ * {@link #TYPE_SESSION_RESUMED}, or {@link #TYPE_SESSION_PAUSED}.
+ */
+ public @EventType int getType() {
+ return mType;
+ }
+
+ /**
+ * Gets when the event was generated, in millis since epoch.
+ */
+ public long getEventTime() {
+ return mEventTime;
+ }
+
+ /**
+ * Gets the whole metadata of the node associated with the event.
+ *
+ * <p>Only set on {@link #TYPE_VIEW_APPEARED} events.
+ */
+ @Nullable
+ public ViewNode getViewNode() {
+ return mNode;
+ }
+
+ /**
+ * Gets the {@link AutofillId} of the node associated with the event.
+ *
+ * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED} (when the event contains just one node - if
+ * it contains more than one, this method returns {@code null} and the actual ids should be
+ * retrived by {@link #getIds()}) and {@link #TYPE_VIEW_TEXT_CHANGED} events.
+ */
+ @Nullable
+ public AutofillId getId() {
+ return mId;
+ }
+
+ /**
+ * Gets the {@link AutofillId AutofillIds} of the nodes associated with the event.
+ *
+ * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED}, when the event contains more than one node
+ * (if it contains just one node, it's returned by {@link #getId()} instead.
+ */
+ @Nullable
+ public List<AutofillId> getIds() {
+ return mIds;
+ }
+
+ /**
+ * Gets the current text of the node associated with the event.
+ *
+ * <p>Only set on {@link #TYPE_VIEW_TEXT_CHANGED} events.
+ */
+ @Nullable
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Gets the rectangle of the insets associated with the event. Valid insets will only be
+ * returned if the type of the event is {@link #TYPE_VIEW_INSETS_CHANGED}, otherwise they
+ * will be null.
+ */
+ @Nullable
+ public Insets getInsets() {
+ return mInsets;
+ }
+
+ /**
+ * Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED}
+ * or {@link #TYPE_VIEW_DISAPPEARED}.
+ *
+ * @hide
+ */
+ public void mergeEvent(@NonNull ContentCaptureEvent event) {
+ Preconditions.checkNotNull(event);
+ final int eventType = event.getType();
+ if (mType != eventType) {
+ Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType) + ") cannot be merged "
+ + "with different eventType=" + getTypeAsString(mType));
+ return;
+ }
+
+ if (eventType == TYPE_VIEW_DISAPPEARED) {
+ final List<AutofillId> ids = event.getIds();
+ final AutofillId id = event.getId();
+ if (ids != null) {
+ if (id != null) {
+ Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event);
+ }
+ for (int i = 0; i < ids.size(); i++) {
+ addAutofillId(ids.get(i));
+ }
+ return;
+ }
+ if (id != null) {
+ addAutofillId(id);
+ return;
+ }
+ throw new IllegalArgumentException("mergeEvent(): got "
+ + "TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
+ } else if (eventType == TYPE_VIEW_TEXT_CHANGED) {
+ setText(event.getText());
+ setComposingIndex(event.getComposingStart(), event.getComposingEnd());
+ setSelectionIndex(event.getSelectionStart(), event.getSelectionEnd());
+ } else {
+ Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType)
+ + ") does not support this event type.");
+ }
+ }
+
+ /** @hide */
+ public void dump(@NonNull PrintWriter pw) {
+ pw.print("type="); pw.print(getTypeAsString(mType));
+ pw.print(", time="); pw.print(mEventTime);
+ if (mId != null) {
+ pw.print(", id="); pw.print(mId);
+ }
+ if (mIds != null) {
+ pw.print(", ids="); pw.print(mIds);
+ }
+ if (mNode != null) {
+ pw.print(", mNode.id="); pw.print(mNode.getAutofillId());
+ }
+ if (mSessionId != NO_SESSION_ID) {
+ pw.print(", sessionId="); pw.print(mSessionId);
+ }
+ if (mParentSessionId != NO_SESSION_ID) {
+ pw.print(", parentSessionId="); pw.print(mParentSessionId);
+ }
+ if (mText != null) {
+ pw.print(", text="); pw.println(getSanitizedString(mText));
+ }
+ if (mClientContext != null) {
+ pw.print(", context="); mClientContext.dump(pw); pw.println();
+ }
+ if (mInsets != null) {
+ pw.print(", insets="); pw.println(mInsets);
+ }
+ if (mComposingStart > MAX_INVALID_VALUE) {
+ pw.print(", composing("); pw.print(mComposingStart);
+ pw.print(", "); pw.print(mComposingEnd); pw.print(")");
+ }
+ if (mSelectionStartIndex > MAX_INVALID_VALUE) {
+ pw.print(", selection("); pw.print(mSelectionStartIndex);
+ pw.print(", "); pw.print(mSelectionEndIndex); pw.print(")");
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringBuilder string = new StringBuilder("ContentCaptureEvent[type=")
+ .append(getTypeAsString(mType));
+ string.append(", session=").append(mSessionId);
+ if (mType == TYPE_SESSION_STARTED && mParentSessionId != NO_SESSION_ID) {
+ string.append(", parent=").append(mParentSessionId);
+ }
+ if (mId != null) {
+ string.append(", id=").append(mId);
+ }
+ if (mIds != null) {
+ string.append(", ids=").append(mIds);
+ }
+ if (mNode != null) {
+ final String className = mNode.getClassName();
+ string.append(", class=").append(className);
+ string.append(", id=").append(mNode.getAutofillId());
+ if (mNode.getText() != null) {
+ string.append(", text=")
+ .append(DEBUG ? mNode.getText() : getSanitizedString(mNode.getText()));
+ }
+ }
+ if (mText != null) {
+ string.append(", text=")
+ .append(DEBUG ? mText : getSanitizedString(mText));
+ }
+ if (mClientContext != null) {
+ string.append(", context=").append(mClientContext);
+ }
+ if (mInsets != null) {
+ string.append(", insets=").append(mInsets);
+ }
+ if (mComposingStart > MAX_INVALID_VALUE) {
+ string.append(", composing=[")
+ .append(mComposingStart).append(",").append(mComposingEnd).append("]");
+ }
+ if (mSelectionStartIndex > MAX_INVALID_VALUE) {
+ string.append(", selection=[")
+ .append(mSelectionStartIndex).append(",")
+ .append(mSelectionEndIndex).append("]");
+ }
+ return string.append(']').toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mSessionId);
+ parcel.writeInt(mType);
+ parcel.writeLong(mEventTime);
+ parcel.writeParcelable(mId, flags);
+ parcel.writeTypedList(mIds);
+ ViewNode.writeToParcel(parcel, mNode, flags);
+ parcel.writeCharSequence(mText);
+ if (mType == TYPE_SESSION_STARTED || mType == TYPE_SESSION_FINISHED) {
+ parcel.writeInt(mParentSessionId);
+ }
+ if (mType == TYPE_SESSION_STARTED || mType == TYPE_CONTEXT_UPDATED) {
+ parcel.writeParcelable(mClientContext, flags);
+ }
+ if (mType == TYPE_VIEW_INSETS_CHANGED) {
+ parcel.writeParcelable(mInsets, flags);
+ }
+ if (mType == TYPE_VIEW_TEXT_CHANGED) {
+ parcel.writeInt(mComposingStart);
+ parcel.writeInt(mComposingEnd);
+ parcel.writeInt(mSelectionStartIndex);
+ parcel.writeInt(mSelectionEndIndex);
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ContentCaptureEvent> CREATOR =
+ new Parcelable.Creator<ContentCaptureEvent>() {
+
+ @Override
+ @NonNull
+ public ContentCaptureEvent createFromParcel(Parcel parcel) {
+ final int sessionId = parcel.readInt();
+ final int type = parcel.readInt();
+ final long eventTime = parcel.readLong();
+ final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type, eventTime);
+ final AutofillId id = parcel.readParcelable(null);
+ if (id != null) {
+ event.setAutofillId(id);
+ }
+ final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR);
+ if (ids != null) {
+ event.setAutofillIds(ids);
+ }
+ final ViewNode node = ViewNode.readFromParcel(parcel);
+ if (node != null) {
+ event.setViewNode(node);
+ }
+ event.setText(parcel.readCharSequence());
+ if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) {
+ event.setParentSessionId(parcel.readInt());
+ }
+ if (type == TYPE_SESSION_STARTED || type == TYPE_CONTEXT_UPDATED) {
+ event.setClientContext(parcel.readParcelable(null));
+ }
+ if (type == TYPE_VIEW_INSETS_CHANGED) {
+ event.setInsets(parcel.readParcelable(null));
+ }
+ if (type == TYPE_VIEW_TEXT_CHANGED) {
+ event.setComposingIndex(parcel.readInt(), parcel.readInt());
+ event.restoreComposingSpan();
+ event.setSelectionIndex(parcel.readInt(), parcel.readInt());
+ event.restoreSelectionSpans();
+ }
+ return event;
+ }
+
+ @Override
+ @NonNull
+ public ContentCaptureEvent[] newArray(int size) {
+ return new ContentCaptureEvent[size];
+ }
+ };
+
+ /** @hide */
+ public static String getTypeAsString(@EventType int type) {
+ switch (type) {
+ case TYPE_SESSION_STARTED:
+ return "SESSION_STARTED";
+ case TYPE_SESSION_FINISHED:
+ return "SESSION_FINISHED";
+ case TYPE_SESSION_RESUMED:
+ return "SESSION_RESUMED";
+ case TYPE_SESSION_PAUSED:
+ return "SESSION_PAUSED";
+ case TYPE_VIEW_APPEARED:
+ return "VIEW_APPEARED";
+ case TYPE_VIEW_DISAPPEARED:
+ return "VIEW_DISAPPEARED";
+ case TYPE_VIEW_TEXT_CHANGED:
+ return "VIEW_TEXT_CHANGED";
+ case TYPE_VIEW_TREE_APPEARING:
+ return "VIEW_TREE_APPEARING";
+ case TYPE_VIEW_TREE_APPEARED:
+ return "VIEW_TREE_APPEARED";
+ case TYPE_CONTEXT_UPDATED:
+ return "CONTEXT_UPDATED";
+ case TYPE_VIEW_INSETS_CHANGED:
+ return "VIEW_INSETS_CHANGED";
+ default:
+ return "UKNOWN_TYPE: " + type;
+ }
+ }
+}
diff --git a/android/view/contentcapture/ContentCaptureHelper.java b/android/view/contentcapture/ContentCaptureHelper.java
new file mode 100644
index 0000000..c7ca220
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL;
+import static android.view.contentcapture.ContentCaptureManager.LOGGING_LEVEL_DEBUG;
+import static android.view.contentcapture.ContentCaptureManager.LOGGING_LEVEL_OFF;
+import static android.view.contentcapture.ContentCaptureManager.LOGGING_LEVEL_VERBOSE;
+
+import android.annotation.Nullable;
+import android.os.Build;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.contentcapture.ContentCaptureManager.LoggingLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class for this package and server's.
+ *
+ * @hide
+ */
+public final class ContentCaptureHelper {
+
+ private static final String TAG = ContentCaptureHelper.class.getSimpleName();
+
+ public static boolean sVerbose = false;
+ public static boolean sDebug = true;
+
+ /**
+ * Used to log text that could contain PII.
+ */
+ @Nullable
+ public static String getSanitizedString(@Nullable CharSequence text) {
+ return text == null ? null : text.length() + "_chars";
+ }
+
+ /**
+ * Gets the default logging level for the device.
+ */
+ @LoggingLevel
+ public static int getDefaultLoggingLevel() {
+ return Build.IS_DEBUGGABLE ? LOGGING_LEVEL_DEBUG : LOGGING_LEVEL_OFF;
+ }
+
+ /**
+ * Sets the value of the static logging level constants based on device config.
+ */
+ public static void setLoggingLevel() {
+ final int defaultLevel = getDefaultLoggingLevel();
+ final int level = DeviceConfig.getInt(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL, defaultLevel);
+ setLoggingLevel(level);
+ }
+
+ /**
+ * Sets the value of the static logging level constants based the given level.
+ */
+ public static void setLoggingLevel(@LoggingLevel int level) {
+ Log.i(TAG, "Setting logging level to " + getLoggingLevelAsString(level));
+ sVerbose = sDebug = false;
+ switch (level) {
+ case LOGGING_LEVEL_VERBOSE:
+ sVerbose = true;
+ // fall through
+ case LOGGING_LEVEL_DEBUG:
+ sDebug = true;
+ return;
+ case LOGGING_LEVEL_OFF:
+ // You log nothing, Jon Snow!
+ return;
+ default:
+ Log.w(TAG, "setLoggingLevel(): invalud level: " + level);
+ }
+ }
+
+ /**
+ * Gets a user-friendly value for a content capture logging level.
+ */
+ public static String getLoggingLevelAsString(@LoggingLevel int level) {
+ switch (level) {
+ case LOGGING_LEVEL_OFF:
+ return "OFF";
+ case LOGGING_LEVEL_DEBUG:
+ return "DEBUG";
+ case LOGGING_LEVEL_VERBOSE:
+ return "VERBOSE";
+ default:
+ return "UNKNOWN-" + level;
+ }
+ }
+
+ /**
+ * Converts a set to a list.
+ */
+ @Nullable
+ public static <T> ArrayList<T> toList(@Nullable Set<T> set) {
+ return set == null ? null : new ArrayList<T>(set);
+ }
+
+ /**
+ * Converts a list to a set.
+ */
+ @Nullable
+ public static <T> ArraySet<T> toSet(@Nullable List<T> list) {
+ return list == null ? null : new ArraySet<T>(list);
+ }
+
+ private ContentCaptureHelper() {
+ throw new UnsupportedOperationException("contains only static methods");
+ }
+}
diff --git a/android/view/contentcapture/ContentCaptureManager.java b/android/view/contentcapture/ContentCaptureManager.java
new file mode 100644
index 0000000..9241c30
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureManager.java
@@ -0,0 +1,943 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+import static android.view.contentcapture.ContentCaptureHelper.toSet;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.annotation.UiThread;
+import android.annotation.UserIdInt;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.Slog;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.WindowManager;
+import android.view.contentcapture.ContentCaptureSession.FlushReason;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.SyncResultReceiver;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * <p>The {@link ContentCaptureManager} provides additional ways for for apps to
+ * integrate with the content capture subsystem.
+ *
+ * <p>Content capture provides real-time, continuous capture of application activity, display and
+ * events to an intelligence service that is provided by the Android system. The intelligence
+ * service then uses that info to mediate and speed user journey through different apps. For
+ * example, when the user receives a restaurant address in a chat app and switches to a map app
+ * to search for that restaurant, the intelligence service could offer an autofill dialog to
+ * let the user automatically select its address.
+ *
+ * <p>Content capture was designed with two major concerns in mind: privacy and performance.
+ *
+ * <ul>
+ * <li><b>Privacy:</b> the intelligence service is a trusted component provided that is provided
+ * by the device manufacturer and that cannot be changed by the user (although the user can
+ * globaly disable content capture using the Android Settings app). This service can only use the
+ * data for in-device machine learning, which is enforced both by process isolation and
+ * <a href="https://source.android.com/compatibility/cdd">CDD requirements</a>.
+ * <li><b>Performance:</b> content capture is highly optimized to minimize its impact in the app
+ * jankiness and overall device system health. For example, its only enabled on apps (or even
+ * specific activities from an app) that were explicitly allowlisted by the intelligence service,
+ * and it buffers the events so they are sent in a batch to the service (see
+ * {@link #isContentCaptureEnabled()} for other cases when its disabled).
+ * </ul>
+ *
+ * <p>In fact, before using this manager, the app developer should check if it's available. Example:
+ * <pre><code>
+ * ContentCaptureManager mgr = context.getSystemService(ContentCaptureManager.class);
+ * if (mgr != null && mgr.isContentCaptureEnabled()) {
+ * // ...
+ * }
+ * </code></pre>
+ *
+ * <p>App developers usually don't need to explicitly interact with content capture, except when the
+ * app:
+ *
+ * <ul>
+ * <li>Can define a contextual {@link android.content.LocusId} to identify unique state (such as a
+ * conversation between 2 chat users).
+ * <li>Can have multiple view hierarchies with different contextual meaning (for example, a
+ * browser app with multiple tabs, each representing a different URL).
+ * <li>Contains custom views (that extend View directly and are not provided by the standard
+ * Android SDK.
+ * <li>Contains views that provide their own virtual hierarchy (like a web browser that render the
+ * HTML elements using a Canvas).
+ * </ul>
+ *
+ * <p>The main integration point with content capture is the {@link ContentCaptureSession}. A "main"
+ * session is automatically created by the Android System when content capture is enabled for the
+ * activity and its used by the standard Android views to notify the content capture service of
+ * events such as views being added, views been removed, and text changed by user input. The session
+ * could have a {@link ContentCaptureContext} to provide more contextual info about it, such as
+ * the locus associated with the view hierarchy (see {@link android.content.LocusId} for more info
+ * about locus). By default, the main session doesn't have a {@code ContentCaptureContext}, but you
+ * can change it after its created. Example:
+ *
+ * <pre><code>
+ * protected void onCreate(Bundle savedInstanceState) {
+ * // Initialize view structure
+ * ContentCaptureSession session = rootView.getContentCaptureSession();
+ * if (session != null) {
+ * session.setContentCaptureContext(ContentCaptureContext.forLocusId("chat_UserA_UserB"));
+ * }
+ * }
+ * </code></pre>
+ *
+ * <p>If your activity contains view hierarchies with a different contextual meaning, you should
+ * created child sessions for each view hierarchy root. For example, if your activity is a browser,
+ * you could use the main session for the main URL being rendered, then child sessions for each
+ * {@code IFRAME}:
+ *
+ * <pre><code>
+ * ContentCaptureSession mMainSession;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * // Initialize view structure...
+ * mMainSession = rootView.getContentCaptureSession();
+ * if (mMainSession != null) {
+ * mMainSession.setContentCaptureContext(
+ * ContentCaptureContext.forLocusId("https://example.com"));
+ * }
+ * }
+ *
+ * private void loadIFrame(View iframeRootView, String url) {
+ * if (mMainSession != null) {
+ * ContentCaptureSession iFrameSession = mMainSession.newChild(
+ * ContentCaptureContext.forLocusId(url));
+ * }
+ * iframeRootView.setContentCaptureSession(iFrameSession);
+ * }
+ * // Load iframe...
+ * }
+ * </code></pre>
+ *
+ * <p>If your activity has custom views (i.e., views that extend {@link View} directly and provide
+ * just one logical view, not a virtual tree hiearchy) and it provides content that's relevant for
+ * content capture (as of {@link android.os.Build.VERSION_CODES#Q Android Q}, the only relevant
+ * content is text), then your view implementation should:
+ *
+ * <ul>
+ * <li>Set it as important for content capture.
+ * <li>Fill {@link ViewStructure} used for content capture.
+ * <li>Notify the {@link ContentCaptureSession} when the text is changed by user input.
+ * </ul>
+ *
+ * <p>Here's an example of the relevant methods for an {@code EditText}-like view:
+ *
+ * <pre><code>
+ * public class MyEditText extends View {
+ *
+ * public MyEditText(...) {
+ * if (getImportantForContentCapture() == IMPORTANT_FOR_CONTENT_CAPTURE_AUTO) {
+ * setImportantForContentCapture(IMPORTANT_FOR_CONTENT_CAPTURE_YES);
+ * }
+ * }
+ *
+ * public void onProvideContentCaptureStructure(@NonNull ViewStructure structure, int flags) {
+ * super.onProvideContentCaptureStructure(structure, flags);
+ *
+ * structure.setText(getText(), getSelectionStart(), getSelectionEnd());
+ * structure.setHint(getHint());
+ * structure.setInputType(getInputType());
+ * // set other properties like setTextIdEntry(), setTextLines(), setTextStyle(),
+ * // setMinTextEms(), setMaxTextEms(), setMaxTextLength()
+ * }
+ *
+ * private void onTextChanged() {
+ * if (isLaidOut() && isImportantForContentCapture() && isTextEditable()) {
+ * ContentCaptureManager mgr = mContext.getSystemService(ContentCaptureManager.class);
+ * if (cm != null && cm.isContentCaptureEnabled()) {
+ * ContentCaptureSession session = getContentCaptureSession();
+ * if (session != null) {
+ * session.notifyViewTextChanged(getAutofillId(), getText());
+ * }
+ * }
+ * }
+ * </code></pre>
+ *
+ * <p>If your view provides its own virtual hierarchy (for example, if it's a browser that draws
+ * the HTML using {@link Canvas} or native libraries in a different render process), then the view
+ * is also responsible to notify the session when the virtual elements appear and disappear - see
+ * {@link View#onProvideContentCaptureStructure(ViewStructure, int)} for more info.
+ */
+@SystemService(Context.CONTENT_CAPTURE_MANAGER_SERVICE)
+public final class ContentCaptureManager {
+
+ private static final String TAG = ContentCaptureManager.class.getSimpleName();
+
+ /** @hide */
+ public static final boolean DEBUG = false;
+
+ /** Error happened during the data sharing session. */
+ public static final int DATA_SHARE_ERROR_UNKNOWN = 1;
+
+ /** Request has been rejected, because a concurrent data share sessions is in progress. */
+ public static final int DATA_SHARE_ERROR_CONCURRENT_REQUEST = 2;
+
+ /** Request has been interrupted because of data share session timeout. */
+ public static final int DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED = 3;
+
+ /** @hide */
+ @IntDef(flag = false, value = {
+ DATA_SHARE_ERROR_UNKNOWN,
+ DATA_SHARE_ERROR_CONCURRENT_REQUEST,
+ DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DataShareError {}
+
+ /** @hide */
+ public static final int RESULT_CODE_OK = 0;
+ /** @hide */
+ public static final int RESULT_CODE_TRUE = 1;
+ /** @hide */
+ public static final int RESULT_CODE_FALSE = 2;
+ /** @hide */
+ public static final int RESULT_CODE_SECURITY_EXCEPTION = -1;
+
+ /**
+ * ID used to indicate that a session does not exist
+ * @hide
+ */
+ @SystemApi
+ public static final int NO_SESSION_ID = 0;
+
+ /**
+ * Timeout for calls to system_server.
+ */
+ private static final int SYNC_CALLS_TIMEOUT_MS = 5000;
+
+ /**
+ * DeviceConfig property used by {@code com.android.server.SystemServer} on start to decide
+ * whether the content capture service should be created or not
+ *
+ * <p>By default it should *NOT* be set (or set to {@code "default"}, so the decision is based
+ * on whether the OEM provides an implementation for the service), but it can be overridden to:
+ *
+ * <ul>
+ * <li>Provide a "kill switch" so OEMs can disable it remotely in case of emergency (when
+ * it's set to {@code "false"}).
+ * <li>Enable the CTS tests to be run on AOSP builds (when it's set to {@code "true"}).
+ * </ul>
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED =
+ "service_explicitly_enabled";
+
+ /**
+ * Maximum number of events that are buffered before sent to the app.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE = "max_buffer_size";
+
+ /**
+ * Frequency (in ms) of buffer flushes when no events are received.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY = "idle_flush_frequency";
+
+ /**
+ * Frequency (in ms) of buffer flushes when no events are received and the last one was a
+ * text change event.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY =
+ "text_change_flush_frequency";
+
+ /**
+ * Size of events that are logging on {@code dump}.
+ *
+ * <p>Set it to {@code 0} or less to disable history.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE = "log_history_size";
+
+ /**
+ * Sets the logging level for {@code logcat} statements.
+ *
+ * <p>Valid values are: {@link #LOGGING_LEVEL_OFF}, {@value #LOGGING_LEVEL_DEBUG}, and
+ * {@link #LOGGING_LEVEL_VERBOSE}.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final String DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL = "logging_level";
+
+ /**
+ * Sets how long (in ms) the service is bound while idle.
+ *
+ * <p>Use {@code 0} to keep it permanently bound.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT = "idle_unbind_timeout";
+
+ /** @hide */
+ @TestApi
+ public static final int LOGGING_LEVEL_OFF = 0;
+
+ /** @hide */
+ @TestApi
+ public static final int LOGGING_LEVEL_DEBUG = 1;
+
+ /** @hide */
+ @TestApi
+ public static final int LOGGING_LEVEL_VERBOSE = 2;
+
+ /** @hide */
+ @IntDef(flag = false, value = {
+ LOGGING_LEVEL_OFF,
+ LOGGING_LEVEL_DEBUG,
+ LOGGING_LEVEL_VERBOSE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LoggingLevel {}
+
+
+ /** @hide */
+ public static final int DEFAULT_MAX_BUFFER_SIZE = 500; // Enough for typical busy screen.
+ /** @hide */
+ public static final int DEFAULT_IDLE_FLUSHING_FREQUENCY_MS = 5_000;
+ /** @hide */
+ public static final int DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS = 1_000;
+ /** @hide */
+ public static final int DEFAULT_LOG_HISTORY_SIZE = 10;
+
+ private final Object mLock = new Object();
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final IContentCaptureManager mService;
+
+ @GuardedBy("mLock")
+ private final LocalDataShareAdapterResourceManager mDataShareAdapterResourceManager;
+
+ @NonNull
+ final ContentCaptureOptions mOptions;
+
+ // Flags used for starting session.
+ @GuardedBy("mLock")
+ private int mFlags;
+
+ // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
+ // held at the Application level
+ @NonNull
+ private final Handler mHandler;
+
+ @GuardedBy("mLock")
+ private MainContentCaptureSession mMainSession;
+
+ /** @hide */
+ public interface ContentCaptureClient {
+ /**
+ * Gets the component name of the client.
+ */
+ @NonNull
+ ComponentName contentCaptureClientGetComponentName();
+ }
+
+ /** @hide */
+ public ContentCaptureManager(@NonNull Context context,
+ @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
+ mContext = Preconditions.checkNotNull(context, "context cannot be null");
+ mService = Preconditions.checkNotNull(service, "service cannot be null");
+ mOptions = Preconditions.checkNotNull(options, "options cannot be null");
+
+ ContentCaptureHelper.setLoggingLevel(mOptions.loggingLevel);
+
+ if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
+
+ // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we
+ // do, then we should optimize it to run the tests after the Choreographer finishes the most
+ // important steps of the frame.
+ mHandler = Handler.createAsync(Looper.getMainLooper());
+
+ mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager();
+ }
+
+ /**
+ * Gets the main session associated with the context.
+ *
+ * <p>By default there's just one (associated with the activity lifecycle), but apps could
+ * explicitly add more using
+ * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}.
+ *
+ * @hide
+ */
+ @NonNull
+ @UiThread
+ public MainContentCaptureSession getMainContentCaptureSession() {
+ synchronized (mLock) {
+ if (mMainSession == null) {
+ mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService);
+ if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
+ }
+ return mMainSession;
+ }
+ }
+
+ /** @hide */
+ @UiThread
+ public void onActivityCreated(@NonNull IBinder applicationToken,
+ @NonNull IBinder shareableActivityToken, @NonNull ComponentName activityComponent) {
+ if (mOptions.lite) return;
+ synchronized (mLock) {
+ getMainContentCaptureSession().start(applicationToken, shareableActivityToken,
+ activityComponent, mFlags);
+ }
+ }
+
+ /** @hide */
+ @UiThread
+ public void onActivityResumed() {
+ if (mOptions.lite) return;
+ getMainContentCaptureSession().notifySessionResumed();
+ }
+
+ /** @hide */
+ @UiThread
+ public void onActivityPaused() {
+ if (mOptions.lite) return;
+ getMainContentCaptureSession().notifySessionPaused();
+ }
+
+ /** @hide */
+ @UiThread
+ public void onActivityDestroyed() {
+ if (mOptions.lite) return;
+ getMainContentCaptureSession().destroy();
+ }
+
+ /**
+ * Flushes the content of all sessions.
+ *
+ * <p>Typically called by {@code Activity} when it's paused / resumed.
+ *
+ * @hide
+ */
+ @UiThread
+ public void flush(@FlushReason int reason) {
+ if (mOptions.lite) return;
+ getMainContentCaptureSession().flush(reason);
+ }
+
+ /**
+ * Returns the component name of the system service that is consuming the captured events for
+ * the current user.
+ */
+ @Nullable
+ public ComponentName getServiceComponentName() {
+ if (!isContentCaptureEnabled() && !mOptions.lite) return null;
+
+ final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.getServiceComponentName(resultReceiver);
+ return resultReceiver.getParcelableResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get service componentName.");
+ }
+ }
+
+ /**
+ * Gets the (optional) intent used to launch the service-specific settings.
+ *
+ * <p>This method is static because it's called by Settings, which might not be allowlisted
+ * for content capture (in which case the ContentCaptureManager on its context would be null).
+ *
+ * @hide
+ */
+ // TODO: use "lite" options as it's done by activities from the content capture service
+ @Nullable
+ public static ComponentName getServiceSettingsComponentName() {
+ final IBinder binder = ServiceManager
+ .checkService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
+ if (binder == null) return null;
+
+ final IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(binder);
+ final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ service.getServiceSettingsActivity(resultReceiver);
+ final int resultCode = resultReceiver.getIntResult();
+ if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) {
+ throw new SecurityException(resultReceiver.getStringResult());
+ }
+ return resultReceiver.getParcelableResult();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.e(TAG, "Fail to get service settings componentName: " + e);
+ return null;
+ }
+ }
+
+ /**
+ * Checks whether content capture is enabled for this activity.
+ *
+ * <p>There are many reasons it could be disabled, such as:
+ * <ul>
+ * <li>App itself disabled content capture through {@link #setContentCaptureEnabled(boolean)}.
+ * <li>Intelligence service did not allowlist content capture for this activity's package.
+ * <li>Intelligence service did not allowlist content capture for this specific activity.
+ * <li>Intelligence service disabled content capture globally.
+ * <li>User disabled content capture globally through the Android Settings app.
+ * <li>Device manufacturer (OEM) disabled content capture globally.
+ * <li>Transient errors, such as intelligence service package being updated.
+ * </ul>
+ */
+ public boolean isContentCaptureEnabled() {
+ if (mOptions.lite) return false;
+
+ final MainContentCaptureSession mainSession;
+ synchronized (mLock) {
+ mainSession = mMainSession;
+ }
+ // The main session is only set when the activity starts, so we need to return true until
+ // then.
+ if (mainSession != null && mainSession.isDisabled()) return false;
+
+ return true;
+ }
+
+ /**
+ * Gets the list of conditions for when content capture should be allowed.
+ *
+ * <p>This method is typically used by web browsers so they don't generate unnecessary content
+ * capture events for websites the content capture service is not interested on.
+ *
+ * @return list of conditions, or {@code null} if the service didn't set any restriction
+ * (in which case content capture events should always be generated). If the list is empty,
+ * then it should not generate any event at all.
+ */
+ @Nullable
+ public Set<ContentCaptureCondition> getContentCaptureConditions() {
+ // NOTE: we could cache the conditions on ContentCaptureOptions, but then it would be stick
+ // to the lifetime of the app. OTOH, by dynamically calling the server every time, we allow
+ // the service to fine tune how long-lived apps (like browsers) are allowlisted.
+ if (!isContentCaptureEnabled() && !mOptions.lite) return null;
+
+ final SyncResultReceiver resultReceiver = syncRun(
+ (r) -> mService.getContentCaptureConditions(mContext.getPackageName(), r));
+
+ try {
+ final ArrayList<ContentCaptureCondition> result = resultReceiver
+ .getParcelableListResult();
+ return toSet(result);
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get content capture conditions.");
+ }
+ }
+
+ /**
+ * Called by apps to explicitly enable or disable content capture.
+ *
+ * <p><b>Note: </b> this call is not persisted accross reboots, so apps should typically call
+ * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}.
+ */
+ public void setContentCaptureEnabled(boolean enabled) {
+ if (sDebug) {
+ Log.d(TAG, "setContentCaptureEnabled(): setting to " + enabled + " for " + mContext);
+ }
+
+ MainContentCaptureSession mainSession;
+ synchronized (mLock) {
+ if (enabled) {
+ mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_APP;
+ } else {
+ mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_APP;
+ }
+ mainSession = mMainSession;
+ }
+ if (mainSession != null) {
+ mainSession.setDisabled(!enabled);
+ }
+ }
+
+ /**
+ * Called by apps to update flag secure when window attributes change.
+ *
+ * @hide
+ */
+ public void updateWindowAttributes(@NonNull WindowManager.LayoutParams params) {
+ if (sDebug) {
+ Log.d(TAG, "updateWindowAttributes(): window flags=" + params.flags);
+ }
+ final boolean flagSecureEnabled =
+ (params.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0;
+
+ MainContentCaptureSession mainSession;
+ synchronized (mLock) {
+ if (flagSecureEnabled) {
+ mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE;
+ } else {
+ mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE;
+ }
+ mainSession = mMainSession;
+ }
+ if (mainSession != null) {
+ mainSession.setDisabled(flagSecureEnabled);
+ }
+ }
+
+ /**
+ * Gets whether content capture is enabled for the given user.
+ *
+ * <p>This method is typically used by the content capture service settings page, so it can
+ * provide a toggle to enable / disable it.
+ *
+ * @throws SecurityException if caller is not the app that owns the content capture service
+ * associated with the user.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isContentCaptureFeatureEnabled() {
+ final SyncResultReceiver resultReceiver = syncRun(
+ (r) -> mService.isContentCaptureFeatureEnabled(r));
+
+ try {
+ final int resultCode = resultReceiver.getIntResult();
+ switch (resultCode) {
+ case RESULT_CODE_TRUE:
+ return true;
+ case RESULT_CODE_FALSE:
+ return false;
+ default:
+ Log.wtf(TAG, "received invalid result: " + resultCode);
+ return false;
+ }
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.e(TAG, "Fail to get content capture feature enable status: " + e);
+ return false;
+ }
+ }
+
+ /**
+ * Called by the app to request the content capture service to remove content capture data
+ * associated with some context.
+ *
+ * @param request object specifying what user data should be removed.
+ */
+ public void removeData(@NonNull DataRemovalRequest request) {
+ Preconditions.checkNotNull(request);
+
+ try {
+ mService.removeData(request);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by the app to request data sharing via writing to a file.
+ *
+ * <p>The ContentCaptureService app will receive a read-only file descriptor pointing to the
+ * same file and will be able to read data being shared from it.
+ *
+ * <p>Note: using this API doesn't guarantee the app staying alive and is "best-effort".
+ * Starting a foreground service would minimize the chances of the app getting killed during the
+ * file sharing session.
+ *
+ * @param request object specifying details of the data being shared.
+ */
+ public void shareData(@NonNull DataShareRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DataShareWriteAdapter dataShareWriteAdapter) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkNotNull(dataShareWriteAdapter);
+ Preconditions.checkNotNull(executor);
+
+ try {
+ mService.shareData(request,
+ new DataShareAdapterDelegate(executor, dataShareWriteAdapter,
+ mDataShareAdapterResourceManager));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Runs a sync method in the service, properly handling exceptions.
+ *
+ * @throws SecurityException if caller is not allowed to execute the method.
+ */
+ @NonNull
+ private SyncResultReceiver syncRun(@NonNull MyRunnable r) {
+ final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ r.run(resultReceiver);
+ final int resultCode = resultReceiver.getIntResult();
+ if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) {
+ throw new SecurityException(resultReceiver.getStringResult());
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ throw new RuntimeException("Fail to get syn run result from SyncResultReceiver.");
+ }
+ return resultReceiver;
+ }
+
+ /** @hide */
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.println("ContentCaptureManager");
+ final String prefix2 = prefix + " ";
+ synchronized (mLock) {
+ pw.print(prefix2); pw.print("isContentCaptureEnabled(): ");
+ pw.println(isContentCaptureEnabled());
+ pw.print(prefix2); pw.print("Debug: "); pw.print(sDebug);
+ pw.print(" Verbose: "); pw.println(sVerbose);
+ pw.print(prefix2); pw.print("Context: "); pw.println(mContext);
+ pw.print(prefix2); pw.print("User: "); pw.println(mContext.getUserId());
+ pw.print(prefix2); pw.print("Service: "); pw.println(mService);
+ pw.print(prefix2); pw.print("Flags: "); pw.println(mFlags);
+ pw.print(prefix2); pw.print("Options: "); mOptions.dumpShort(pw); pw.println();
+ if (mMainSession != null) {
+ final String prefix3 = prefix2 + " ";
+ pw.print(prefix2); pw.println("Main session:");
+ mMainSession.dump(prefix3, pw);
+ } else {
+ pw.print(prefix2); pw.println("No sessions");
+ }
+ }
+ }
+
+ /**
+ * Resets the temporary content capture service implementation to the default component.
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_CONTENT_CAPTURE)
+ public static void resetTemporaryService(@UserIdInt int userId) {
+ final IContentCaptureManager service = getService();
+ if (service == null) {
+ Log.e(TAG, "IContentCaptureManager is null");
+ }
+ try {
+ service.resetTemporaryService(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Temporarily sets the content capture service implementation.
+ *
+ * @param userId user Id to set the temporary service on.
+ * @param serviceName name of the new component
+ * @param duration how long the change will be valid (the service will be automatically reset
+ * to the default component after this timeout expires).
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_CONTENT_CAPTURE)
+ public static void setTemporaryService(
+ @UserIdInt int userId, @NonNull String serviceName, int duration) {
+ final IContentCaptureManager service = getService();
+ if (service == null) {
+ Log.e(TAG, "IContentCaptureManager is null");
+ }
+ try {
+ service.setTemporaryService(userId, serviceName, duration);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets whether the default content capture service should be used.
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_CONTENT_CAPTURE)
+ public static void setDefaultServiceEnabled(@UserIdInt int userId, boolean enabled) {
+ final IContentCaptureManager service = getService();
+ if (service == null) {
+ Log.e(TAG, "IContentCaptureManager is null");
+ }
+ try {
+ service.setDefaultServiceEnabled(userId, enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static IContentCaptureManager getService() {
+ return IContentCaptureManager.Stub.asInterface(ServiceManager.getService(
+ Service.CONTENT_CAPTURE_MANAGER_SERVICE));
+ }
+
+ private interface MyRunnable {
+ void run(@NonNull SyncResultReceiver receiver) throws RemoteException;
+ }
+
+ private static class DataShareAdapterDelegate extends IDataShareWriteAdapter.Stub {
+
+ private final WeakReference<LocalDataShareAdapterResourceManager> mResourceManagerReference;
+
+ private DataShareAdapterDelegate(Executor executor, DataShareWriteAdapter adapter,
+ LocalDataShareAdapterResourceManager resourceManager) {
+ Preconditions.checkNotNull(executor);
+ Preconditions.checkNotNull(adapter);
+ Preconditions.checkNotNull(resourceManager);
+
+ resourceManager.initializeForDelegate(this, adapter, executor);
+ mResourceManagerReference = new WeakReference<>(resourceManager);
+ }
+
+ @Override
+ public void write(ParcelFileDescriptor destination)
+ throws RemoteException {
+ executeAdapterMethodLocked(adapter -> adapter.onWrite(destination), "onWrite");
+ }
+
+ @Override
+ public void error(int errorCode) throws RemoteException {
+ executeAdapterMethodLocked(adapter -> adapter.onError(errorCode), "onError");
+ clearHardReferences();
+ }
+
+ @Override
+ public void rejected() throws RemoteException {
+ executeAdapterMethodLocked(DataShareWriteAdapter::onRejected, "onRejected");
+ clearHardReferences();
+ }
+
+ @Override
+ public void finish() throws RemoteException {
+ clearHardReferences();
+ }
+
+ private void executeAdapterMethodLocked(Consumer<DataShareWriteAdapter> adapterFn,
+ String methodName) {
+ LocalDataShareAdapterResourceManager resourceManager = mResourceManagerReference.get();
+ if (resourceManager == null) {
+ Slog.w(TAG, "Can't execute " + methodName + "(), resource manager has been GC'ed");
+ return;
+ }
+
+ DataShareWriteAdapter adapter = resourceManager.getAdapter(this);
+ Executor executor = resourceManager.getExecutor(this);
+
+ if (adapter == null || executor == null) {
+ Slog.w(TAG, "Can't execute " + methodName + "(), references are null");
+ return;
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> adapterFn.accept(adapter));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ private void clearHardReferences() {
+ LocalDataShareAdapterResourceManager resourceManager = mResourceManagerReference.get();
+ if (resourceManager == null) {
+ Slog.w(TAG, "Can't clear references, resource manager has been GC'ed");
+ return;
+ }
+
+ resourceManager.clearHardReferences(this);
+ }
+ }
+
+ /**
+ * Wrapper class making sure dependencies on the current application stay in the application
+ * context.
+ */
+ private static class LocalDataShareAdapterResourceManager {
+
+ // Keeping hard references to the remote objects in the current process (static context)
+ // to prevent them to be gc'ed during the lifetime of the application. This is an
+ // artifact of only operating with weak references remotely: there has to be at least 1
+ // hard reference in order for this to not be killed.
+ private Map<DataShareAdapterDelegate, DataShareWriteAdapter> mWriteAdapterHardReferences =
+ new HashMap<>();
+ private Map<DataShareAdapterDelegate, Executor> mExecutorHardReferences =
+ new HashMap<>();
+
+ void initializeForDelegate(DataShareAdapterDelegate delegate, DataShareWriteAdapter adapter,
+ Executor executor) {
+ mWriteAdapterHardReferences.put(delegate, adapter);
+ mExecutorHardReferences.put(delegate, executor);
+ }
+
+ Executor getExecutor(DataShareAdapterDelegate delegate) {
+ return mExecutorHardReferences.get(delegate);
+ }
+
+ DataShareWriteAdapter getAdapter(DataShareAdapterDelegate delegate) {
+ return mWriteAdapterHardReferences.get(delegate);
+ }
+
+ void clearHardReferences(DataShareAdapterDelegate delegate) {
+ mWriteAdapterHardReferences.remove(delegate);
+ mExecutorHardReferences.remove(delegate);
+ }
+ }
+}
diff --git a/android/view/contentcapture/ContentCaptureSession.java b/android/view/contentcapture/ContentCaptureSession.java
new file mode 100644
index 0000000..cc47f09
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureSession.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+
+/**
+ * Session used when notifying the Android system about events associated with views.
+ */
+public abstract class ContentCaptureSession implements AutoCloseable {
+
+ private static final String TAG = ContentCaptureSession.class.getSimpleName();
+
+ // TODO(b/158778794): to make the session ids truly globally unique across
+ // processes, we may need to explore other options.
+ private static final SecureRandom ID_GENERATOR = new SecureRandom();
+
+ /**
+ * Initial state, when there is no session.
+ *
+ * @hide
+ */
+ // NOTE: not prefixed by STATE_ so it's not printed on getStateAsString()
+ public static final int UNKNOWN_STATE = 0x0;
+
+ /**
+ * Service's startSession() was called, but server didn't confirm it was created yet.
+ *
+ * @hide
+ */
+ public static final int STATE_WAITING_FOR_SERVER = 0x1;
+
+ /**
+ * Session is active.
+ *
+ * @hide
+ */
+ public static final int STATE_ACTIVE = 0x2;
+
+ /**
+ * Session is disabled because there is no service for this user.
+ *
+ * @hide
+ */
+ public static final int STATE_DISABLED = 0x4;
+
+ /**
+ * Session is disabled because its id already existed on server.
+ *
+ * @hide
+ */
+ public static final int STATE_DUPLICATED_ID = 0x8;
+
+ /**
+ * Session is disabled because service is not set for user.
+ *
+ * @hide
+ */
+ public static final int STATE_NO_SERVICE = 0x10;
+
+ /**
+ * Session is disabled by FLAG_SECURE
+ *
+ * @hide
+ */
+ public static final int STATE_FLAG_SECURE = 0x20;
+
+ /**
+ * Session is disabled manually by the specific app
+ * (through {@link ContentCaptureManager#setContentCaptureEnabled(boolean)}).
+ *
+ * @hide
+ */
+ public static final int STATE_BY_APP = 0x40;
+
+ /**
+ * Session is disabled because session start was never replied.
+ *
+ * @hide
+ */
+ public static final int STATE_NO_RESPONSE = 0x80;
+
+ /**
+ * Session is disabled because an internal error.
+ *
+ * @hide
+ */
+ public static final int STATE_INTERNAL_ERROR = 0x100;
+
+ /**
+ * Session is disabled because service didn't allowlist package or activity.
+ *
+ * @hide
+ */
+ public static final int STATE_NOT_WHITELISTED = 0x200;
+
+ /**
+ * Session is disabled because the service died.
+ *
+ * @hide
+ */
+ public static final int STATE_SERVICE_DIED = 0x400;
+
+ /**
+ * Session is disabled because the service package is being udpated.
+ *
+ * @hide
+ */
+ public static final int STATE_SERVICE_UPDATING = 0x800;
+
+ /**
+ * Session is enabled, after the service died and came back to live.
+ *
+ * @hide
+ */
+ public static final int STATE_SERVICE_RESURRECTED = 0x1000;
+
+ private static final int INITIAL_CHILDREN_CAPACITY = 5;
+
+ /** @hide */
+ public static final int FLUSH_REASON_FULL = 1;
+ /** @hide */
+ public static final int FLUSH_REASON_VIEW_ROOT_ENTERED = 2;
+ /** @hide */
+ public static final int FLUSH_REASON_SESSION_STARTED = 3;
+ /** @hide */
+ public static final int FLUSH_REASON_SESSION_FINISHED = 4;
+ /** @hide */
+ public static final int FLUSH_REASON_IDLE_TIMEOUT = 5;
+ /** @hide */
+ public static final int FLUSH_REASON_TEXT_CHANGE_TIMEOUT = 6;
+ /** @hide */
+ public static final int FLUSH_REASON_SESSION_CONNECTED = 7;
+
+ /** @hide */
+ @IntDef(prefix = { "FLUSH_REASON_" }, value = {
+ FLUSH_REASON_FULL,
+ FLUSH_REASON_VIEW_ROOT_ENTERED,
+ FLUSH_REASON_SESSION_STARTED,
+ FLUSH_REASON_SESSION_FINISHED,
+ FLUSH_REASON_IDLE_TIMEOUT,
+ FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
+ FLUSH_REASON_SESSION_CONNECTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FlushReason{}
+
+ private final Object mLock = new Object();
+
+ /**
+ * Guard use to ignore events after it's destroyed.
+ */
+ @NonNull
+ @GuardedBy("mLock")
+ private boolean mDestroyed;
+
+ /** @hide */
+ @Nullable
+ protected final int mId;
+
+ private int mState = UNKNOWN_STATE;
+
+ // Lazily created on demand.
+ private ContentCaptureSessionId mContentCaptureSessionId;
+
+ /**
+ * {@link ContentCaptureContext} set by client, or {@code null} when it's the
+ * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
+ * context.
+ */
+ @Nullable
+ private ContentCaptureContext mClientContext;
+
+ /**
+ * List of children session.
+ */
+ @Nullable
+ @GuardedBy("mLock")
+ private ArrayList<ContentCaptureSession> mChildren;
+
+ /** @hide */
+ protected ContentCaptureSession() {
+ this(getRandomSessionId());
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public ContentCaptureSession(int id) {
+ Preconditions.checkArgument(id != NO_SESSION_ID);
+ mId = id;
+ }
+
+ // Used by ChildCOntentCaptureSession
+ ContentCaptureSession(@NonNull ContentCaptureContext initialContext) {
+ this();
+ mClientContext = Preconditions.checkNotNull(initialContext);
+ }
+
+ /** @hide */
+ @NonNull
+ abstract MainContentCaptureSession getMainCaptureSession();
+
+ /**
+ * Gets the id used to identify this session.
+ */
+ @NonNull
+ public final ContentCaptureSessionId getContentCaptureSessionId() {
+ if (mContentCaptureSessionId == null) {
+ mContentCaptureSessionId = new ContentCaptureSessionId(mId);
+ }
+ return mContentCaptureSessionId;
+ }
+
+ /** @hide */
+ @NonNull
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Creates a new {@link ContentCaptureSession}.
+ *
+ * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info.
+ */
+ @NonNull
+ public final ContentCaptureSession createContentCaptureSession(
+ @NonNull ContentCaptureContext context) {
+ final ContentCaptureSession child = newChild(context);
+ if (sDebug) {
+ Log.d(TAG, "createContentCaptureSession(" + context + ": parent=" + mId + ", child="
+ + child.mId);
+ }
+ synchronized (mLock) {
+ if (mChildren == null) {
+ mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY);
+ }
+ mChildren.add(child);
+ }
+ return child;
+ }
+
+ abstract ContentCaptureSession newChild(@NonNull ContentCaptureContext context);
+
+ /**
+ * Flushes the buffered events to the service.
+ */
+ abstract void flush(@FlushReason int reason);
+
+ /**
+ * Sets the {@link ContentCaptureContext} associated with the session.
+ *
+ * <p>Typically used to change the context associated with the default session from an activity.
+ */
+ public final void setContentCaptureContext(@Nullable ContentCaptureContext context) {
+ mClientContext = context;
+ updateContentCaptureContext(context);
+ }
+
+ abstract void updateContentCaptureContext(@Nullable ContentCaptureContext context);
+
+ /**
+ * Gets the {@link ContentCaptureContext} associated with the session.
+ *
+ * @return context set on constructor or by
+ * {@link #setContentCaptureContext(ContentCaptureContext)}, or {@code null} if never
+ * explicitly set.
+ */
+ @Nullable
+ public final ContentCaptureContext getContentCaptureContext() {
+ return mClientContext;
+ }
+
+ /**
+ * Destroys this session, flushing out all pending notifications to the service.
+ *
+ * <p>Once destroyed, any new notification will be dropped.
+ */
+ public final void destroy() {
+ synchronized (mLock) {
+ if (mDestroyed) {
+ if (sDebug) Log.d(TAG, "destroy(" + mId + "): already destroyed");
+ return;
+ }
+ mDestroyed = true;
+
+ // TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
+ // id) and send it to the cache of batched commands
+ if (sVerbose) {
+ Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
+ }
+ // Finish children first
+ if (mChildren != null) {
+ final int numberChildren = mChildren.size();
+ if (sVerbose) Log.v(TAG, "Destroying " + numberChildren + " children first");
+ for (int i = 0; i < numberChildren; i++) {
+ final ContentCaptureSession child = mChildren.get(i);
+ try {
+ child.destroy();
+ } catch (Exception e) {
+ Log.w(TAG, "exception destroying child session #" + i + ": " + e);
+ }
+ }
+ }
+ }
+
+ onDestroy();
+ }
+
+ abstract void onDestroy();
+
+ /** @hide */
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ /**
+ * Notifies the Content Capture Service that a node has been added to the view structure.
+ *
+ * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
+ * automatically by the Android System for views that return {@code true} on
+ * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}.
+ *
+ * @param node node that has been added.
+ */
+ public final void notifyViewAppeared(@NonNull ViewStructure node) {
+ Preconditions.checkNotNull(node);
+ if (!isContentCaptureEnabled()) return;
+
+ if (!(node instanceof ViewNode.ViewStructureImpl)) {
+ throw new IllegalArgumentException("Invalid node class: " + node.getClass());
+ }
+
+ internalNotifyViewAppeared((ViewStructureImpl) node);
+ }
+
+ abstract void internalNotifyViewAppeared(@NonNull ViewNode.ViewStructureImpl node);
+
+ /**
+ * Notifies the Content Capture Service that a node has been removed from the view structure.
+ *
+ * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
+ * automatically by the Android System for standard views.
+ *
+ * @param id id of the node that has been removed.
+ */
+ public final void notifyViewDisappeared(@NonNull AutofillId id) {
+ Preconditions.checkNotNull(id);
+ if (!isContentCaptureEnabled()) return;
+
+ internalNotifyViewDisappeared(id);
+ }
+
+ abstract void internalNotifyViewDisappeared(@NonNull AutofillId id);
+
+ /**
+ * Notifies the Content Capture Service that many nodes has been removed from a virtual view
+ * structure.
+ *
+ * <p>Should only be called by views that handle their own virtual view hierarchy.
+ *
+ * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be
+ * obtained by calling {@link ViewStructure#getAutofillId()}).
+ * @param virtualIds ids of the virtual children.
+ *
+ * @throws IllegalArgumentException if the {@code hostId} is an autofill id for a virtual view.
+ * @throws IllegalArgumentException if {@code virtualIds} is empty
+ */
+ public final void notifyViewsDisappeared(@NonNull AutofillId hostId,
+ @NonNull long[] virtualIds) {
+ Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId);
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(virtualIds), "virtual ids cannot be empty");
+ if (!isContentCaptureEnabled()) return;
+
+ // TODO(b/123036895): use a internalNotifyViewsDisappeared that optimizes how the event is
+ // parcelized
+ for (long id : virtualIds) {
+ internalNotifyViewDisappeared(new AutofillId(hostId, id, mId));
+ }
+ }
+
+ /**
+ * Notifies the Intelligence Service that the value of a text node has been changed.
+ *
+ * @param id of the node.
+ * @param text new text.
+ */
+ public final void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
+ Preconditions.checkNotNull(id);
+
+ if (!isContentCaptureEnabled()) return;
+
+ internalNotifyViewTextChanged(id, text);
+ }
+
+ abstract void internalNotifyViewTextChanged(@NonNull AutofillId id,
+ @Nullable CharSequence text);
+
+ /**
+ * Notifies the Intelligence Service that the insets of a view have changed.
+ */
+ public final void notifyViewInsetsChanged(@NonNull Insets viewInsets) {
+ Preconditions.checkNotNull(viewInsets);
+
+ if (!isContentCaptureEnabled()) return;
+
+ internalNotifyViewInsetsChanged(viewInsets);
+ }
+
+ abstract void internalNotifyViewInsetsChanged(@NonNull Insets viewInsets);
+
+ /** @hide */
+ public abstract void internalNotifyViewTreeEvent(boolean started);
+
+ /**
+ * Notifies the Content Capture Service that a session has resumed.
+ */
+ public final void notifySessionResumed() {
+ if (!isContentCaptureEnabled()) return;
+
+ internalNotifySessionResumed();
+ }
+
+ abstract void internalNotifySessionResumed();
+
+ /**
+ * Notifies the Content Capture Service that a session has paused.
+ */
+ public final void notifySessionPaused() {
+ if (!isContentCaptureEnabled()) return;
+
+ internalNotifySessionPaused();
+ }
+
+ abstract void internalNotifySessionPaused();
+
+ /**
+ * Creates a {@link ViewStructure} for a "standard" view.
+ *
+ * <p>This method should be called after a visible view is laid out; the view then must populate
+ * the structure and pass it to {@link #notifyViewAppeared(ViewStructure)}.
+ *
+ * <b>Note: </b>views that manage a virtual structure under this view must populate just the
+ * node representing this view and return right away, then asynchronously report (not
+ * necessarily in the UI thread) when the children nodes appear, disappear or have their text
+ * changed by calling {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)},
+ * {@link ContentCaptureSession#notifyViewDisappeared(AutofillId)}, and
+ * {@link ContentCaptureSession#notifyViewTextChanged(AutofillId, CharSequence)} respectively.
+ * The structure for the a child must be created using
+ * {@link ContentCaptureSession#newVirtualViewStructure(AutofillId, long)}, and the
+ * {@code autofillId} for a child can be obtained either through
+ * {@code childStructure.getAutofillId()} or
+ * {@link ContentCaptureSession#newAutofillId(AutofillId, long)}.
+ *
+ * <p>When the virtual view hierarchy represents a web page, you should also:
+ *
+ * <ul>
+ * <li>Call {@link ContentCaptureManager#getContentCaptureConditions()} to infer content capture
+ * events should be generate for that URL.
+ * <li>Create a new {@link ContentCaptureSession} child for every HTML element that renders a
+ * new URL (like an {@code IFRAME}) and use that session to notify events from that subtree.
+ * </ul>
+ *
+ * <p><b>Note: </b>the following methods of the {@code structure} will be ignored:
+ * <ul>
+ * <li>{@link ViewStructure#setChildCount(int)}
+ * <li>{@link ViewStructure#addChildCount(int)}
+ * <li>{@link ViewStructure#getChildCount()}
+ * <li>{@link ViewStructure#newChild(int)}
+ * <li>{@link ViewStructure#asyncNewChild(int)}
+ * <li>{@link ViewStructure#asyncCommit()}
+ * <li>{@link ViewStructure#setWebDomain(String)}
+ * <li>{@link ViewStructure#newHtmlInfoBuilder(String)}
+ * <li>{@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}
+ * <li>{@link ViewStructure#setDataIsSensitive(boolean)}
+ * <li>{@link ViewStructure#setAlpha(float)}
+ * <li>{@link ViewStructure#setElevation(float)}
+ * <li>{@link ViewStructure#setTransformation(android.graphics.Matrix)}
+ * </ul>
+ */
+ @NonNull
+ public final ViewStructure newViewStructure(@NonNull View view) {
+ return new ViewNode.ViewStructureImpl(view);
+ }
+
+ /**
+ * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify
+ * the children in the session.
+ *
+ * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be
+ * obtained by calling {@link ViewStructure#getAutofillId()}).
+ * @param virtualChildId id of the virtual child, relative to the parent.
+ *
+ * @return if for the virtual child
+ *
+ * @throws IllegalArgumentException if the {@code parentId} is a virtual child id.
+ */
+ public @NonNull AutofillId newAutofillId(@NonNull AutofillId hostId, long virtualChildId) {
+ Preconditions.checkNotNull(hostId);
+ Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId);
+ return new AutofillId(hostId, virtualChildId, mId);
+ }
+
+ /**
+ * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
+ * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy.
+ *
+ * @param parentId id of the virtual view parent (it can be obtained by calling
+ * {@link ViewStructure#getAutofillId()} on the parent).
+ * @param virtualId id of the virtual child, relative to the parent.
+ *
+ * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
+ */
+ @NonNull
+ public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId,
+ long virtualId) {
+ return new ViewNode.ViewStructureImpl(parentId, virtualId, mId);
+ }
+
+ boolean isContentCaptureEnabled() {
+ synchronized (mLock) {
+ return !mDestroyed;
+ }
+ }
+
+ @CallSuper
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("id: "); pw.println(mId);
+ if (mClientContext != null) {
+ pw.print(prefix); mClientContext.dump(pw); pw.println();
+ }
+ synchronized (mLock) {
+ pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
+ if (mChildren != null && !mChildren.isEmpty()) {
+ final String prefix2 = prefix + " ";
+ final int numberChildren = mChildren.size();
+ pw.print(prefix); pw.print("number children: "); pw.println(numberChildren);
+ for (int i = 0; i < numberChildren; i++) {
+ final ContentCaptureSession child = mChildren.get(i);
+ pw.print(prefix); pw.print(i); pw.println(": "); child.dump(prefix2, pw);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(mId);
+ }
+
+ /** @hide */
+ @NonNull
+ protected static String getStateAsString(int state) {
+ return state + " (" + (state == UNKNOWN_STATE ? "UNKNOWN"
+ : DebugUtils.flagsToString(ContentCaptureSession.class, "STATE_", state)) + ")";
+ }
+
+ /** @hide */
+ @NonNull
+ public static String getFlushReasonAsString(@FlushReason int reason) {
+ switch (reason) {
+ case FLUSH_REASON_FULL:
+ return "FULL";
+ case FLUSH_REASON_VIEW_ROOT_ENTERED:
+ return "VIEW_ROOT";
+ case FLUSH_REASON_SESSION_STARTED:
+ return "STARTED";
+ case FLUSH_REASON_SESSION_FINISHED:
+ return "FINISHED";
+ case FLUSH_REASON_IDLE_TIMEOUT:
+ return "IDLE";
+ case FLUSH_REASON_TEXT_CHANGE_TIMEOUT:
+ return "TEXT_CHANGE";
+ case FLUSH_REASON_SESSION_CONNECTED:
+ return "CONNECTED";
+ default:
+ return "UNKOWN-" + reason;
+ }
+ }
+
+ private static int getRandomSessionId() {
+ int id;
+ do {
+ id = ID_GENERATOR.nextInt();
+ } while (id == NO_SESSION_ID);
+ return id;
+ }
+}
diff --git a/android/view/contentcapture/ContentCaptureSessionId.java b/android/view/contentcapture/ContentCaptureSessionId.java
new file mode 100644
index 0000000..413a2f3
--- /dev/null
+++ b/android/view/contentcapture/ContentCaptureSessionId.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.PrintWriter;
+
+/**
+ * Identifier for a Content Capture session.
+ */
+public final class ContentCaptureSessionId implements Parcelable {
+
+ private final @NonNull int mValue;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param value The internal value.
+ *
+ * @hide
+ */
+ public ContentCaptureSessionId(@NonNull int value) {
+ mValue = value;
+ }
+
+ /**
+ * @hide
+ */
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mValue;
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final ContentCaptureSessionId other = (ContentCaptureSessionId) obj;
+ if (mValue != other.mValue) return false;
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p><b>NOTE: </b>this method is only useful for debugging purposes and is not guaranteed to
+ * be stable, hence it should not be used to identify the session.
+ */
+ @Override
+ public String toString() {
+ return Integer.toString(mValue);
+ }
+
+
+ /** @hide */
+ // TODO(b/111276913): dump to proto as well
+ public void dump(PrintWriter pw) {
+ pw.print(mValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mValue);
+ }
+
+ public static final @NonNull Parcelable.Creator<ContentCaptureSessionId> CREATOR =
+ new Parcelable.Creator<ContentCaptureSessionId>() {
+
+ @Override
+ @NonNull
+ public ContentCaptureSessionId createFromParcel(Parcel parcel) {
+ return new ContentCaptureSessionId(parcel.readInt());
+ }
+
+ @Override
+ @NonNull
+ public ContentCaptureSessionId[] newArray(int size) {
+ return new ContentCaptureSessionId[size];
+ }
+ };
+}
diff --git a/android/view/contentcapture/CustomTestActivity.java b/android/view/contentcapture/CustomTestActivity.java
new file mode 100644
index 0000000..e509837
--- /dev/null
+++ b/android/view/contentcapture/CustomTestActivity.java
@@ -0,0 +1,88 @@
+/*
+ * 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.view.contentcapture;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.perftests.contentcapture.R;
+
+/**
+ * A simple activity used for testing, e.g. performance of activity switching, or as a base
+ * container of testing view.
+ */
+public class CustomTestActivity extends Activity {
+ public static final String INTENT_EXTRA_LAYOUT_ID = "layout_id";
+ public static final String INTENT_EXTRA_CUSTOM_VIEWS = "custom_view_number";
+ public static final int MAX_VIEWS = 500;
+ private static final int CUSTOM_CONTAINER_LAYOUT_ID = R.layout.test_container_activity;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getIntent().hasExtra(INTENT_EXTRA_LAYOUT_ID)) {
+ final int layoutId = getIntent().getIntExtra(INTENT_EXTRA_LAYOUT_ID,
+ /* defaultValue= */0);
+ setContentView(layoutId);
+ if (layoutId == CUSTOM_CONTAINER_LAYOUT_ID) {
+ createCustomViews(findViewById(R.id.root_view),
+ getIntent().getIntExtra(INTENT_EXTRA_CUSTOM_VIEWS, MAX_VIEWS));
+ }
+ }
+ }
+
+ private void createCustomViews(LinearLayout root, int number) {
+ LinearLayout horizontalLayout = null;
+ for (int i = 0; i < number; i++) {
+ final int j = i % 8;
+ if (horizontalLayout != null && j == 0) {
+ root.addView(horizontalLayout);
+ horizontalLayout = null;
+ }
+ if (horizontalLayout == null) {
+ horizontalLayout = createHorizontalLayout();
+ }
+ horizontalLayout.addView(createItem(null, i));
+ }
+ if (horizontalLayout != null) {
+ root.addView(horizontalLayout);
+ }
+ }
+
+ private LinearLayout createHorizontalLayout() {
+ final LinearLayout layout = new LinearLayout(getApplicationContext());
+ layout.setOrientation(LinearLayout.HORIZONTAL);
+ return layout;
+ }
+
+ private LinearLayout createItem(Drawable drawable, int index) {
+ final LinearLayout group = new LinearLayout(getApplicationContext());
+ group.setOrientation(LinearLayout.VERTICAL);
+ group.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT, /* weight= */ 1.0f));
+
+ final TextView text = new TextView(this);
+ text.setText("i = " + index);
+ group.addView(text);
+
+ return group;
+ }
+}
diff --git a/android/view/contentcapture/DataRemovalRequest.java b/android/view/contentcapture/DataRemovalRequest.java
new file mode 100644
index 0000000..f403dac
--- /dev/null
+++ b/android/view/contentcapture/DataRemovalRequest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.ActivityThread;
+import android.content.LocusId;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.IntArray;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class used by apps to request the content capture service to remove data associated with
+ * {@link LocusId LocusIds}.
+ *
+ * <p>An app which has tagged data with a LocusId can therefore delete them later. This is intended
+ * to let apps propagate deletions of user data into the operating system.
+ */
+public final class DataRemovalRequest implements Parcelable {
+
+ /**
+ * When set, service should use the {@link LocusId#getId()} as prefix for the data to be
+ * removed.
+ */
+ public static final int FLAG_IS_PREFIX = 0x1;
+
+ /** @hide */
+ @IntDef(prefix = { "FLAG" }, flag = true, value = {
+ FLAG_IS_PREFIX
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Flags {}
+
+ private final String mPackageName;
+
+ private final boolean mForEverything;
+ private ArrayList<LocusIdRequest> mLocusIdRequests;
+
+ private DataRemovalRequest(@NonNull Builder builder) {
+ mPackageName = ActivityThread.currentActivityThread().getApplication().getPackageName();
+ mForEverything = builder.mForEverything;
+ if (builder.mLocusIds != null) {
+ final int size = builder.mLocusIds.size();
+ mLocusIdRequests = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ mLocusIdRequests.add(new LocusIdRequest(builder.mLocusIds.get(i),
+ builder.mFlags.get(i)));
+ }
+ }
+ }
+
+ private DataRemovalRequest(@NonNull Parcel parcel) {
+ mPackageName = parcel.readString();
+ mForEverything = parcel.readBoolean();
+ if (!mForEverything) {
+ final int size = parcel.readInt();
+ mLocusIdRequests = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ mLocusIdRequests.add(new LocusIdRequest((LocusId) parcel.readValue(null),
+ parcel.readInt()));
+ }
+ }
+ }
+
+ /**
+ * Gets the name of the app that's making the request.
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Checks if app is requesting to remove content capture data associated with its package.
+ */
+ public boolean isForEverything() {
+ return mForEverything;
+ }
+
+ /**
+ * Gets the list of {@code LousId}s the apps is requesting to remove.
+ */
+ @NonNull
+ public List<LocusIdRequest> getLocusIdRequests() {
+ return mLocusIdRequests;
+ }
+
+ /**
+ * Builder for {@link DataRemovalRequest} objects.
+ */
+ public static final class Builder {
+
+ private boolean mForEverything;
+ private ArrayList<LocusId> mLocusIds;
+ private IntArray mFlags;
+
+ private boolean mDestroyed;
+
+ /**
+ * Requests servive to remove all content capture data associated with the app's package.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder forEverything() {
+ throwIfDestroyed();
+ Preconditions.checkState(mLocusIds == null, "Already added LocusIds");
+
+ mForEverything = true;
+ return this;
+ }
+
+ /**
+ * Request service to remove data associated with a given {@link LocusId}.
+ *
+ * @param locusId the {@link LocusId} being requested to be removed.
+ * @param flags either {@link DataRemovalRequest#FLAG_IS_PREFIX} or {@code 0}
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder addLocusId(@NonNull LocusId locusId, @Flags int flags) {
+ throwIfDestroyed();
+ Preconditions.checkState(!mForEverything, "Already is for everything");
+ Preconditions.checkNotNull(locusId);
+ // felipeal: check flags
+
+ if (mLocusIds == null) {
+ mLocusIds = new ArrayList<>();
+ mFlags = new IntArray();
+ }
+
+ mLocusIds.add(locusId);
+ mFlags.add(flags);
+ return this;
+ }
+
+ /**
+ * Builds the {@link DataRemovalRequest}.
+ */
+ @NonNull
+ public DataRemovalRequest build() {
+ throwIfDestroyed();
+
+ Preconditions.checkState(mForEverything || mLocusIds != null,
+ "must call either #forEverything() or add one #addLocusId()");
+
+ mDestroyed = true;
+ return new DataRemovalRequest(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already destroyed!");
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mPackageName);
+ parcel.writeBoolean(mForEverything);
+ if (!mForEverything) {
+ final int size = mLocusIdRequests.size();
+ parcel.writeInt(size);
+ for (int i = 0; i < size; i++) {
+ final LocusIdRequest request = mLocusIdRequests.get(i);
+ parcel.writeValue(request.getLocusId());
+ parcel.writeInt(request.getFlags());
+ }
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<DataRemovalRequest> CREATOR =
+ new Parcelable.Creator<DataRemovalRequest>() {
+
+ @Override
+ @NonNull
+ public DataRemovalRequest createFromParcel(Parcel parcel) {
+ return new DataRemovalRequest(parcel);
+ }
+
+ @Override
+ @NonNull
+ public DataRemovalRequest[] newArray(int size) {
+ return new DataRemovalRequest[size];
+ }
+ };
+
+ /**
+ * Representation of a request to remove data associated with a {@link LocusId}.
+ */
+ public final class LocusIdRequest {
+ private final @NonNull LocusId mLocusId;
+ private final @Flags int mFlags;
+
+ private LocusIdRequest(@NonNull LocusId locusId, @Flags int flags) {
+ this.mLocusId = locusId;
+ this.mFlags = flags;
+ }
+
+ /**
+ * Gets the {@code LocusId} per se.
+ */
+ @NonNull
+ public LocusId getLocusId() {
+ return mLocusId;
+ }
+
+ /**
+ * Gets the flags associates with request.
+ *
+ * @return either {@link DataRemovalRequest#FLAG_IS_PREFIX} or {@code 0}.
+ */
+ @NonNull
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+ }
+}
diff --git a/android/view/contentcapture/DataShareRequest.java b/android/view/contentcapture/DataShareRequest.java
new file mode 100644
index 0000000..78c0ef9
--- /dev/null
+++ b/android/view/contentcapture/DataShareRequest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.content.LocusId;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Preconditions;
+
+/** Container class representing a request to share data with Content Capture service. */
+@DataClass(
+ genConstructor = false,
+ genEqualsHashCode = true,
+ genHiddenConstDefs = true,
+ genParcelable = true,
+ genToString = true
+)
+public final class DataShareRequest implements Parcelable {
+
+ /** Name of the package making the request. */
+ @NonNull private final String mPackageName;
+
+ /** Locus id helping to identify what data is being shared. */
+ @Nullable private final LocusId mLocusId;
+
+ /** MIME type of the data being shared. */
+ @NonNull private final String mMimeType;
+
+ /** Constructs a request to share data with the Content Capture Service. */
+ public DataShareRequest(@Nullable LocusId locusId, @NonNull String mimeType) {
+ Preconditions.checkNotNull(mimeType);
+
+ mPackageName = ActivityThread.currentActivityThread().getApplication().getPackageName();
+ mLocusId = locusId;
+ mMimeType = mimeType;
+ }
+
+
+
+ // Code below generated by codegen v1.0.14.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/contentcapture/DataShareRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Name of the package making the request.
+ */
+ @DataClass.Generated.Member
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Locus id helping to identify what data is being shared.
+ */
+ @DataClass.Generated.Member
+ public @Nullable LocusId getLocusId() {
+ return mLocusId;
+ }
+
+ /**
+ * MIME type of the data being shared.
+ */
+ @DataClass.Generated.Member
+ public @NonNull String getMimeType() {
+ return mMimeType;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "DataShareRequest { " +
+ "packageName = " + mPackageName + ", " +
+ "locusId = " + mLocusId + ", " +
+ "mimeType = " + mMimeType +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(DataShareRequest other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ DataShareRequest that = (DataShareRequest) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Objects.equals(mPackageName, that.mPackageName)
+ && java.util.Objects.equals(mLocusId, that.mLocusId)
+ && java.util.Objects.equals(mMimeType, that.mMimeType);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mPackageName);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mLocusId);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mMimeType);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mLocusId != null) flg |= 0x2;
+ dest.writeByte(flg);
+ dest.writeString(mPackageName);
+ if (mLocusId != null) dest.writeTypedObject(mLocusId, flags);
+ dest.writeString(mMimeType);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ DataShareRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ String packageName = in.readString();
+ LocusId locusId = (flg & 0x2) == 0 ? null : (LocusId) in.readTypedObject(LocusId.CREATOR);
+ String mimeType = in.readString();
+
+ this.mPackageName = packageName;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mPackageName);
+ this.mLocusId = locusId;
+ this.mMimeType = mimeType;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mMimeType);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<DataShareRequest> CREATOR
+ = new Parcelable.Creator<DataShareRequest>() {
+ @Override
+ public DataShareRequest[] newArray(int size) {
+ return new DataShareRequest[size];
+ }
+
+ @Override
+ public DataShareRequest createFromParcel(@NonNull Parcel in) {
+ return new DataShareRequest(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1579870254459L,
+ codegenVersion = "1.0.14",
+ sourceFile = "frameworks/base/core/java/android/view/contentcapture/DataShareRequest.java",
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable android.content.LocusId mLocusId\nprivate final @android.annotation.NonNull java.lang.String mMimeType\nclass DataShareRequest extends java.lang.Object implements [android.os.Parcelable]\[email protected](genConstructor=false, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/contentcapture/DataShareWriteAdapter.java b/android/view/contentcapture/DataShareWriteAdapter.java
new file mode 100644
index 0000000..3b5b756
--- /dev/null
+++ b/android/view/contentcapture/DataShareWriteAdapter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.os.ParcelFileDescriptor;
+import android.view.contentcapture.ContentCaptureManager.DataShareError;
+
+/** Adapter class used by apps to share data with the Content Capture service. */
+public interface DataShareWriteAdapter {
+
+ /**
+ * Method invoked when the data share session has been started and the app needs to start
+ * writing into the file used for sharing.
+ *
+ * <p>App needs to handle explicitly cases when the file descriptor is closed and handle
+ * gracefully if IOExceptions happen.
+ *
+ * @param destination file descriptor used to write data into.
+ */
+ void onWrite(@NonNull ParcelFileDescriptor destination);
+
+ /** Data share sessions has been rejected by the Content Capture service. */
+ void onRejected();
+
+ /**
+ * Method invoked when an error occurred, for example sessions has not been started or
+ * terminated unsuccessfully.
+ *
+ * @param errorCode the error code corresponding to an ERROR_* value.
+ */
+ default void onError(@DataShareError int errorCode) {
+ /* do nothing - stub */
+ }
+}
diff --git a/android/view/contentcapture/LoginTest.java b/android/view/contentcapture/LoginTest.java
new file mode 100644
index 0000000..7257509
--- /dev/null
+++ b/android/view/contentcapture/LoginTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.CREATED;
+import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.DESTROYED;
+
+import android.perftests.utils.BenchmarkState;
+import android.view.View;
+
+import androidx.test.filters.LargeTest;
+
+import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
+import com.android.perftests.contentcapture.R;
+
+import org.junit.Test;
+
+@LargeTest
+public class LoginTest extends AbstractContentCapturePerfTestCase {
+
+ @Test
+ public void testLaunchActivity() throws Throwable {
+ enableService();
+
+ testActivityLaunchTime(R.layout.test_login_activity, 0);
+ }
+
+ @Test
+ public void testLaunchActivity_contain100Views() throws Throwable {
+ enableService();
+
+ testActivityLaunchTime(R.layout.test_container_activity, 100);
+ }
+
+ @Test
+ public void testLaunchActivity_contain300Views() throws Throwable {
+ enableService();
+
+ testActivityLaunchTime(R.layout.test_container_activity, 300);
+ }
+
+ @Test
+ public void testLaunchActivity_contain500Views() throws Throwable {
+ enableService();
+
+ testActivityLaunchTime(R.layout.test_container_activity, 500);
+ }
+
+ @Test
+ public void testLaunchActivity_noService() throws Throwable {
+ testActivityLaunchTime(R.layout.test_login_activity, 0);
+ }
+
+ @Test
+ public void testLaunchActivity_noService_contain100Views() throws Throwable {
+ testActivityLaunchTime(R.layout.test_container_activity, 100);
+ }
+
+ @Test
+ public void testLaunchActivity_noService_contain300Views() throws Throwable {
+ testActivityLaunchTime(R.layout.test_container_activity, 300);
+ }
+
+ @Test
+ public void testLaunchActivity_noService_contain500Views() throws Throwable {
+ testActivityLaunchTime(R.layout.test_container_activity, 500);
+ }
+
+ private void testActivityLaunchTime(int layoutId, int numViews) throws Throwable {
+ final ActivityWatcher watcher = startWatcher();
+
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ launchActivity(layoutId, numViews);
+
+ // Ignore the time to finish the activity
+ state.pauseTiming();
+ watcher.waitFor(CREATED);
+ finishActivity();
+ watcher.waitFor(DESTROYED);
+ state.resumeTiming();
+ }
+ }
+
+ @Test
+ public void testOnVisibilityAggregated_visibleChanged() throws Throwable {
+ enableService();
+ final CustomTestActivity activity = launchActivity();
+ final View root = activity.getWindow().getDecorView();
+ final View username = root.findViewById(R.id.username);
+
+ testOnVisibilityAggregated(username);
+ }
+
+ @Test
+ public void testOnVisibilityAggregated_visibleChanged_noService() throws Throwable {
+ final CustomTestActivity activity = launchActivity();
+ final View root = activity.getWindow().getDecorView();
+ final View username = root.findViewById(R.id.username);
+
+ testOnVisibilityAggregated(username);
+ }
+
+ @Test
+ public void testOnVisibilityAggregated_visibleChanged_noOptions() throws Throwable {
+ enableService();
+ clearOptions();
+ final CustomTestActivity activity = launchActivity();
+ final View root = activity.getWindow().getDecorView();
+ final View username = root.findViewById(R.id.username);
+
+ testOnVisibilityAggregated(username);
+ }
+
+ @Test
+ public void testOnVisibilityAggregated_visibleChanged_notImportant() throws Throwable {
+ enableService();
+ final CustomTestActivity activity = launchActivity();
+ final View root = activity.getWindow().getDecorView();
+ final View username = root.findViewById(R.id.username);
+ username.setImportantForContentCapture(View.IMPORTANT_FOR_CONTENT_CAPTURE_NO);
+
+ testOnVisibilityAggregated(username);
+ }
+
+ private void testOnVisibilityAggregated(View view) throws Throwable {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ // Only count the time of onVisibilityAggregated()
+ state.pauseTiming();
+ mActivityRule.runOnUiThread(() -> {
+ state.resumeTiming();
+ view.onVisibilityAggregated(false);
+ state.pauseTiming();
+ });
+ mActivityRule.runOnUiThread(() -> {
+ state.resumeTiming();
+ view.onVisibilityAggregated(true);
+ state.pauseTiming();
+ });
+ state.resumeTiming();
+ }
+ }
+}
diff --git a/android/view/contentcapture/MainContentCaptureSession.java b/android/view/contentcapture/MainContentCaptureSession.java
new file mode 100644
index 0000000..4cf5532
--- /dev/null
+++ b/android/view/contentcapture/MainContentCaptureSession.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_INSETS_CHANGED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
+import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Insets;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.TimeUtils;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+import android.view.inputmethod.BaseInputConnection;
+
+import com.android.internal.os.IResultReceiver;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Main session associated with a context.
+ *
+ * <p>This session is created when the activity starts and finished when it stops; clients can use
+ * it to create children activities.
+ *
+ * @hide
+ */
+public final class MainContentCaptureSession extends ContentCaptureSession {
+
+ private static final String TAG = MainContentCaptureSession.class.getSimpleName();
+
+ // For readability purposes...
+ private static final boolean FORCE_FLUSH = true;
+
+ /**
+ * Handler message used to flush the buffer.
+ */
+ private static final int MSG_FLUSH = 1;
+
+ /**
+ * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
+ * @hide
+ */
+ public static final String EXTRA_BINDER = "binder";
+
+ /**
+ * Name of the {@link IResultReceiver} extra used to pass the content capture enabled state.
+ * @hide
+ */
+ public static final String EXTRA_ENABLED_STATE = "enabled";
+
+ @NonNull
+ private final AtomicBoolean mDisabled = new AtomicBoolean(false);
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final ContentCaptureManager mManager;
+
+ @NonNull
+ private final Handler mHandler;
+
+ /**
+ * Interface to the system_server binder object - it's only used to start the session (and
+ * notify when the session is finished).
+ */
+ @NonNull
+ private final IContentCaptureManager mSystemServerInterface;
+
+ /**
+ * Direct interface to the service binder object - it's used to send the events, including the
+ * last ones (when the session is finished)
+ */
+ @NonNull
+ private IContentCaptureDirectManager mDirectServiceInterface;
+ @Nullable
+ private DeathRecipient mDirectServiceVulture;
+
+ private int mState = UNKNOWN_STATE;
+
+ @Nullable
+ private IBinder mApplicationToken;
+ @Nullable
+ private IBinder mShareableActivityToken;
+
+ @Nullable
+ private ComponentName mComponentName;
+
+ /**
+ * List of events held to be sent as a batch.
+ */
+ @Nullable
+ private ArrayList<ContentCaptureEvent> mEvents;
+
+ // Used just for debugging purposes (on dump)
+ private long mNextFlush;
+
+ /**
+ * Whether the next buffer flush is queued by a text changed event.
+ */
+ private boolean mNextFlushForTextChanged = false;
+
+ @Nullable
+ private final LocalLog mFlushHistory;
+
+ /**
+ * Binder object used to update the session state.
+ */
+ @NonNull
+ private final SessionStateReceiver mSessionStateReceiver;
+
+ private static class SessionStateReceiver extends IResultReceiver.Stub {
+ private final WeakReference<MainContentCaptureSession> mMainSession;
+
+ SessionStateReceiver(MainContentCaptureSession session) {
+ mMainSession = new WeakReference<>(session);
+ }
+
+ @Override
+ public void send(int resultCode, Bundle resultData) {
+ final MainContentCaptureSession mainSession = mMainSession.get();
+ if (mainSession == null) {
+ Log.w(TAG, "received result after mina session released");
+ return;
+ }
+ final IBinder binder;
+ if (resultData != null) {
+ // Change in content capture enabled.
+ final boolean hasEnabled = resultData.getBoolean(EXTRA_ENABLED_STATE);
+ if (hasEnabled) {
+ final boolean disabled = (resultCode == RESULT_CODE_FALSE);
+ mainSession.mDisabled.set(disabled);
+ return;
+ }
+ binder = resultData.getBinder(EXTRA_BINDER);
+ if (binder == null) {
+ Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
+ mainSession.mHandler.post(() -> mainSession.resetSession(
+ STATE_DISABLED | STATE_INTERNAL_ERROR));
+ return;
+ }
+ } else {
+ binder = null;
+ }
+ mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder));
+ }
+ }
+
+ protected MainContentCaptureSession(@NonNull Context context,
+ @NonNull ContentCaptureManager manager, @NonNull Handler handler,
+ @NonNull IContentCaptureManager systemServerInterface) {
+ mContext = context;
+ mManager = manager;
+ mHandler = handler;
+ mSystemServerInterface = systemServerInterface;
+
+ final int logHistorySize = mManager.mOptions.logHistorySize;
+ mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
+
+ mSessionStateReceiver = new SessionStateReceiver(this);
+ }
+
+ @Override
+ MainContentCaptureSession getMainCaptureSession() {
+ return this;
+ }
+
+ @Override
+ ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
+ final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
+ notifyChildSessionStarted(mId, child.mId, clientContext);
+ return child;
+ }
+
+ /**
+ * Starts this session.
+ */
+ @UiThread
+ void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
+ @NonNull ComponentName component, int flags) {
+ if (!isContentCaptureEnabled()) return;
+
+ if (sVerbose) {
+ Log.v(TAG, "start(): token=" + token + ", comp="
+ + ComponentName.flattenToShortString(component));
+ }
+
+ if (hasStarted()) {
+ // TODO(b/122959591): make sure this is expected (and when), or use Log.w
+ if (sDebug) {
+ Log.d(TAG, "ignoring handleStartSession(" + token + "/"
+ + ComponentName.flattenToShortString(component) + " while on state "
+ + getStateAsString(mState));
+ }
+ return;
+ }
+ mState = STATE_WAITING_FOR_SERVER;
+ mApplicationToken = token;
+ mShareableActivityToken = shareableActivityToken;
+ mComponentName = component;
+
+ if (sVerbose) {
+ Log.v(TAG, "handleStartSession(): token=" + token + ", act="
+ + getDebugState() + ", id=" + mId);
+ }
+
+ try {
+ mSystemServerInterface.startSession(mApplicationToken, mShareableActivityToken,
+ component, mId, flags, mSessionStateReceiver);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
+ }
+ }
+
+ @Override
+ void onDestroy() {
+ mHandler.removeMessages(MSG_FLUSH);
+ mHandler.post(() -> {
+ try {
+ flush(FLUSH_REASON_SESSION_FINISHED);
+ } finally {
+ destroySession();
+ }
+ });
+ }
+
+ /**
+ * Callback from {@code system_server} after call to
+ * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
+ * IResultReceiver)}.
+ *
+ * @param resultCode session state
+ * @param binder handle to {@code IContentCaptureDirectManager}
+ */
+ @UiThread
+ private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
+ if (binder != null) {
+ mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
+ mDirectServiceVulture = () -> {
+ Log.w(TAG, "Keeping session " + mId + " when service died");
+ mState = STATE_SERVICE_DIED;
+ mDisabled.set(true);
+ };
+ try {
+ binder.linkToDeath(mDirectServiceVulture, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
+ }
+ }
+
+ if ((resultCode & STATE_DISABLED) != 0) {
+ resetSession(resultCode);
+ } else {
+ mState = resultCode;
+ mDisabled.set(false);
+ // Flush any pending data immediately as buffering forced until now.
+ flushIfNeeded(FLUSH_REASON_SESSION_CONNECTED);
+ }
+ if (sVerbose) {
+ Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
+ + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
+ + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
+ }
+ }
+
+ @UiThread
+ private void sendEvent(@NonNull ContentCaptureEvent event) {
+ sendEvent(event, /* forceFlush= */ false);
+ }
+
+ @UiThread
+ private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ final int eventType = event.getType();
+ if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
+ if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
+ && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
+ // TODO(b/120494182): comment when this could happen (dialogs?)
+ if (sVerbose) {
+ Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
+ + ContentCaptureEvent.getTypeAsString(eventType)
+ + "): dropping because session not started yet");
+ }
+ return;
+ }
+ if (mDisabled.get()) {
+ // This happens when the event was queued in the handler before the sesison was ready,
+ // then handleSessionStarted() returned and set it as disabled - we need to drop it,
+ // otherwise it will keep triggering handleScheduleFlush()
+ if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
+ return;
+ }
+ final int maxBufferSize = mManager.mOptions.maxBufferSize;
+ if (mEvents == null) {
+ if (sVerbose) {
+ Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
+ }
+ mEvents = new ArrayList<>(maxBufferSize);
+ }
+
+ // Some type of events can be merged together
+ boolean addEvent = true;
+
+ if (eventType == TYPE_VIEW_TEXT_CHANGED) {
+ // We determine whether to add or merge the current event by following criteria:
+ // 1. Don't have composing span: always add.
+ // 2. Have composing span:
+ // 2.1 either last or current text is empty: add.
+ // 2.2 last event doesn't have composing span: add.
+ // Otherwise, merge.
+ final CharSequence text = event.getText();
+ final boolean hasComposingSpan = event.hasComposingSpan();
+ if (hasComposingSpan) {
+ ContentCaptureEvent lastEvent = null;
+ for (int index = mEvents.size() - 1; index >= 0; index--) {
+ final ContentCaptureEvent tmpEvent = mEvents.get(index);
+ if (event.getId().equals(tmpEvent.getId())) {
+ lastEvent = tmpEvent;
+ break;
+ }
+ }
+ if (lastEvent != null && lastEvent.hasComposingSpan()) {
+ final CharSequence lastText = lastEvent.getText();
+ final boolean bothNonEmpty = !TextUtils.isEmpty(lastText)
+ && !TextUtils.isEmpty(text);
+ boolean equalContent =
+ TextUtils.equals(lastText, text)
+ && lastEvent.hasSameComposingSpan(event)
+ && lastEvent.hasSameSelectionSpan(event);
+ if (equalContent) {
+ addEvent = false;
+ } else if (bothNonEmpty) {
+ lastEvent.mergeEvent(event);
+ addEvent = false;
+ }
+ if (!addEvent && sVerbose) {
+ Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
+ + getSanitizedString(text));
+ }
+ }
+ }
+ }
+
+ if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
+ final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
+ if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
+ && event.getSessionId() == lastEvent.getSessionId()) {
+ if (sVerbose) {
+ Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
+ + lastEvent.getSessionId());
+ }
+ lastEvent.mergeEvent(event);
+ addEvent = false;
+ }
+ }
+
+ if (addEvent) {
+ mEvents.add(event);
+ }
+
+ // TODO: we need to change when the flush happens so that we don't flush while the
+ // composing span hasn't changed. But we might need to keep flushing the events for the
+ // non-editable views and views that don't have the composing state; otherwise some other
+ // Content Capture features may be delayed.
+
+ final int numberEvents = mEvents.size();
+
+ final boolean bufferEvent = numberEvents < maxBufferSize;
+
+ if (bufferEvent && !forceFlush) {
+ final int flushReason;
+ if (eventType == TYPE_VIEW_TEXT_CHANGED) {
+ mNextFlushForTextChanged = true;
+ flushReason = FLUSH_REASON_TEXT_CHANGE_TIMEOUT;
+ } else {
+ if (mNextFlushForTextChanged) {
+ if (sVerbose) {
+ Log.i(TAG, "Not scheduling flush because next flush is for text changed");
+ }
+ return;
+ }
+
+ flushReason = FLUSH_REASON_IDLE_TIMEOUT;
+ }
+ scheduleFlush(flushReason, /* checkExisting= */ true);
+ return;
+ }
+
+ if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
+ // Callback from startSession hasn't been called yet - typically happens on system
+ // apps that are started before the system service
+ // TODO(b/122959591): try to ignore session while system is not ready / boot
+ // not complete instead. Similarly, the manager service should return right away
+ // when the user does not have a service set
+ if (sDebug) {
+ Log.d(TAG, "Closing session for " + getDebugState()
+ + " after " + numberEvents + " delayed events");
+ }
+ resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
+ // TODO(b/111276913): denylist activity / use special flag to indicate that
+ // when it's launched again
+ return;
+ }
+ final int flushReason;
+ switch (eventType) {
+ case ContentCaptureEvent.TYPE_SESSION_STARTED:
+ flushReason = FLUSH_REASON_SESSION_STARTED;
+ break;
+ case ContentCaptureEvent.TYPE_SESSION_FINISHED:
+ flushReason = FLUSH_REASON_SESSION_FINISHED;
+ break;
+ default:
+ flushReason = FLUSH_REASON_FULL;
+ }
+
+ flush(flushReason);
+ }
+
+ @UiThread
+ private boolean hasStarted() {
+ return mState != UNKNOWN_STATE;
+ }
+
+ @UiThread
+ private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
+ if (sVerbose) {
+ Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
+ + ", checkExisting=" + checkExisting);
+ }
+ if (!hasStarted()) {
+ if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
+ return;
+ }
+
+ if (mDisabled.get()) {
+ // Should not be called on this state, as handleSendEvent checks.
+ // But we rather add one if check and log than re-schedule and keep the session alive...
+ Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
+ + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
+ return;
+ }
+ if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
+ // "Renew" the flush message by removing the previous one
+ mHandler.removeMessages(MSG_FLUSH);
+ }
+
+ final int flushFrequencyMs;
+ if (reason == FLUSH_REASON_TEXT_CHANGE_TIMEOUT) {
+ flushFrequencyMs = mManager.mOptions.textChangeFlushingFrequencyMs;
+ } else {
+ if (reason != FLUSH_REASON_IDLE_TIMEOUT) {
+ if (sDebug) {
+ Log.d(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): not a timeout "
+ + "reason because mDirectServiceInterface is not ready yet");
+ }
+ }
+ flushFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
+ }
+
+ mNextFlush = System.currentTimeMillis() + flushFrequencyMs;
+ if (sVerbose) {
+ Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
+ + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
+ }
+ // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
+ mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
+ }
+
+ @UiThread
+ private void flushIfNeeded(@FlushReason int reason) {
+ if (mEvents == null || mEvents.isEmpty()) {
+ if (sVerbose) Log.v(TAG, "Nothing to flush");
+ return;
+ }
+ flush(reason);
+ }
+
+ @Override
+ @UiThread
+ void flush(@FlushReason int reason) {
+ if (mEvents == null) return;
+
+ if (mDisabled.get()) {
+ Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
+ + "disabled");
+ return;
+ }
+
+ if (mDirectServiceInterface == null) {
+ if (sVerbose) {
+ Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
+ + "client not ready: " + mEvents);
+ }
+ if (!mHandler.hasMessages(MSG_FLUSH)) {
+ scheduleFlush(reason, /* checkExisting= */ false);
+ }
+ return;
+ }
+
+ mNextFlushForTextChanged = false;
+
+ final int numberEvents = mEvents.size();
+ final String reasonString = getFlushReasonAsString(reason);
+ if (sDebug) {
+ Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason));
+ }
+ if (mFlushHistory != null) {
+ // Logs reason, size, max size, idle timeout
+ final String logRecord = "r=" + reasonString + " s=" + numberEvents
+ + " m=" + mManager.mOptions.maxBufferSize
+ + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
+ mFlushHistory.log(logRecord);
+ }
+ try {
+ mHandler.removeMessages(MSG_FLUSH);
+
+ final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
+ mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
+ + ": " + e);
+ }
+ }
+
+ @Override
+ public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
+ notifyContextUpdated(mId, context);
+ }
+
+ /**
+ * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
+ */
+ @NonNull
+ @UiThread
+ private ParceledListSlice<ContentCaptureEvent> clearEvents() {
+ // NOTE: we must save a reference to the current mEvents and then set it to to null,
+ // otherwise clearing it would clear it in the receiving side if the service is also local.
+ if (mEvents == null) {
+ return new ParceledListSlice<>(Collections.EMPTY_LIST);
+ }
+
+ final List<ContentCaptureEvent> events = new ArrayList<>(mEvents);
+ mEvents.clear();
+ return new ParceledListSlice<>(events);
+ }
+
+ @UiThread
+ private void destroySession() {
+ if (sDebug) {
+ Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
+ + getDebugState());
+ }
+
+ try {
+ mSystemServerInterface.finishSession(mId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error destroying system-service session " + mId + " for "
+ + getDebugState() + ": " + e);
+ }
+
+ if (mDirectServiceInterface != null) {
+ mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
+ }
+ mDirectServiceInterface = null;
+ }
+
+ // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
+ // clearings out.
+ @UiThread
+ private void resetSession(int newState) {
+ if (sVerbose) {
+ Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
+ + getStateAsString(mState) + " to " + getStateAsString(newState));
+ }
+ mState = newState;
+ mDisabled.set((newState & STATE_DISABLED) != 0);
+ // TODO(b/122454205): must reset children (which currently is owned by superclass)
+ mApplicationToken = null;
+ mShareableActivityToken = null;
+ mComponentName = null;
+ mEvents = null;
+ if (mDirectServiceInterface != null) {
+ mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
+ }
+ mDirectServiceInterface = null;
+ mHandler.removeMessages(MSG_FLUSH);
+ }
+
+ @Override
+ void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
+ notifyViewAppeared(mId, node);
+ }
+
+ @Override
+ void internalNotifyViewDisappeared(@NonNull AutofillId id) {
+ notifyViewDisappeared(mId, id);
+ }
+
+ @Override
+ void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
+ notifyViewTextChanged(mId, id, text);
+ }
+
+ @Override
+ void internalNotifyViewInsetsChanged(@NonNull Insets viewInsets) {
+ notifyViewInsetsChanged(mId, viewInsets);
+ }
+
+ @Override
+ public void internalNotifyViewTreeEvent(boolean started) {
+ notifyViewTreeEvent(mId, started);
+ }
+
+ @Override
+ public void internalNotifySessionResumed() {
+ notifySessionResumed(mId);
+ }
+
+ @Override
+ public void internalNotifySessionPaused() {
+ notifySessionPaused(mId);
+ }
+
+ @Override
+ boolean isContentCaptureEnabled() {
+ return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
+ }
+
+ // Called by ContentCaptureManager.isContentCaptureEnabled
+ boolean isDisabled() {
+ return mDisabled.get();
+ }
+
+ /**
+ * Sets the disabled state of content capture.
+ *
+ * @return whether disabled state was changed.
+ */
+ boolean setDisabled(boolean disabled) {
+ return mDisabled.compareAndSet(!disabled, disabled);
+ }
+
+ // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is
+ // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
+ // change should also get get rid of the "internalNotifyXXXX" methods above
+ void notifyChildSessionStarted(int parentSessionId, int childSessionId,
+ @NonNull ContentCaptureContext clientContext) {
+ mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+ .setParentSessionId(parentSessionId).setClientContext(clientContext),
+ FORCE_FLUSH));
+ }
+
+ void notifyChildSessionFinished(int parentSessionId, int childSessionId) {
+ mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+ .setParentSessionId(parentSessionId), FORCE_FLUSH));
+ }
+
+ void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
+ mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
+ .setViewNode(node.mNode)));
+ }
+
+ /** Public because is also used by ViewRootImpl */
+ public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
+ mHandler.post(() -> sendEvent(
+ new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id)));
+ }
+
+ void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) {
+ // Since the same CharSequence instance may be reused in the TextView, we need to make
+ // a copy of its content so that its value will not be changed by subsequent updates
+ // in the TextView.
+ final CharSequence eventText = stringOrSpannedStringWithoutNoCopySpans(text);
+
+ final int composingStart;
+ final int composingEnd;
+ if (text instanceof Spannable) {
+ composingStart = BaseInputConnection.getComposingSpanStart((Spannable) text);
+ composingEnd = BaseInputConnection.getComposingSpanEnd((Spannable) text);
+ } else {
+ composingStart = ContentCaptureEvent.MAX_INVALID_VALUE;
+ composingEnd = ContentCaptureEvent.MAX_INVALID_VALUE;
+ }
+
+ final int startIndex = Selection.getSelectionStart(text);
+ final int endIndex = Selection.getSelectionEnd(text);
+ mHandler.post(() -> sendEvent(
+ new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
+ .setAutofillId(id).setText(eventText)
+ .setComposingIndex(composingStart, composingEnd)
+ .setSelectionIndex(startIndex, endIndex)));
+ }
+
+ private CharSequence stringOrSpannedStringWithoutNoCopySpans(CharSequence source) {
+ if (source == null) {
+ return null;
+ } else if (source instanceof Spanned) {
+ return new SpannableString(source, /* ignoreNoCopySpan= */ true);
+ } else {
+ return source.toString();
+ }
+ }
+
+ /** Public because is also used by ViewRootImpl */
+ public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
+ mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
+ .setInsets(viewInsets)));
+ }
+
+ /** Public because is also used by ViewRootImpl */
+ public void notifyViewTreeEvent(int sessionId, boolean started) {
+ final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
+ mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, type), FORCE_FLUSH));
+ }
+
+ void notifySessionResumed(int sessionId) {
+ mHandler.post(() -> sendEvent(
+ new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH));
+ }
+
+ void notifySessionPaused(int sessionId) {
+ mHandler.post(() -> sendEvent(
+ new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH));
+ }
+
+ void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
+ mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
+ .setClientContext(context), FORCE_FLUSH));
+ }
+
+ @Override
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ super.dump(prefix, pw);
+
+ pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
+ pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
+ if (mDirectServiceInterface != null) {
+ pw.print(prefix); pw.print("mDirectServiceInterface: ");
+ pw.println(mDirectServiceInterface);
+ }
+ pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
+ pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
+ pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
+ if (mApplicationToken != null) {
+ pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
+ }
+ if (mShareableActivityToken != null) {
+ pw.print(prefix); pw.print("sharable activity token: ");
+ pw.println(mShareableActivityToken);
+ }
+ if (mComponentName != null) {
+ pw.print(prefix); pw.print("component name: ");
+ pw.println(mComponentName.flattenToShortString());
+ }
+ if (mEvents != null && !mEvents.isEmpty()) {
+ final int numberEvents = mEvents.size();
+ pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
+ pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
+ if (sVerbose && numberEvents > 0) {
+ final String prefix3 = prefix + " ";
+ for (int i = 0; i < numberEvents; i++) {
+ final ContentCaptureEvent event = mEvents.get(i);
+ pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
+ pw.println();
+ }
+ }
+ pw.print(prefix); pw.print("mNextFlushForTextChanged: ");
+ pw.println(mNextFlushForTextChanged);
+ pw.print(prefix); pw.print("flush frequency: ");
+ if (mNextFlushForTextChanged) {
+ pw.println(mManager.mOptions.textChangeFlushingFrequencyMs);
+ } else {
+ pw.println(mManager.mOptions.idleFlushingFrequencyMs);
+ }
+ pw.print(prefix); pw.print("next flush: ");
+ TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
+ pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
+ }
+ if (mFlushHistory != null) {
+ pw.print(prefix); pw.println("flush history:");
+ mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
+ } else {
+ pw.print(prefix); pw.println("not logging flush history");
+ }
+
+ super.dump(prefix, pw);
+ }
+
+ /**
+ * Gets a string that can be used to identify the activity on logging statements.
+ */
+ private String getActivityName() {
+ return mComponentName == null
+ ? "pkg:" + mContext.getPackageName()
+ : "act:" + mComponentName.flattenToShortString();
+ }
+
+ @NonNull
+ private String getDebugState() {
+ return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
+ + mDisabled.get() + "]";
+ }
+
+ @NonNull
+ private String getDebugState(@FlushReason int reason) {
+ return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
+ }
+}
diff --git a/android/view/contentcapture/MyContentCaptureService.java b/android/view/contentcapture/MyContentCaptureService.java
new file mode 100644
index 0000000..d07ed37
--- /dev/null
+++ b/android/view/contentcapture/MyContentCaptureService.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.content.ComponentName;
+import android.service.contentcapture.ActivityEvent;
+import android.service.contentcapture.ContentCaptureService;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class MyContentCaptureService extends ContentCaptureService {
+
+ private static final String TAG = MyContentCaptureService.class.getSimpleName();
+ private static final String MY_PACKAGE = "com.android.perftests.contentcapture";
+ public static final String SERVICE_NAME = MY_PACKAGE + "/"
+ + MyContentCaptureService.class.getName();
+
+ private static ServiceWatcher sServiceWatcher;
+
+ @NonNull
+ public static ServiceWatcher setServiceWatcher() {
+ if (sServiceWatcher != null) {
+ throw new IllegalStateException("There Can Be Only One!");
+ }
+ sServiceWatcher = new ServiceWatcher();
+ return sServiceWatcher;
+ }
+
+ public static void resetStaticState() {
+ sServiceWatcher = null;
+ }
+
+ private static void clearServiceWatcher() {
+ final ServiceWatcher sw = sServiceWatcher;
+ if (sw != null) {
+ if (sw.mReadyToClear) {
+ sw.mService = null;
+ sServiceWatcher = null;
+ } else {
+ sw.mReadyToClear = true;
+ }
+ }
+ }
+
+ @Override
+ public void onConnected() {
+ Log.i(TAG, "onConnected: sServiceWatcher=" + sServiceWatcher);
+
+ if (sServiceWatcher == null) {
+ Log.e(TAG, "onConnected() without a watcher");
+ return;
+ }
+
+ if (!sServiceWatcher.mReadyToClear && sServiceWatcher.mService != null) {
+ Log.e(TAG, "onConnected(): already created: " + sServiceWatcher);
+ return;
+ }
+
+ sServiceWatcher.mService = this;
+ sServiceWatcher.mCreated.countDown();
+ sServiceWatcher.mReadyToClear = false;
+ }
+
+ @Override
+ public void onDisconnected() {
+ Log.i(TAG, "onDisconnected: sServiceWatcher=" + sServiceWatcher);
+
+ if (sServiceWatcher == null) {
+ Log.e(TAG, "onDisconnected() without a watcher");
+ return;
+ }
+ if (sServiceWatcher.mService == null) {
+ Log.e(TAG, "onDisconnected(): no service on " + sServiceWatcher);
+ return;
+ }
+
+ sServiceWatcher.mDestroyed.countDown();
+ clearServiceWatcher();
+ }
+
+ @Override
+ public void onCreateContentCaptureSession(ContentCaptureContext context,
+ ContentCaptureSessionId sessionId) {
+ Log.i(TAG, "onCreateContentCaptureSession(ctx=" + context + ", session=" + sessionId);
+ }
+
+ @Override
+ public void onDestroyContentCaptureSession(ContentCaptureSessionId sessionId) {
+ Log.i(TAG, "onDestroyContentCaptureSession(session=" + sessionId + ")");
+ }
+
+ @Override
+ public void onContentCaptureEvent(ContentCaptureSessionId sessionId,
+ ContentCaptureEvent event) {
+ Log.i(TAG, "onContentCaptureEventsRequest(session=" + sessionId + "): " + event);
+ }
+
+ @Override
+ public void onActivityEvent(ActivityEvent event) {
+ Log.i(TAG, "onActivityEvent(): " + event);
+ }
+
+ public static final class ServiceWatcher {
+
+ private static final long GENERIC_TIMEOUT_MS = 10_000;
+ private final CountDownLatch mCreated = new CountDownLatch(1);
+ private final CountDownLatch mDestroyed = new CountDownLatch(1);
+ private boolean mReadyToClear = true;
+ private Pair<Set<String>, Set<ComponentName>> mAllowList;
+
+ private MyContentCaptureService mService;
+
+ @NonNull
+ public MyContentCaptureService waitOnCreate() throws InterruptedException {
+ await(mCreated, "not created");
+
+ if (mService == null) {
+ throw new IllegalStateException("not created");
+ }
+
+ if (mAllowList != null) {
+ Log.d(TAG, "Allow after created: " + mAllowList);
+ mService.setContentCaptureWhitelist(mAllowList.first, mAllowList.second);
+ }
+
+ return mService;
+ }
+
+ public void waitOnDestroy() throws InterruptedException {
+ await(mDestroyed, "not destroyed");
+ }
+
+ /**
+ * Allow just this package.
+ */
+ public void setAllowSelf() {
+ final ArraySet<String> pkgs = new ArraySet<>(1);
+ pkgs.add(MY_PACKAGE);
+ mAllowList = new Pair<>(pkgs, null);
+ }
+
+ @Override
+ public String toString() {
+ return "mService: " + mService + " created: " + (mCreated.getCount() == 0)
+ + " destroyed: " + (mDestroyed.getCount() == 0);
+ }
+
+ /**
+ * Awaits for a latch to be counted down.
+ */
+ private static void await(@NonNull CountDownLatch latch, @NonNull String fmt,
+ @Nullable Object... args)
+ throws InterruptedException {
+ final boolean called = latch.await(GENERIC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (!called) {
+ throw new IllegalStateException(String.format(fmt, args)
+ + " in " + GENERIC_TIMEOUT_MS + "ms");
+ }
+ }
+ }
+}
diff --git a/android/view/contentcapture/ViewNode.java b/android/view/contentcapture/ViewNode.java
new file mode 100644
index 0000000..c882c6e
--- /dev/null
+++ b/android/view/contentcapture/ViewNode.java
@@ -0,0 +1,1069 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.assist.AssistStructure;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.ViewStructure;
+import android.view.ViewStructure.HtmlInfo.Builder;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+//TODO(b/122484602): add javadocs / implement Parcelable / implement
+//TODO(b/122484602): for now it's extending ViewNode directly as it needs most of its properties,
+// but it might be better to create a common, abstract android.view.ViewNode class that both extend
+// instead
+/** @hide */
+@SystemApi
+public final class ViewNode extends AssistStructure.ViewNode {
+
+ private static final String TAG = ViewNode.class.getSimpleName();
+
+ private static final long FLAGS_HAS_TEXT = 1L << 0;
+ private static final long FLAGS_HAS_COMPLEX_TEXT = 1L << 1;
+ private static final long FLAGS_VISIBILITY_MASK = View.VISIBLE | View.INVISIBLE | View.GONE;
+ private static final long FLAGS_HAS_CLASSNAME = 1L << 4;
+ private static final long FLAGS_HAS_AUTOFILL_ID = 1L << 5;
+ private static final long FLAGS_HAS_AUTOFILL_PARENT_ID = 1L << 6;
+ private static final long FLAGS_HAS_ID = 1L << 7;
+ private static final long FLAGS_HAS_LARGE_COORDS = 1L << 8;
+ private static final long FLAGS_HAS_SCROLL = 1L << 9;
+ private static final long FLAGS_ASSIST_BLOCKED = 1L << 10;
+ private static final long FLAGS_DISABLED = 1L << 11;
+ private static final long FLAGS_CLICKABLE = 1L << 12;
+ private static final long FLAGS_LONG_CLICKABLE = 1L << 13;
+ private static final long FLAGS_CONTEXT_CLICKABLE = 1L << 14;
+ private static final long FLAGS_FOCUSABLE = 1L << 15;
+ private static final long FLAGS_FOCUSED = 1L << 16;
+ private static final long FLAGS_ACCESSIBILITY_FOCUSED = 1L << 17;
+ private static final long FLAGS_CHECKABLE = 1L << 18;
+ private static final long FLAGS_CHECKED = 1L << 19;
+ private static final long FLAGS_SELECTED = 1L << 20;
+ private static final long FLAGS_ACTIVATED = 1L << 21;
+ private static final long FLAGS_OPAQUE = 1L << 22;
+ private static final long FLAGS_HAS_CONTENT_DESCRIPTION = 1L << 23;
+ private static final long FLAGS_HAS_EXTRAS = 1L << 24;
+ private static final long FLAGS_HAS_LOCALE_LIST = 1L << 25;
+ private static final long FLAGS_HAS_INPUT_TYPE = 1L << 26;
+ private static final long FLAGS_HAS_MIN_TEXT_EMS = 1L << 27;
+ private static final long FLAGS_HAS_MAX_TEXT_EMS = 1L << 28;
+ private static final long FLAGS_HAS_MAX_TEXT_LENGTH = 1L << 29;
+ private static final long FLAGS_HAS_TEXT_ID_ENTRY = 1L << 30;
+ private static final long FLAGS_HAS_AUTOFILL_TYPE = 1L << 31;
+ private static final long FLAGS_HAS_AUTOFILL_VALUE = 1L << 32;
+ private static final long FLAGS_HAS_AUTOFILL_HINTS = 1L << 33;
+ private static final long FLAGS_HAS_AUTOFILL_OPTIONS = 1L << 34;
+ private static final long FLAGS_HAS_HINT_ID_ENTRY = 1L << 35;
+ private static final long FLAGS_HAS_MIME_TYPES = 1L << 36;
+
+ /** Flags used to optimize what's written to the parcel */
+ private long mFlags;
+
+ private AutofillId mParentAutofillId;
+
+ private AutofillId mAutofillId;
+ private ViewNodeText mText;
+ private String mClassName;
+ private int mId = View.NO_ID;
+ private String mIdPackage;
+ private String mIdType;
+ private String mIdEntry;
+ private int mX;
+ private int mY;
+ private int mScrollX;
+ private int mScrollY;
+ private int mWidth;
+ private int mHeight;
+ private CharSequence mContentDescription;
+ private Bundle mExtras;
+ private LocaleList mLocaleList;
+ private int mInputType;
+ private int mMinEms = -1;
+ private int mMaxEms = -1;
+ private int mMaxLength = -1;
+ private String mTextIdEntry;
+ private String mHintIdEntry;
+ private @View.AutofillType int mAutofillType = View.AUTOFILL_TYPE_NONE;
+ private String[] mAutofillHints;
+ private AutofillValue mAutofillValue;
+ private CharSequence[] mAutofillOptions;
+ private String[] mReceiveContentMimeTypes;
+
+ /** @hide */
+ public ViewNode() {
+ }
+
+ private ViewNode(long nodeFlags, @NonNull Parcel parcel) {
+ mFlags = nodeFlags;
+
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_ID) != 0) {
+ mAutofillId = parcel.readParcelable(null);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_PARENT_ID) != 0) {
+ mParentAutofillId = parcel.readParcelable(null);
+ }
+ if ((nodeFlags & FLAGS_HAS_TEXT) != 0) {
+ mText = new ViewNodeText(parcel, (nodeFlags & FLAGS_HAS_COMPLEX_TEXT) == 0);
+ }
+ if ((nodeFlags & FLAGS_HAS_CLASSNAME) != 0) {
+ mClassName = parcel.readString();
+ }
+ if ((nodeFlags & FLAGS_HAS_ID) != 0) {
+ mId = parcel.readInt();
+ if (mId != View.NO_ID) {
+ mIdEntry = parcel.readString();
+ if (mIdEntry != null) {
+ mIdType = parcel.readString();
+ mIdPackage = parcel.readString();
+ }
+ }
+ }
+ if ((nodeFlags & FLAGS_HAS_LARGE_COORDS) != 0) {
+ mX = parcel.readInt();
+ mY = parcel.readInt();
+ mWidth = parcel.readInt();
+ mHeight = parcel.readInt();
+ } else {
+ int val = parcel.readInt();
+ mX = val & 0x7fff;
+ mY = (val >> 16) & 0x7fff;
+ val = parcel.readInt();
+ mWidth = val & 0x7fff;
+ mHeight = (val >> 16) & 0x7fff;
+ }
+ if ((nodeFlags & FLAGS_HAS_SCROLL) != 0) {
+ mScrollX = parcel.readInt();
+ mScrollY = parcel.readInt();
+ }
+ if ((nodeFlags & FLAGS_HAS_CONTENT_DESCRIPTION) != 0) {
+ mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ }
+ if ((nodeFlags & FLAGS_HAS_EXTRAS) != 0) {
+ mExtras = parcel.readBundle();
+ }
+ if ((nodeFlags & FLAGS_HAS_LOCALE_LIST) != 0) {
+ mLocaleList = parcel.readParcelable(null);
+ }
+ if ((nodeFlags & FLAGS_HAS_MIME_TYPES) != 0) {
+ mReceiveContentMimeTypes = parcel.readStringArray();
+ }
+ if ((nodeFlags & FLAGS_HAS_INPUT_TYPE) != 0) {
+ mInputType = parcel.readInt();
+ }
+ if ((nodeFlags & FLAGS_HAS_MIN_TEXT_EMS) != 0) {
+ mMinEms = parcel.readInt();
+ }
+ if ((nodeFlags & FLAGS_HAS_MAX_TEXT_EMS) != 0) {
+ mMaxEms = parcel.readInt();
+ }
+ if ((nodeFlags & FLAGS_HAS_MAX_TEXT_LENGTH) != 0) {
+ mMaxLength = parcel.readInt();
+ }
+ if ((nodeFlags & FLAGS_HAS_TEXT_ID_ENTRY) != 0) {
+ mTextIdEntry = parcel.readString();
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_TYPE) != 0) {
+ mAutofillType = parcel.readInt();
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_HINTS) != 0) {
+ mAutofillHints = parcel.readStringArray();
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_VALUE) != 0) {
+ mAutofillValue = parcel.readParcelable(null);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_OPTIONS) != 0) {
+ mAutofillOptions = parcel.readCharSequenceArray();
+ }
+ if ((nodeFlags & FLAGS_HAS_HINT_ID_ENTRY) != 0) {
+ mHintIdEntry = parcel.readString();
+ }
+ }
+
+ /**
+ * Returns the {@link AutofillId} of this view's parent, if the parent is also part of the
+ * screen observation tree.
+ */
+ @Nullable
+ public AutofillId getParentAutofillId() {
+ return mParentAutofillId;
+ }
+
+ @Nullable
+ @Override
+ public AutofillId getAutofillId() {
+ return mAutofillId;
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getText() {
+ return mText != null ? mText.mText : null;
+ }
+
+ @Nullable
+ @Override
+ public String getClassName() {
+ return mClassName;
+ }
+
+ @Override
+ public int getId() {
+ return mId;
+ }
+
+ @Nullable
+ @Override
+ public String getIdPackage() {
+ return mIdPackage;
+ }
+
+ @Nullable
+ @Override
+ public String getIdType() {
+ return mIdType;
+ }
+
+ @Nullable
+ @Override
+ public String getIdEntry() {
+ return mIdEntry;
+ }
+
+ @Override
+ public int getLeft() {
+ return mX;
+ }
+
+ @Override
+ public int getTop() {
+ return mY;
+ }
+
+ @Override
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ @Override
+ public int getScrollY() {
+ return mScrollY;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public boolean isAssistBlocked() {
+ return (mFlags & FLAGS_ASSIST_BLOCKED) != 0;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return (mFlags & FLAGS_DISABLED) == 0;
+ }
+
+ @Override
+ public boolean isClickable() {
+ return (mFlags & FLAGS_CLICKABLE) != 0;
+ }
+
+ @Override
+ public boolean isLongClickable() {
+ return (mFlags & FLAGS_LONG_CLICKABLE) != 0;
+ }
+
+ @Override
+ public boolean isContextClickable() {
+ return (mFlags & FLAGS_CONTEXT_CLICKABLE) != 0;
+ }
+
+ @Override
+ public boolean isFocusable() {
+ return (mFlags & FLAGS_FOCUSABLE) != 0;
+ }
+
+ @Override
+ public boolean isFocused() {
+ return (mFlags & FLAGS_FOCUSED) != 0;
+ }
+
+ @Override
+ public boolean isAccessibilityFocused() {
+ return (mFlags & FLAGS_ACCESSIBILITY_FOCUSED) != 0;
+ }
+
+ @Override
+ public boolean isCheckable() {
+ return (mFlags & FLAGS_CHECKABLE) != 0;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return (mFlags & FLAGS_CHECKED) != 0;
+ }
+
+ @Override
+ public boolean isSelected() {
+ return (mFlags & FLAGS_SELECTED) != 0;
+ }
+
+ @Override
+ public boolean isActivated() {
+ return (mFlags & FLAGS_ACTIVATED) != 0;
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return (mFlags & FLAGS_OPAQUE) != 0;
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ @Nullable
+ @Override
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Nullable
+ @Override
+ public String getHint() {
+ return mText != null ? mText.mHint : null;
+ }
+
+ @Nullable
+ @Override
+ public String getHintIdEntry() {
+ return mHintIdEntry;
+ }
+
+ @Override
+ public int getTextSelectionStart() {
+ return mText != null ? mText.mTextSelectionStart : -1;
+ }
+
+ @Override
+ public int getTextSelectionEnd() {
+ return mText != null ? mText.mTextSelectionEnd : -1;
+ }
+
+ @Override
+ public int getTextColor() {
+ return mText != null ? mText.mTextColor : TEXT_COLOR_UNDEFINED;
+ }
+
+ @Override
+ public int getTextBackgroundColor() {
+ return mText != null ? mText.mTextBackgroundColor : TEXT_COLOR_UNDEFINED;
+ }
+
+ @Override
+ public float getTextSize() {
+ return mText != null ? mText.mTextSize : 0;
+ }
+
+ @Override
+ public int getTextStyle() {
+ return mText != null ? mText.mTextStyle : 0;
+ }
+
+ @Nullable
+ @Override
+ public int[] getTextLineCharOffsets() {
+ return mText != null ? mText.mLineCharOffsets : null;
+ }
+
+ @Nullable
+ @Override
+ public int[] getTextLineBaselines() {
+ return mText != null ? mText.mLineBaselines : null;
+ }
+
+ @Override
+ public int getVisibility() {
+ return (int) (mFlags & FLAGS_VISIBILITY_MASK);
+ }
+
+ @Override
+ public int getInputType() {
+ return mInputType;
+ }
+
+ @Override
+ public int getMinTextEms() {
+ return mMinEms;
+ }
+
+ @Override
+ public int getMaxTextEms() {
+ return mMaxEms;
+ }
+
+ @Override
+ public int getMaxTextLength() {
+ return mMaxLength;
+ }
+
+ @Nullable
+ @Override
+ public String getTextIdEntry() {
+ return mTextIdEntry;
+ }
+
+ @Override
+ public @View.AutofillType int getAutofillType() {
+ return mAutofillType;
+ }
+
+ @Override
+ @Nullable public String[] getAutofillHints() {
+ return mAutofillHints;
+ }
+
+ @Override
+ @Nullable public AutofillValue getAutofillValue() {
+ return mAutofillValue;
+ }
+
+ @Override
+ @Nullable public CharSequence[] getAutofillOptions() {
+ return mAutofillOptions;
+ }
+
+ @Override
+ @Nullable
+ public String[] getReceiveContentMimeTypes() {
+ return mReceiveContentMimeTypes;
+ }
+
+ @Nullable
+ @Override
+ public LocaleList getLocaleList() {
+ return mLocaleList;
+ }
+
+ private void writeSelfToParcel(@NonNull Parcel parcel, int parcelFlags) {
+ long nodeFlags = mFlags;
+
+ if (mAutofillId != null) {
+ nodeFlags |= FLAGS_HAS_AUTOFILL_ID;
+ }
+
+ if (mParentAutofillId != null) {
+ nodeFlags |= FLAGS_HAS_AUTOFILL_PARENT_ID;
+ }
+
+ if (mText != null) {
+ nodeFlags |= FLAGS_HAS_TEXT;
+ if (!mText.isSimple()) {
+ nodeFlags |= FLAGS_HAS_COMPLEX_TEXT;
+ }
+ }
+ if (mClassName != null) {
+ nodeFlags |= FLAGS_HAS_CLASSNAME;
+ }
+ if (mId != View.NO_ID) {
+ nodeFlags |= FLAGS_HAS_ID;
+ }
+ if ((mX & ~0x7fff) != 0 || (mY & ~0x7fff) != 0
+ || (mWidth & ~0x7fff) != 0 | (mHeight & ~0x7fff) != 0) {
+ nodeFlags |= FLAGS_HAS_LARGE_COORDS;
+ }
+ if (mScrollX != 0 || mScrollY != 0) {
+ nodeFlags |= FLAGS_HAS_SCROLL;
+ }
+ if (mContentDescription != null) {
+ nodeFlags |= FLAGS_HAS_CONTENT_DESCRIPTION;
+ }
+ if (mExtras != null) {
+ nodeFlags |= FLAGS_HAS_EXTRAS;
+ }
+ if (mLocaleList != null) {
+ nodeFlags |= FLAGS_HAS_LOCALE_LIST;
+ }
+ if (mReceiveContentMimeTypes != null) {
+ nodeFlags |= FLAGS_HAS_MIME_TYPES;
+ }
+ if (mInputType != 0) {
+ nodeFlags |= FLAGS_HAS_INPUT_TYPE;
+ }
+ if (mMinEms > -1) {
+ nodeFlags |= FLAGS_HAS_MIN_TEXT_EMS;
+ }
+ if (mMaxEms > -1) {
+ nodeFlags |= FLAGS_HAS_MAX_TEXT_EMS;
+ }
+ if (mMaxLength > -1) {
+ nodeFlags |= FLAGS_HAS_MAX_TEXT_LENGTH;
+ }
+ if (mTextIdEntry != null) {
+ nodeFlags |= FLAGS_HAS_TEXT_ID_ENTRY;
+ }
+ if (mAutofillValue != null) {
+ nodeFlags |= FLAGS_HAS_AUTOFILL_VALUE;
+ }
+ if (mAutofillType != View.AUTOFILL_TYPE_NONE) {
+ nodeFlags |= FLAGS_HAS_AUTOFILL_TYPE;
+ }
+ if (mAutofillHints != null) {
+ nodeFlags |= FLAGS_HAS_AUTOFILL_HINTS;
+ }
+ if (mAutofillOptions != null) {
+ nodeFlags |= FLAGS_HAS_AUTOFILL_OPTIONS;
+ }
+ if (mHintIdEntry != null) {
+ nodeFlags |= FLAGS_HAS_HINT_ID_ENTRY;
+ }
+ parcel.writeLong(nodeFlags);
+
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_ID) != 0) {
+ parcel.writeParcelable(mAutofillId, parcelFlags);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_PARENT_ID) != 0) {
+ parcel.writeParcelable(mParentAutofillId, parcelFlags);
+ }
+ if ((nodeFlags & FLAGS_HAS_TEXT) != 0) {
+ mText.writeToParcel(parcel, (nodeFlags & FLAGS_HAS_COMPLEX_TEXT) == 0);
+ }
+ if ((nodeFlags & FLAGS_HAS_CLASSNAME) != 0) {
+ parcel.writeString(mClassName);
+ }
+ if ((nodeFlags & FLAGS_HAS_ID) != 0) {
+ parcel.writeInt(mId);
+ if (mId != View.NO_ID) {
+ parcel.writeString(mIdEntry);
+ if (mIdEntry != null) {
+ parcel.writeString(mIdType);
+ parcel.writeString(mIdPackage);
+ }
+ }
+ }
+ if ((nodeFlags & FLAGS_HAS_LARGE_COORDS) != 0) {
+ parcel.writeInt(mX);
+ parcel.writeInt(mY);
+ parcel.writeInt(mWidth);
+ parcel.writeInt(mHeight);
+ } else {
+ parcel.writeInt((mY << 16) | mX);
+ parcel.writeInt((mHeight << 16) | mWidth);
+ }
+ if ((nodeFlags & FLAGS_HAS_SCROLL) != 0) {
+ parcel.writeInt(mScrollX);
+ parcel.writeInt(mScrollY);
+ }
+ if ((nodeFlags & FLAGS_HAS_CONTENT_DESCRIPTION) != 0) {
+ TextUtils.writeToParcel(mContentDescription, parcel, 0);
+ }
+ if ((nodeFlags & FLAGS_HAS_EXTRAS) != 0) {
+ parcel.writeBundle(mExtras);
+ }
+ if ((nodeFlags & FLAGS_HAS_LOCALE_LIST) != 0) {
+ parcel.writeParcelable(mLocaleList, 0);
+ }
+ if ((nodeFlags & FLAGS_HAS_MIME_TYPES) != 0) {
+ parcel.writeStringArray(mReceiveContentMimeTypes);
+ }
+ if ((nodeFlags & FLAGS_HAS_INPUT_TYPE) != 0) {
+ parcel.writeInt(mInputType);
+ }
+ if ((nodeFlags & FLAGS_HAS_MIN_TEXT_EMS) != 0) {
+ parcel.writeInt(mMinEms);
+ }
+ if ((nodeFlags & FLAGS_HAS_MAX_TEXT_EMS) != 0) {
+ parcel.writeInt(mMaxEms);
+ }
+ if ((nodeFlags & FLAGS_HAS_MAX_TEXT_LENGTH) != 0) {
+ parcel.writeInt(mMaxLength);
+ }
+ if ((nodeFlags & FLAGS_HAS_TEXT_ID_ENTRY) != 0) {
+ parcel.writeString(mTextIdEntry);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_TYPE) != 0) {
+ parcel.writeInt(mAutofillType);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_HINTS) != 0) {
+ parcel.writeStringArray(mAutofillHints);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_VALUE) != 0) {
+ parcel.writeParcelable(mAutofillValue, 0);
+ }
+ if ((nodeFlags & FLAGS_HAS_AUTOFILL_OPTIONS) != 0) {
+ parcel.writeCharSequenceArray(mAutofillOptions);
+ }
+ if ((nodeFlags & FLAGS_HAS_HINT_ID_ENTRY) != 0) {
+ parcel.writeString(mHintIdEntry);
+ }
+ }
+
+ /** @hide */
+ @TestApi
+ public static void writeToParcel(@NonNull Parcel parcel, @Nullable ViewNode node, int flags) {
+ if (node == null) {
+ parcel.writeLong(0);
+ } else {
+ node.writeSelfToParcel(parcel, flags);
+ }
+ }
+
+ /** @hide */
+ @TestApi
+ public static @Nullable ViewNode readFromParcel(@NonNull Parcel parcel) {
+ final long nodeFlags = parcel.readLong();
+ return nodeFlags == 0 ? null : new ViewNode(nodeFlags, parcel);
+ }
+
+ /** @hide */
+ @TestApi
+ public static final class ViewStructureImpl extends ViewStructure {
+
+ final ViewNode mNode = new ViewNode();
+
+ /** @hide */
+ @TestApi
+ public ViewStructureImpl(@NonNull View view) {
+ mNode.mAutofillId = Preconditions.checkNotNull(view).getAutofillId();
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ mNode.mParentAutofillId = ((View) parent).getAutofillId();
+ }
+ }
+
+ /** @hide */
+ @TestApi
+ public ViewStructureImpl(@NonNull AutofillId parentId, long virtualId, int sessionId) {
+ mNode.mParentAutofillId = Preconditions.checkNotNull(parentId);
+ mNode.mAutofillId = new AutofillId(parentId, virtualId, sessionId);
+ }
+
+ /** @hide */
+ @TestApi
+ public ViewNode getNode() {
+ return mNode;
+ }
+
+ @Override
+ public void setId(int id, String packageName, String typeName, String entryName) {
+ mNode.mId = id;
+ mNode.mIdPackage = packageName;
+ mNode.mIdType = typeName;
+ mNode.mIdEntry = entryName;
+ }
+
+ @Override
+ public void setDimens(int left, int top, int scrollX, int scrollY, int width, int height) {
+ mNode.mX = left;
+ mNode.mY = top;
+ mNode.mScrollX = scrollX;
+ mNode.mScrollY = scrollY;
+ mNode.mWidth = width;
+ mNode.mHeight = height;
+ }
+
+ @Override
+ public void setTransformation(Matrix matrix) {
+ Log.w(TAG, "setTransformation() is not supported");
+ }
+
+ @Override
+ public void setElevation(float elevation) {
+ Log.w(TAG, "setElevation() is not supported");
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ Log.w(TAG, "setAlpha() is not supported");
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_VISIBILITY_MASK)
+ | (visibility & FLAGS_VISIBILITY_MASK);
+ }
+
+ @Override
+ public void setAssistBlocked(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_ASSIST_BLOCKED)
+ | (state ? FLAGS_ASSIST_BLOCKED : 0);
+ }
+
+ @Override
+ public void setEnabled(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_DISABLED) | (state ? 0 : FLAGS_DISABLED);
+ }
+
+ @Override
+ public void setClickable(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_CLICKABLE) | (state ? FLAGS_CLICKABLE : 0);
+ }
+
+ @Override
+ public void setLongClickable(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_LONG_CLICKABLE)
+ | (state ? FLAGS_LONG_CLICKABLE : 0);
+ }
+
+ @Override
+ public void setContextClickable(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_CONTEXT_CLICKABLE)
+ | (state ? FLAGS_CONTEXT_CLICKABLE : 0);
+ }
+
+ @Override
+ public void setFocusable(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_FOCUSABLE) | (state ? FLAGS_FOCUSABLE : 0);
+ }
+
+ @Override
+ public void setFocused(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_FOCUSED) | (state ? FLAGS_FOCUSED : 0);
+ }
+
+ @Override
+ public void setAccessibilityFocused(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_ACCESSIBILITY_FOCUSED)
+ | (state ? FLAGS_ACCESSIBILITY_FOCUSED : 0);
+ }
+
+ @Override
+ public void setCheckable(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_CHECKABLE) | (state ? FLAGS_CHECKABLE : 0);
+ }
+
+ @Override
+ public void setChecked(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_CHECKED) | (state ? FLAGS_CHECKED : 0);
+ }
+
+ @Override
+ public void setSelected(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_SELECTED) | (state ? FLAGS_SELECTED : 0);
+ }
+
+ @Override
+ public void setActivated(boolean state) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_ACTIVATED) | (state ? FLAGS_ACTIVATED : 0);
+ }
+
+ @Override
+ public void setOpaque(boolean opaque) {
+ mNode.mFlags = (mNode.mFlags & ~FLAGS_OPAQUE) | (opaque ? FLAGS_OPAQUE : 0);
+ }
+
+ @Override
+ public void setClassName(String className) {
+ mNode.mClassName = className;
+ }
+
+ @Override
+ public void setContentDescription(CharSequence contentDescription) {
+ mNode.mContentDescription = contentDescription;
+ }
+
+ @Override
+ public void setText(CharSequence text) {
+ final ViewNodeText t = getNodeText();
+ t.mText = TextUtils.trimNoCopySpans(text);
+ t.mTextSelectionStart = t.mTextSelectionEnd = -1;
+ }
+
+ @Override
+ public void setText(CharSequence text, int selectionStart, int selectionEnd) {
+ final ViewNodeText t = getNodeText();
+ t.mText = TextUtils.trimNoCopySpans(text);
+ t.mTextSelectionStart = selectionStart;
+ t.mTextSelectionEnd = selectionEnd;
+ }
+
+ @Override
+ public void setTextStyle(float size, int fgColor, int bgColor, int style) {
+ final ViewNodeText t = getNodeText();
+ t.mTextColor = fgColor;
+ t.mTextBackgroundColor = bgColor;
+ t.mTextSize = size;
+ t.mTextStyle = style;
+ }
+
+ @Override
+ public void setTextLines(int[] charOffsets, int[] baselines) {
+ final ViewNodeText t = getNodeText();
+ t.mLineCharOffsets = charOffsets;
+ t.mLineBaselines = baselines;
+ }
+
+ @Override
+ public void setTextIdEntry(@NonNull String entryName) {
+ mNode.mTextIdEntry = Preconditions.checkNotNull(entryName);
+ }
+
+ @Override
+ public void setHint(CharSequence hint) {
+ getNodeText().mHint = hint != null ? hint.toString() : null;
+ }
+
+ @Override
+ public void setHintIdEntry(String entryName) {
+ mNode.mHintIdEntry = Preconditions.checkNotNull(entryName);
+ }
+
+ @Override
+ public CharSequence getText() {
+ return mNode.getText();
+ }
+
+ @Override
+ public int getTextSelectionStart() {
+ return mNode.getTextSelectionStart();
+ }
+
+ @Override
+ public int getTextSelectionEnd() {
+ return mNode.getTextSelectionEnd();
+ }
+
+ @Override
+ public CharSequence getHint() {
+ return mNode.getHint();
+ }
+
+ @Override
+ public Bundle getExtras() {
+ if (mNode.mExtras != null) {
+ return mNode.mExtras;
+ }
+ mNode.mExtras = new Bundle();
+ return mNode.mExtras;
+ }
+
+ @Override
+ public boolean hasExtras() {
+ return mNode.mExtras != null;
+ }
+
+ @Override
+ public void setChildCount(int num) {
+ Log.w(TAG, "setChildCount() is not supported");
+ }
+
+ @Override
+ public int addChildCount(int num) {
+ Log.w(TAG, "addChildCount() is not supported");
+ return 0;
+ }
+
+ @Override
+ public int getChildCount() {
+ Log.w(TAG, "getChildCount() is not supported");
+ return 0;
+ }
+
+ @Override
+ public ViewStructure newChild(int index) {
+ Log.w(TAG, "newChild() is not supported");
+ return null;
+ }
+
+ @Override
+ public ViewStructure asyncNewChild(int index) {
+ Log.w(TAG, "asyncNewChild() is not supported");
+ return null;
+ }
+
+ @Override
+ public AutofillId getAutofillId() {
+ return mNode.mAutofillId;
+ }
+
+ @Override
+ public void setAutofillId(AutofillId id) {
+ mNode.mAutofillId = Preconditions.checkNotNull(id);
+ }
+
+
+ @Override
+ public void setAutofillId(AutofillId parentId, int virtualId) {
+ mNode.mParentAutofillId = Preconditions.checkNotNull(parentId);
+ mNode.mAutofillId = new AutofillId(parentId, virtualId);
+ }
+
+ @Override
+ public void setAutofillType(@View.AutofillType int type) {
+ mNode.mAutofillType = type;
+ }
+
+ @Override
+ public void setReceiveContentMimeTypes(@Nullable String[] mimeTypes) {
+ mNode.mReceiveContentMimeTypes = mimeTypes;
+ }
+
+ @Override
+ public void setAutofillHints(String[] hints) {
+ mNode.mAutofillHints = hints;
+ }
+
+ @Override
+ public void setAutofillValue(AutofillValue value) {
+ mNode.mAutofillValue = value;
+ }
+
+ @Override
+ public void setAutofillOptions(CharSequence[] options) {
+ mNode.mAutofillOptions = options;
+ }
+
+ @Override
+ public void setInputType(int inputType) {
+ mNode.mInputType = inputType;
+ }
+
+ @Override
+ public void setMinTextEms(int minEms) {
+ mNode.mMinEms = minEms;
+ }
+
+ @Override
+ public void setMaxTextEms(int maxEms) {
+ mNode.mMaxEms = maxEms;
+ }
+
+ @Override
+ public void setMaxTextLength(int maxLength) {
+ mNode.mMaxLength = maxLength;
+ }
+
+ @Override
+ public void setDataIsSensitive(boolean sensitive) {
+ Log.w(TAG, "setDataIsSensitive() is not supported");
+ }
+
+ @Override
+ public void asyncCommit() {
+ Log.w(TAG, "asyncCommit() is not supported");
+ }
+
+ @Override
+ public Rect getTempRect() {
+ Log.w(TAG, "getTempRect() is not supported");
+ return null;
+ }
+
+ @Override
+ public void setWebDomain(String domain) {
+ Log.w(TAG, "setWebDomain() is not supported");
+ }
+
+ @Override
+ public void setLocaleList(LocaleList localeList) {
+ mNode.mLocaleList = localeList;
+ }
+
+ @Override
+ public Builder newHtmlInfoBuilder(String tagName) {
+ Log.w(TAG, "newHtmlInfoBuilder() is not supported");
+ return null;
+ }
+
+ @Override
+ public void setHtmlInfo(HtmlInfo htmlInfo) {
+ Log.w(TAG, "setHtmlInfo() is not supported");
+ }
+
+ private ViewNodeText getNodeText() {
+ if (mNode.mText != null) {
+ return mNode.mText;
+ }
+ mNode.mText = new ViewNodeText();
+ return mNode.mText;
+ }
+ }
+
+ //TODO(b/122484602): copied 'as-is' from AssistStructure, except for writeSensitive
+ static final class ViewNodeText {
+ CharSequence mText;
+ float mTextSize;
+ int mTextStyle;
+ int mTextColor = ViewNode.TEXT_COLOR_UNDEFINED;
+ int mTextBackgroundColor = ViewNode.TEXT_COLOR_UNDEFINED;
+ int mTextSelectionStart;
+ int mTextSelectionEnd;
+ int[] mLineCharOffsets;
+ int[] mLineBaselines;
+ String mHint;
+
+ ViewNodeText() {
+ }
+
+ boolean isSimple() {
+ return mTextBackgroundColor == ViewNode.TEXT_COLOR_UNDEFINED
+ && mTextSelectionStart == 0 && mTextSelectionEnd == 0
+ && mLineCharOffsets == null && mLineBaselines == null && mHint == null;
+ }
+
+ ViewNodeText(Parcel in, boolean simple) {
+ mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mTextSize = in.readFloat();
+ mTextStyle = in.readInt();
+ mTextColor = in.readInt();
+ if (!simple) {
+ mTextBackgroundColor = in.readInt();
+ mTextSelectionStart = in.readInt();
+ mTextSelectionEnd = in.readInt();
+ mLineCharOffsets = in.createIntArray();
+ mLineBaselines = in.createIntArray();
+ mHint = in.readString();
+ }
+ }
+
+ void writeToParcel(Parcel out, boolean simple) {
+ TextUtils.writeToParcel(mText, out, 0);
+ out.writeFloat(mTextSize);
+ out.writeInt(mTextStyle);
+ out.writeInt(mTextColor);
+ if (!simple) {
+ out.writeInt(mTextBackgroundColor);
+ out.writeInt(mTextSelectionStart);
+ out.writeInt(mTextSelectionEnd);
+ out.writeIntArray(mLineCharOffsets);
+ out.writeIntArray(mLineBaselines);
+ out.writeString(mHint);
+ }
+ }
+ }
+}
diff --git a/android/view/displayhash/DisplayHash.java b/android/view/displayhash/DisplayHash.java
new file mode 100644
index 0000000..4ec0a59
--- /dev/null
+++ b/android/view/displayhash/DisplayHash.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.displayhash;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * The DisplayHash used to validate information about what was present on screen.
+ */
+public final class DisplayHash implements Parcelable {
+ /**
+ * The timestamp when the hash was generated.
+ */
+ @CurrentTimeMillisLong
+ private final long mTimeMillis;
+
+ /**
+ * The bounds of the requested area to generate the hash. This is in window space passed in
+ * by the client.
+ */
+ @NonNull
+ private final Rect mBoundsInWindow;
+
+ /**
+ * The selected hash algorithm that generated the image hash.
+ */
+ @NonNull
+ private final String mHashAlgorithm;
+
+ /**
+ * The image hash generated when creating the DisplayHash.
+ */
+ @NonNull
+ private final byte[] mImageHash;
+
+ /**
+ * The hmac generated by the system and used to verify whether this token was generated by
+ * the system.
+ */
+ @NonNull
+ private final byte[] mHmac;
+
+ /**
+ * Creates a new DisplayHash.
+ *
+ * @param timeMillis The timestamp when the hash was generated.
+ * @param boundsInWindow The bounds of the requested area to generate the hash. This is
+ * in window space passed in by the client.
+ * @param hashAlgorithm The selected hash algorithm that generated the image hash.
+ * @param imageHash The image hash generated when creating the DisplayHash.
+ * @param hmac The hmac generated by the system and used to verify whether this
+ * token was generated by
+ * the system. This should only be accessed by a system process.
+ * @hide
+ */
+ @SystemApi
+ public DisplayHash(@CurrentTimeMillisLong long timeMillis, @NonNull Rect boundsInWindow,
+ @NonNull String hashAlgorithm, @NonNull byte[] imageHash, @NonNull byte[] hmac) {
+ mTimeMillis = timeMillis;
+ mBoundsInWindow = boundsInWindow;
+ AnnotationValidations.validate(NonNull.class, null, mBoundsInWindow);
+ mHashAlgorithm = hashAlgorithm;
+ AnnotationValidations.validate(NonNull.class, null, mHashAlgorithm);
+ mImageHash = imageHash;
+ AnnotationValidations.validate(NonNull.class, null, mImageHash);
+ mHmac = hmac;
+ AnnotationValidations.validate(NonNull.class, null, mHmac);
+ }
+
+ /**
+ * The timestamp when the hash was generated.
+ *
+ * @hide
+ */
+ @SystemApi
+ @CurrentTimeMillisLong
+ public long getTimeMillis() {
+ return mTimeMillis;
+ }
+
+ /**
+ * The bounds of the requested area to to generate the hash. This is in window space passed in
+ * by the client.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Rect getBoundsInWindow() {
+ return mBoundsInWindow;
+ }
+
+ /**
+ * The selected hash algorithm that generated the image hash.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public String getHashAlgorithm() {
+ return mHashAlgorithm;
+ }
+
+ /**
+ * The image hash generated when creating the DisplayHash.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public byte[] getImageHash() {
+ return mImageHash;
+ }
+
+ /**
+ * The hmac generated by the system and used to verify whether this token was generated by
+ * the system. This should only be accessed by a system process.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public byte[] getHmac() {
+ return mHmac;
+ }
+
+ /** @hide **/
+ @Override
+ public String toString() {
+ return "DisplayHash { "
+ + "timeMillis = " + mTimeMillis + ", "
+ + "boundsInWindow = " + mBoundsInWindow + ", "
+ + "hashAlgorithm = " + mHashAlgorithm + ", "
+ + "imageHash = " + byteArrayToString(mImageHash) + ", "
+ + "hmac = " + byteArrayToString(mHmac)
+ + " }";
+ }
+
+ private String byteArrayToString(byte[] byteArray) {
+ if (byteArray == null) {
+ return "null";
+ }
+ int iMax = byteArray.length - 1;
+ if (iMax == -1) {
+ return "[]";
+ }
+
+ StringBuilder b = new StringBuilder();
+ b.append('[');
+ for (int i = 0; ; i++) {
+ String formatted = String.format("%02X", byteArray[i] & 0xFF);
+ b.append(formatted);
+ if (i == iMax) {
+ return b.append(']').toString();
+ }
+ b.append(", ");
+ }
+ }
+
+ /** @hide **/
+ @SystemApi
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mTimeMillis);
+ dest.writeTypedObject(mBoundsInWindow, flags);
+ dest.writeString(mHashAlgorithm);
+ dest.writeByteArray(mImageHash);
+ dest.writeByteArray(mHmac);
+ }
+
+ /** @hide **/
+ @SystemApi
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private DisplayHash(@NonNull Parcel in) {
+ mTimeMillis = in.readLong();
+ Rect boundsInWindow = in.readTypedObject(Rect.CREATOR);
+ String hashAlgorithm = in.readString();
+ byte[] imageHash = in.createByteArray();
+ byte[] hmac = in.createByteArray();
+
+ mBoundsInWindow = boundsInWindow;
+ AnnotationValidations.validate(NonNull.class, null, mBoundsInWindow);
+ mHashAlgorithm = hashAlgorithm;
+ AnnotationValidations.validate(NonNull.class, null, mHashAlgorithm);
+ mImageHash = imageHash;
+ AnnotationValidations.validate(NonNull.class, null, mImageHash);
+ mHmac = hmac;
+ AnnotationValidations.validate(NonNull.class, null, mHmac);
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<DisplayHash> CREATOR =
+ new Parcelable.Creator<DisplayHash>() {
+ @Override
+ public DisplayHash[] newArray(int size) {
+ return new DisplayHash[size];
+ }
+
+ @Override
+ public DisplayHash createFromParcel(@NonNull Parcel in) {
+ return new DisplayHash(in);
+ }
+ };
+}
diff --git a/android/view/displayhash/DisplayHashManager.java b/android/view/displayhash/DisplayHashManager.java
new file mode 100644
index 0000000..4f5fef6
--- /dev/null
+++ b/android/view/displayhash/DisplayHashManager.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.displayhash;
+
+
+import static android.content.Context.DISPLAY_HASH_SERVICE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Manages DisplayHash requests. The manager object can be retrieved by calling
+ * {@code Context.getSystemService(Context.DISPLAY_HASH_SERVICE)}
+ */
+@SystemService(DISPLAY_HASH_SERVICE)
+public final class DisplayHashManager {
+ private static final String TAG = "DisplayHashManager";
+
+ private final Object mSupportedHashingAlgorithmLock = new Object();
+
+ @GuardedBy("mSupportedAlgorithmLock")
+ private static Set<String> sSupportedHashAlgorithms;
+
+ /**
+ * @hide
+ */
+ public DisplayHashManager() {
+ }
+
+ /**
+ * Get a Set of DisplayHash algorithms that the device supports.
+ *
+ * @return a String Set of supported hashing algorithms. The String value of one
+ * algorithm should be used when requesting to generate the DisplayHash.
+ */
+ @NonNull
+ public Set<String> getSupportedHashAlgorithms() {
+ synchronized (mSupportedHashingAlgorithmLock) {
+ if (sSupportedHashAlgorithms != null) {
+ return sSupportedHashAlgorithms;
+ }
+
+ try {
+ String[] supportedAlgorithms = WindowManagerGlobal.getWindowManagerService()
+ .getSupportedDisplayHashAlgorithms();
+ if (supportedAlgorithms == null) {
+ return Collections.emptySet();
+ }
+ sSupportedHashAlgorithms = new ArraySet<>(supportedAlgorithms);
+ return sSupportedHashAlgorithms;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send request getSupportedHashingAlgorithms", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Call to verify that the DisplayHash passed in was generated by the system.
+ *
+ * @param displayHash The hash to verify that it was generated by the system.
+ * @return a {@link VerifiedDisplayHash} if the hash was generated by the system or null
+ * if the hash cannot be verified.
+ */
+ @Nullable
+ public VerifiedDisplayHash verifyDisplayHash(@NonNull DisplayHash displayHash) {
+ try {
+ return WindowManagerGlobal.getWindowManagerService().verifyDisplayHash(displayHash);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send request verifyImpressionToken", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Call to enable or disable the throttling when generating a display hash. This should only be
+ * used for testing. Throttling is enabled by default.
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(android.Manifest.permission.READ_FRAME_BUFFER)
+ public void setDisplayHashThrottlingEnabled(boolean enable) {
+ try {
+ WindowManagerGlobal.getWindowManagerService().setDisplayHashThrottlingEnabled(enable);
+ } catch (RemoteException e) {
+ }
+ }
+}
diff --git a/android/view/displayhash/DisplayHashResultCallback.java b/android/view/displayhash/DisplayHashResultCallback.java
new file mode 100644
index 0000000..6e3d9a8
--- /dev/null
+++ b/android/view/displayhash/DisplayHashResultCallback.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.displayhash;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * Use when calling {@link View#generateDisplayHash(String, Rect, Executor,
+ * DisplayHashResultCallback)}.
+ *
+ * The callback will only invoke either {@link #onDisplayHashResult} when the system successfully
+ * generated the {@link DisplayHash} or {@link #onDisplayHashError(int)} when it failed.
+ */
+public interface DisplayHashResultCallback {
+ /**
+ * @hide
+ */
+ String EXTRA_DISPLAY_HASH = "DISPLAY_HASH";
+
+ /**
+ * @hide
+ */
+ String EXTRA_DISPLAY_HASH_ERROR_CODE = "DISPLAY_HASH_ERROR_CODE";
+
+ /**
+ * An unknown error occurred.
+ */
+ int DISPLAY_HASH_ERROR_UNKNOWN = -1;
+
+ /**
+ * The bounds used when requesting the hash hash were invalid or empty.
+ */
+ int DISPLAY_HASH_ERROR_INVALID_BOUNDS = -2;
+
+ /**
+ * The window for the view that requested the hash is no longer around. This can happen if the
+ * window is getting torn down.
+ */
+ int DISPLAY_HASH_ERROR_MISSING_WINDOW = -3;
+
+ /**
+ * The view that requested the hash is not visible on screen. This could either mean
+ * that the view bounds are offscreen, window bounds are offscreen, view is not visible, or
+ * window is not visible.
+ */
+ int DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN = -4;
+
+ /**
+ * The hash algorithm sent to generate the hash was invalid. This means the value is not one
+ * of the supported values in {@link DisplayHashManager#getSupportedHashAlgorithms()}
+ */
+ int DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM = -5;
+
+ /**
+ * The caller requested to generate the hash too frequently. The caller should try again at a
+ * after some time has passed to ensure the system isn't overloaded.
+ */
+ int DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS = -6;
+
+ /** @hide */
+ @IntDef(prefix = {"DISPLAY_HASH_ERROR_"}, value = {
+ DISPLAY_HASH_ERROR_UNKNOWN,
+ DISPLAY_HASH_ERROR_INVALID_BOUNDS,
+ DISPLAY_HASH_ERROR_MISSING_WINDOW,
+ DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN,
+ DISPLAY_HASH_ERROR_INVALID_HASH_ALGORITHM,
+ DISPLAY_HASH_ERROR_TOO_MANY_REQUESTS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface DisplayHashErrorCode {
+ }
+
+ /**
+ * Callback invoked when calling
+ * {@link android.view.View#generateDisplayHash(String, Rect, Executor,
+ * DisplayHashResultCallback)}
+ *
+ * @param displayHash The DisplayHash generated. If the hash cannot be generated,
+ * {@link #onDisplayHashError(int)} will be called instead
+ */
+ void onDisplayHashResult(@NonNull DisplayHash displayHash);
+
+ /**
+ * Callback invoked when
+ * {@link android.view.View#generateDisplayHash(String, Rect, Executor,
+ * DisplayHashResultCallback)} results in an error and cannot generate a display hash.
+ *
+ * @param errorCode One of the values in {@link DisplayHashErrorCode}
+ */
+ void onDisplayHashError(@DisplayHashErrorCode int errorCode);
+}
diff --git a/android/view/displayhash/VerifiedDisplayHash.java b/android/view/displayhash/VerifiedDisplayHash.java
new file mode 100644
index 0000000..b17c068
--- /dev/null
+++ b/android/view/displayhash/VerifiedDisplayHash.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.displayhash;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * The verified display hash used to validate information about what was present on screen.
+ */
+@DataClass(genToString = true, genAidl = true)
+public final class VerifiedDisplayHash implements Parcelable {
+ /**
+ * The timestamp when the hash was generated.
+ */
+ @CurrentTimeMillisLong
+ private final long mTimeMillis;
+
+ /**
+ * The bounds of the requested area to generate the hash. This is in window space passed in
+ * by the client.
+ */
+ @NonNull
+ private final Rect mBoundsInWindow;
+
+ /**
+ * The selected hash algorithm that generated the image hash.
+ */
+ @NonNull
+ private final String mHashAlgorithm;
+
+ /**
+ * The image hash generated when creating the DisplayHash.
+ */
+ @NonNull
+ private final byte[] mImageHash;
+
+ private String imageHashToString() {
+ return byteArrayToString(mImageHash);
+ }
+
+ private String byteArrayToString(byte[] byteArray) {
+ if (byteArray == null) {
+ return "null";
+ }
+ int iMax = byteArray.length - 1;
+ if (iMax == -1) {
+ return "[]";
+ }
+
+ StringBuilder b = new StringBuilder();
+ b.append('[');
+ for (int i = 0; ; i++) {
+ String formatted = String.format("%02X", byteArray[i] & 0xFF);
+ b.append(formatted);
+ if (i == iMax) {
+ return b.append(']').toString();
+ }
+ b.append(", ");
+ }
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/displayhash/VerifiedDisplayHash.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new VerifiedDisplayHash.
+ *
+ * @param timeMillis
+ * The timestamp when the hash was generated.
+ * @param boundsInWindow
+ * The bounds of the requested area to generate the hash. This is in window space passed in
+ * by the client.
+ * @param hashAlgorithm
+ * The selected hash algorithm that generated the image hash.
+ * @param imageHash
+ * The image hash generated when creating the DisplayHash.
+ */
+ @DataClass.Generated.Member
+ public VerifiedDisplayHash(
+ @CurrentTimeMillisLong long timeMillis,
+ @NonNull Rect boundsInWindow,
+ @NonNull String hashAlgorithm,
+ @NonNull byte[] imageHash) {
+ this.mTimeMillis = timeMillis;
+ com.android.internal.util.AnnotationValidations.validate(
+ CurrentTimeMillisLong.class, null, mTimeMillis);
+ this.mBoundsInWindow = boundsInWindow;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mBoundsInWindow);
+ this.mHashAlgorithm = hashAlgorithm;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHashAlgorithm);
+ this.mImageHash = imageHash;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mImageHash);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The timestamp when the hash was generated.
+ */
+ @DataClass.Generated.Member
+ public @CurrentTimeMillisLong long getTimeMillis() {
+ return mTimeMillis;
+ }
+
+ /**
+ * The bounds of the requested area to generate the hash. This is in window space passed in
+ * by the client.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Rect getBoundsInWindow() {
+ return mBoundsInWindow;
+ }
+
+ /**
+ * The selected hash algorithm that generated the image hash.
+ */
+ @DataClass.Generated.Member
+ public @NonNull String getHashAlgorithm() {
+ return mHashAlgorithm;
+ }
+
+ /**
+ * The image hash generated when creating the DisplayHash.
+ */
+ @DataClass.Generated.Member
+ public @NonNull byte[] getImageHash() {
+ return mImageHash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "VerifiedDisplayHash { " +
+ "timeMillis = " + mTimeMillis + ", " +
+ "boundsInWindow = " + mBoundsInWindow + ", " +
+ "hashAlgorithm = " + mHashAlgorithm + ", " +
+ "imageHash = " + imageHashToString() +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeLong(mTimeMillis);
+ dest.writeTypedObject(mBoundsInWindow, flags);
+ dest.writeString(mHashAlgorithm);
+ dest.writeByteArray(mImageHash);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ VerifiedDisplayHash(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ long timeMillis = in.readLong();
+ Rect boundsInWindow = (Rect) in.readTypedObject(Rect.CREATOR);
+ String hashAlgorithm = in.readString();
+ byte[] imageHash = in.createByteArray();
+
+ this.mTimeMillis = timeMillis;
+ com.android.internal.util.AnnotationValidations.validate(
+ CurrentTimeMillisLong.class, null, mTimeMillis);
+ this.mBoundsInWindow = boundsInWindow;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mBoundsInWindow);
+ this.mHashAlgorithm = hashAlgorithm;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHashAlgorithm);
+ this.mImageHash = imageHash;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mImageHash);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<VerifiedDisplayHash> CREATOR
+ = new Parcelable.Creator<VerifiedDisplayHash>() {
+ @Override
+ public VerifiedDisplayHash[] newArray(int size) {
+ return new VerifiedDisplayHash[size];
+ }
+
+ @Override
+ public VerifiedDisplayHash createFromParcel(@NonNull android.os.Parcel in) {
+ return new VerifiedDisplayHash(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1617747271440L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/displayhash/VerifiedDisplayHash.java",
+ inputSignatures = "private final @android.annotation.CurrentTimeMillisLong long mTimeMillis\nprivate final @android.annotation.NonNull android.graphics.Rect mBoundsInWindow\nprivate final @android.annotation.NonNull java.lang.String mHashAlgorithm\nprivate final @android.annotation.NonNull byte[] mImageHash\nprivate java.lang.String imageHashToString()\nprivate java.lang.String byteArrayToString(byte[])\nclass VerifiedDisplayHash extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true, genAidl=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/inputmethod/BaseInputConnection.java b/android/view/inputmethod/BaseInputConnection.java
new file mode 100644
index 0000000..c4540b0
--- /dev/null
+++ b/android/view/inputmethod/BaseInputConnection.java
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import static android.view.ContentInfo.SOURCE_INPUT_METHOD;
+
+import android.annotation.CallSuper;
+import android.annotation.IntRange;
+import android.annotation.Nullable;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.MetaKeyKeyListener;
+import android.util.Log;
+import android.util.LogPrinter;
+import android.view.ContentInfo;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.internal.util.Preconditions;
+
+class ComposingText implements NoCopySpan {
+}
+
+/**
+ * Base class for implementors of the InputConnection interface, taking care
+ * of most of the common behavior for providing a connection to an Editable.
+ * Implementors of this class will want to be sure to implement
+ * {@link #getEditable} to provide access to their own editable object, and
+ * to refer to the documentation in {@link InputConnection}.
+ */
+public class BaseInputConnection implements InputConnection {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "BaseInputConnection";
+ static final Object COMPOSING = new ComposingText();
+
+ /** @hide */
+ protected final InputMethodManager mIMM;
+ final View mTargetView;
+ final boolean mFallbackMode;
+
+ private Object[] mDefaultComposingSpans;
+
+ Editable mEditable;
+ KeyCharacterMap mKeyCharacterMap;
+
+ BaseInputConnection(InputMethodManager mgr, boolean fullEditor) {
+ mIMM = mgr;
+ mTargetView = null;
+ mFallbackMode = !fullEditor;
+ }
+
+ public BaseInputConnection(View targetView, boolean fullEditor) {
+ mIMM = (InputMethodManager)targetView.getContext().getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ mTargetView = targetView;
+ mFallbackMode = !fullEditor;
+ }
+
+ public static final void removeComposingSpans(Spannable text) {
+ text.removeSpan(COMPOSING);
+ Object[] sps = text.getSpans(0, text.length(), Object.class);
+ if (sps != null) {
+ for (int i=sps.length-1; i>=0; i--) {
+ Object o = sps[i];
+ if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) {
+ text.removeSpan(o);
+ }
+ }
+ }
+ }
+
+ public static void setComposingSpans(Spannable text) {
+ setComposingSpans(text, 0, text.length());
+ }
+
+ /** @hide */
+ public static void setComposingSpans(Spannable text, int start, int end) {
+ final Object[] sps = text.getSpans(start, end, Object.class);
+ if (sps != null) {
+ for (int i=sps.length-1; i>=0; i--) {
+ final Object o = sps[i];
+ if (o == COMPOSING) {
+ text.removeSpan(o);
+ continue;
+ }
+
+ final int fl = text.getSpanFlags(o);
+ if ((fl & (Spanned.SPAN_COMPOSING | Spanned.SPAN_POINT_MARK_MASK))
+ != (Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) {
+ text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o),
+ (fl & ~Spanned.SPAN_POINT_MARK_MASK)
+ | Spanned.SPAN_COMPOSING
+ | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ text.setSpan(COMPOSING, start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
+ }
+
+ public static int getComposingSpanStart(Spannable text) {
+ return text.getSpanStart(COMPOSING);
+ }
+
+ public static int getComposingSpanEnd(Spannable text) {
+ return text.getSpanEnd(COMPOSING);
+ }
+
+ /**
+ * Return the target of edit operations. The default implementation
+ * returns its own fake editable that is just used for composing text;
+ * subclasses that are real text editors should override this and
+ * supply their own.
+ */
+ public Editable getEditable() {
+ if (mEditable == null) {
+ mEditable = Editable.Factory.getInstance().newEditable("");
+ Selection.setSelection(mEditable, 0);
+ }
+ return mEditable;
+ }
+
+ /**
+ * Default implementation does nothing.
+ */
+ public boolean beginBatchEdit() {
+ return false;
+ }
+
+ /**
+ * Default implementation does nothing.
+ */
+ public boolean endBatchEdit() {
+ return false;
+ }
+
+ /**
+ * Called after only the composing region is modified (so it isn't called if the text also
+ * changes).
+ * <p>
+ * Default implementation does nothing.
+ *
+ * @hide
+ */
+ public void endComposingRegionEditInternal() {
+ }
+
+ /**
+ * Default implementation calls {@link #finishComposingText()} and
+ * {@code setImeConsumesInput(false)}.
+ */
+ @CallSuper
+ public void closeConnection() {
+ finishComposingText();
+ setImeConsumesInput(false);
+ }
+
+ /**
+ * Default implementation uses
+ * {@link MetaKeyKeyListener#clearMetaKeyState(long, int)
+ * MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state.
+ */
+ public boolean clearMetaKeyStates(int states) {
+ final Editable content = getEditable();
+ if (content == null) return false;
+ MetaKeyKeyListener.clearMetaKeyState(content, states);
+ return true;
+ }
+
+ /**
+ * Default implementation does nothing and returns false.
+ */
+ public boolean commitCompletion(CompletionInfo text) {
+ return false;
+ }
+
+ /**
+ * Default implementation does nothing and returns false.
+ */
+ public boolean commitCorrection(CorrectionInfo correctionInfo) {
+ return false;
+ }
+
+ /**
+ * Default implementation replaces any existing composing text with
+ * the given text. In addition, only if fallback mode, a key event is
+ * sent for the new text and the current editable buffer cleared.
+ */
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ if (DEBUG) Log.v(TAG, "commitText " + text);
+ replaceText(text, newCursorPosition, false);
+ sendCurrentText();
+ return true;
+ }
+
+ /**
+ * The default implementation performs the deletion around the current selection position of the
+ * editable text.
+ *
+ * @param beforeLength The number of characters before the cursor to be deleted, in code unit.
+ * If this is greater than the number of existing characters between the beginning of the
+ * text and the cursor, then this method does not fail but deletes all the characters in
+ * that range.
+ * @param afterLength The number of characters after the cursor to be deleted, in code unit.
+ * If this is greater than the number of existing characters between the cursor and
+ * the end of the text, then this method does not fail but deletes all the characters in
+ * that range.
+ *
+ * @return {@code true} when selected text is deleted, {@code false} when either the
+ * selection is invalid or not yet attached (i.e. selection start or end is -1),
+ * or the editable text is {@code null}.
+ */
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
+ + " / " + afterLength);
+ final Editable content = getEditable();
+ if (content == null) return false;
+
+ beginBatchEdit();
+
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ // Skip when the selection is not yet attached.
+ if (a == -1 || b == -1) {
+ endBatchEdit();
+ return false;
+ }
+
+ // Ignore the composing text.
+ int ca = getComposingSpanStart(content);
+ int cb = getComposingSpanEnd(content);
+ if (cb < ca) {
+ int tmp = ca;
+ ca = cb;
+ cb = tmp;
+ }
+ if (ca != -1 && cb != -1) {
+ if (ca < a) a = ca;
+ if (cb > b) b = cb;
+ }
+
+ int deleted = 0;
+
+ if (beforeLength > 0) {
+ int start = a - beforeLength;
+ if (start < 0) start = 0;
+
+ final int numDeleteBefore = a - start;
+ if (a >= 0 && numDeleteBefore > 0) {
+ content.delete(start, a);
+ deleted = numDeleteBefore;
+ }
+ }
+
+ if (afterLength > 0) {
+ b = b - deleted;
+
+ int end = b + afterLength;
+ if (end > content.length()) end = content.length();
+
+ final int numDeleteAfter = end - b;
+ if (b >= 0 && numDeleteAfter > 0) {
+ content.delete(b, end);
+ }
+ }
+
+ endBatchEdit();
+
+ return true;
+ }
+
+ private static int INVALID_INDEX = -1;
+ private static int findIndexBackward(final CharSequence cs, final int from,
+ final int numCodePoints) {
+ int currentIndex = from;
+ boolean waitingHighSurrogate = false;
+ final int N = cs.length();
+ if (currentIndex < 0 || N < currentIndex) {
+ return INVALID_INDEX; // The starting point is out of range.
+ }
+ if (numCodePoints < 0) {
+ return INVALID_INDEX; // Basically this should not happen.
+ }
+ int remainingCodePoints = numCodePoints;
+ while (true) {
+ if (remainingCodePoints == 0) {
+ return currentIndex; // Reached to the requested length in code points.
+ }
+
+ --currentIndex;
+ if (currentIndex < 0) {
+ if (waitingHighSurrogate) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ return 0; // Reached to the beginning of the text w/o any invalid surrogate pair.
+ }
+ final char c = cs.charAt(currentIndex);
+ if (waitingHighSurrogate) {
+ if (!java.lang.Character.isHighSurrogate(c)) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ waitingHighSurrogate = false;
+ --remainingCodePoints;
+ continue;
+ }
+ if (!java.lang.Character.isSurrogate(c)) {
+ --remainingCodePoints;
+ continue;
+ }
+ if (java.lang.Character.isHighSurrogate(c)) {
+ return INVALID_INDEX; // A invalid surrogate pair is found.
+ }
+ waitingHighSurrogate = true;
+ }
+ }
+
+ private static int findIndexForward(final CharSequence cs, final int from,
+ final int numCodePoints) {
+ int currentIndex = from;
+ boolean waitingLowSurrogate = false;
+ final int N = cs.length();
+ if (currentIndex < 0 || N < currentIndex) {
+ return INVALID_INDEX; // The starting point is out of range.
+ }
+ if (numCodePoints < 0) {
+ return INVALID_INDEX; // Basically this should not happen.
+ }
+ int remainingCodePoints = numCodePoints;
+
+ while (true) {
+ if (remainingCodePoints == 0) {
+ return currentIndex; // Reached to the requested length in code points.
+ }
+
+ if (currentIndex >= N) {
+ if (waitingLowSurrogate) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ return N; // Reached to the end of the text w/o any invalid surrogate pair.
+ }
+ final char c = cs.charAt(currentIndex);
+ if (waitingLowSurrogate) {
+ if (!java.lang.Character.isLowSurrogate(c)) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ --remainingCodePoints;
+ waitingLowSurrogate = false;
+ ++currentIndex;
+ continue;
+ }
+ if (!java.lang.Character.isSurrogate(c)) {
+ --remainingCodePoints;
+ ++currentIndex;
+ continue;
+ }
+ if (java.lang.Character.isLowSurrogate(c)) {
+ return INVALID_INDEX; // A invalid surrogate pair is found.
+ }
+ waitingLowSurrogate = true;
+ ++currentIndex;
+ }
+ }
+
+ /**
+ * The default implementation performs the deletion around the current selection position of the
+ * editable text.
+ * @param beforeLength The number of characters before the cursor to be deleted, in code points.
+ * If this is greater than the number of existing characters between the beginning of the
+ * text and the cursor, then this method does not fail but deletes all the characters in
+ * that range.
+ * @param afterLength The number of characters after the cursor to be deleted, in code points.
+ * If this is greater than the number of existing characters between the cursor and
+ * the end of the text, then this method does not fail but deletes all the characters in
+ * that range.
+ */
+ public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
+ if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
+ + " / " + afterLength);
+ final Editable content = getEditable();
+ if (content == null) return false;
+
+ beginBatchEdit();
+
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ // Ignore the composing text.
+ int ca = getComposingSpanStart(content);
+ int cb = getComposingSpanEnd(content);
+ if (cb < ca) {
+ int tmp = ca;
+ ca = cb;
+ cb = tmp;
+ }
+ if (ca != -1 && cb != -1) {
+ if (ca < a) a = ca;
+ if (cb > b) b = cb;
+ }
+
+ if (a >= 0 && b >= 0) {
+ final int start = findIndexBackward(content, a, Math.max(beforeLength, 0));
+ if (start != INVALID_INDEX) {
+ final int end = findIndexForward(content, b, Math.max(afterLength, 0));
+ if (end != INVALID_INDEX) {
+ final int numDeleteBefore = a - start;
+ if (numDeleteBefore > 0) {
+ content.delete(start, a);
+ }
+ final int numDeleteAfter = end - b;
+ if (numDeleteAfter > 0) {
+ content.delete(b - numDeleteBefore, end - numDeleteBefore);
+ }
+ }
+ }
+ // NOTE: You may think we should return false here if start and/or end is INVALID_INDEX,
+ // but the truth is that IInputConnectionWrapper running in the middle of IPC calls
+ // always returns true to the IME without waiting for the completion of this method as
+ // IInputConnectionWrapper#isAtive() returns true. This is actually why some methods
+ // including this method look like asynchronous calls from the IME.
+ }
+
+ endBatchEdit();
+
+ return true;
+ }
+
+ /**
+ * The default implementation removes the composing state from the
+ * current editable text. In addition, only if fallback mode, a key event is
+ * sent for the new text and the current editable buffer cleared.
+ */
+ public boolean finishComposingText() {
+ if (DEBUG) Log.v(TAG, "finishComposingText");
+ final Editable content = getEditable();
+ if (content != null) {
+ beginBatchEdit();
+ removeComposingSpans(content);
+ // Note: sendCurrentText does nothing unless mFallbackMode is set
+ sendCurrentText();
+ endBatchEdit();
+ endComposingRegionEditInternal();
+ }
+ return true;
+ }
+
+ /**
+ * The default implementation uses TextUtils.getCapsMode to get the
+ * cursor caps mode for the current selection position in the editable
+ * text, unless in fallback mode in which case 0 is always returned.
+ */
+ public int getCursorCapsMode(int reqModes) {
+ if (mFallbackMode) return 0;
+
+ final Editable content = getEditable();
+ if (content == null) return 0;
+
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ return TextUtils.getCapsMode(content, a, reqModes);
+ }
+
+ /**
+ * The default implementation always returns null.
+ */
+ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+ return null;
+ }
+
+ /**
+ * The default implementation returns the given amount of text from the
+ * current cursor position in the buffer.
+ */
+ @Nullable
+ public CharSequence getTextBeforeCursor(@IntRange(from = 0) int length, int flags) {
+ Preconditions.checkArgumentNonnegative(length);
+
+ final Editable content = getEditable();
+ if (content == null) return null;
+
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ if (a <= 0) {
+ return "";
+ }
+
+ if (length > a) {
+ length = a;
+ }
+
+ if ((flags&GET_TEXT_WITH_STYLES) != 0) {
+ return content.subSequence(a - length, a);
+ }
+ return TextUtils.substring(content, a - length, a);
+ }
+
+ /**
+ * The default implementation returns the text currently selected, or null if none is
+ * selected.
+ */
+ public CharSequence getSelectedText(int flags) {
+ final Editable content = getEditable();
+ if (content == null) return null;
+
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ if (a == b || a < 0) return null;
+
+ if ((flags&GET_TEXT_WITH_STYLES) != 0) {
+ return content.subSequence(a, b);
+ }
+ return TextUtils.substring(content, a, b);
+ }
+
+ /**
+ * The default implementation returns the given amount of text from the
+ * current cursor position in the buffer.
+ */
+ @Nullable
+ public CharSequence getTextAfterCursor(@IntRange(from = 0) int length, int flags) {
+ Preconditions.checkArgumentNonnegative(length);
+
+ final Editable content = getEditable();
+ if (content == null) return null;
+
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ // Guard against the case where the cursor has not been positioned yet.
+ if (b < 0) {
+ b = 0;
+ }
+
+ if (b + length > content.length()) {
+ length = content.length() - b;
+ }
+
+
+ if ((flags&GET_TEXT_WITH_STYLES) != 0) {
+ return content.subSequence(b, b + length);
+ }
+ return TextUtils.substring(content, b, b + length);
+ }
+
+ /**
+ * The default implementation returns the given amount of text around the current cursor
+ * position in the buffer.
+ */
+ @Nullable
+ public SurroundingText getSurroundingText(
+ @IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength, int flags) {
+ Preconditions.checkArgumentNonnegative(beforeLength);
+ Preconditions.checkArgumentNonnegative(afterLength);
+
+ final Editable content = getEditable();
+ // If {@link #getEditable()} is null or {@code mEditable} is equal to {@link #getEditable()}
+ // (a.k.a, a fake editable), it means we cannot get valid content from the editable, so
+ // fallback to retrieve surrounding text from other APIs.
+ if (content == null || mEditable == content) {
+ return InputConnection.super.getSurroundingText(beforeLength, afterLength, flags);
+ }
+
+ int selStart = Selection.getSelectionStart(content);
+ int selEnd = Selection.getSelectionEnd(content);
+
+ // Guard against the case where the cursor has not been positioned yet.
+ if (selStart < 0 || selEnd < 0) {
+ return null;
+ }
+
+ if (selStart > selEnd) {
+ int tmp = selStart;
+ selStart = selEnd;
+ selEnd = tmp;
+ }
+
+ int contentLength = content.length();
+ int startPos = selStart - beforeLength;
+ int endPos = selEnd + afterLength;
+
+ // Guards the start and end pos within range [0, contentLength].
+ startPos = Math.max(0, startPos);
+ endPos = Math.min(contentLength, endPos);
+
+ CharSequence surroundingText;
+ if ((flags & GET_TEXT_WITH_STYLES) != 0) {
+ surroundingText = content.subSequence(startPos, endPos);
+ } else {
+ surroundingText = TextUtils.substring(content, startPos, endPos);
+ }
+ return new SurroundingText(
+ surroundingText, selStart - startPos, selEnd - startPos, startPos);
+ }
+
+ /**
+ * The default implementation turns this into the enter key.
+ */
+ public boolean performEditorAction(int actionCode) {
+ long eventTime = SystemClock.uptimeMillis();
+ sendKeyEvent(new KeyEvent(eventTime, eventTime,
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
+ | KeyEvent.FLAG_EDITOR_ACTION));
+ sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
+ | KeyEvent.FLAG_EDITOR_ACTION));
+ return true;
+ }
+
+ /**
+ * The default implementation does nothing.
+ */
+ public boolean performContextMenuAction(int id) {
+ return false;
+ }
+
+ /**
+ * The default implementation does nothing.
+ */
+ public boolean performPrivateCommand(String action, Bundle data) {
+ return false;
+ }
+
+ /**
+ * The default implementation does nothing.
+ */
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+ return false;
+ }
+
+ public Handler getHandler() {
+ return null;
+ }
+
+ /**
+ * The default implementation places the given text into the editable,
+ * replacing any existing composing text. The new text is marked as
+ * in a composing state with the composing style.
+ */
+ public boolean setComposingText(CharSequence text, int newCursorPosition) {
+ if (DEBUG) Log.v(TAG, "setComposingText " + text);
+ replaceText(text, newCursorPosition, true);
+ return true;
+ }
+
+ public boolean setComposingRegion(int start, int end) {
+ final Editable content = getEditable();
+ if (content != null) {
+ beginBatchEdit();
+ removeComposingSpans(content);
+ int a = start;
+ int b = end;
+ if (a > b) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+ // Clip the end points to be within the content bounds.
+ final int length = content.length();
+ if (a < 0) a = 0;
+ if (b < 0) b = 0;
+ if (a > length) a = length;
+ if (b > length) b = length;
+
+ ensureDefaultComposingSpans();
+ if (mDefaultComposingSpans != null) {
+ for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
+ content.setSpan(mDefaultComposingSpans[i], a, b,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
+ }
+ }
+
+ content.setSpan(COMPOSING, a, b,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
+
+ // Note: sendCurrentText does nothing unless mFallbackMode is set
+ sendCurrentText();
+ endBatchEdit();
+ endComposingRegionEditInternal();
+ }
+ return true;
+ }
+
+ /**
+ * The default implementation changes the selection position in the
+ * current editable text.
+ */
+ public boolean setSelection(int start, int end) {
+ if (DEBUG) Log.v(TAG, "setSelection " + start + ", " + end);
+ final Editable content = getEditable();
+ if (content == null) return false;
+ int len = content.length();
+ if (start > len || end > len || start < 0 || end < 0) {
+ // If the given selection is out of bounds, just ignore it.
+ // Most likely the text was changed out from under the IME,
+ // and the IME is going to have to update all of its state
+ // anyway.
+ return true;
+ }
+ if (start == end && MetaKeyKeyListener.getMetaState(content,
+ MetaKeyKeyListener.META_SELECTING) != 0) {
+ // If we are in selection mode, then we want to extend the
+ // selection instead of replacing it.
+ Selection.extendSelection(content, start);
+ } else {
+ Selection.setSelection(content, start, end);
+ }
+ return true;
+ }
+
+ /**
+ * Provides standard implementation for sending a key event to the window
+ * attached to the input connection's view.
+ */
+ public boolean sendKeyEvent(KeyEvent event) {
+ mIMM.dispatchKeyEventFromInputMethod(mTargetView, event);
+ return false;
+ }
+
+ /**
+ * Updates InputMethodManager with the current fullscreen mode.
+ */
+ public boolean reportFullscreenMode(boolean enabled) {
+ return true;
+ }
+
+ private void sendCurrentText() {
+ if (!mFallbackMode) {
+ return;
+ }
+
+ Editable content = getEditable();
+ if (content != null) {
+ final int N = content.length();
+ if (N == 0) {
+ return;
+ }
+ if (N == 1) {
+ // If it's 1 character, we have a chance of being
+ // able to generate normal key events...
+ if (mKeyCharacterMap == null) {
+ mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ }
+ char[] chars = new char[1];
+ content.getChars(0, 1, chars, 0);
+ KeyEvent[] events = mKeyCharacterMap.getEvents(chars);
+ if (events != null) {
+ for (int i=0; i<events.length; i++) {
+ if (DEBUG) Log.v(TAG, "Sending: " + events[i]);
+ sendKeyEvent(events[i]);
+ }
+ content.clear();
+ return;
+ }
+ }
+
+ // Otherwise, revert to the special key event containing
+ // the actual characters.
+ KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(),
+ content.toString(), KeyCharacterMap.VIRTUAL_KEYBOARD, 0);
+ sendKeyEvent(event);
+ content.clear();
+ }
+ }
+
+ private void ensureDefaultComposingSpans() {
+ if (mDefaultComposingSpans == null) {
+ Context context;
+ if (mTargetView != null) {
+ context = mTargetView.getContext();
+ } else if (mIMM.mCurRootView != null) {
+ final View servedView = mIMM.mCurRootView.getImeFocusController().getServedView();
+ context = servedView != null ? servedView.getContext() : null;
+ } else {
+ context = null;
+ }
+ if (context != null) {
+ TypedArray ta = context.getTheme()
+ .obtainStyledAttributes(new int[] {
+ com.android.internal.R.attr.candidatesTextStyleSpans
+ });
+ CharSequence style = ta.getText(0);
+ ta.recycle();
+ if (style != null && style instanceof Spanned) {
+ mDefaultComposingSpans = ((Spanned)style).getSpans(
+ 0, style.length(), Object.class);
+ }
+ }
+ }
+ }
+
+ private void replaceText(CharSequence text, int newCursorPosition,
+ boolean composing) {
+ final Editable content = getEditable();
+ if (content == null) {
+ return;
+ }
+
+ beginBatchEdit();
+
+ // delete composing text set previously.
+ int a = getComposingSpanStart(content);
+ int b = getComposingSpanEnd(content);
+
+ if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b);
+
+ if (b < a) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ if (a != -1 && b != -1) {
+ removeComposingSpans(content);
+ } else {
+ a = Selection.getSelectionStart(content);
+ b = Selection.getSelectionEnd(content);
+ if (a < 0) a = 0;
+ if (b < 0) b = 0;
+ if (b < a) {
+ int tmp = a;
+ a = b;
+ b = tmp;
+ }
+ }
+
+ if (composing) {
+ Spannable sp = null;
+ if (!(text instanceof Spannable)) {
+ sp = new SpannableStringBuilder(text);
+ text = sp;
+ ensureDefaultComposingSpans();
+ if (mDefaultComposingSpans != null) {
+ for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
+ sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
+ }
+ }
+ } else {
+ sp = (Spannable)text;
+ }
+ setComposingSpans(sp);
+ }
+
+ if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \""
+ + text + "\", composing=" + composing
+ + ", type=" + text.getClass().getCanonicalName());
+
+ if (DEBUG) {
+ LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
+ lp.println("Current text:");
+ TextUtils.dumpSpans(content, lp, " ");
+ lp.println("Composing text:");
+ TextUtils.dumpSpans(text, lp, " ");
+ }
+
+ // Position the cursor appropriately, so that after replacing the
+ // desired range of text it will be located in the correct spot.
+ // This allows us to deal with filters performing edits on the text
+ // we are providing here.
+ if (newCursorPosition > 0) {
+ newCursorPosition += b - 1;
+ } else {
+ newCursorPosition += a;
+ }
+ if (newCursorPosition < 0) newCursorPosition = 0;
+ if (newCursorPosition > content.length())
+ newCursorPosition = content.length();
+ Selection.setSelection(content, newCursorPosition);
+
+ content.replace(a, b, text);
+
+ if (DEBUG) {
+ LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
+ lp.println("Final text:");
+ TextUtils.dumpSpans(content, lp, " ");
+ }
+
+ endBatchEdit();
+ }
+
+ /**
+ * Default implementation which invokes {@link View#performReceiveContent} on the target
+ * view if the view {@link View#getReceiveContentMimeTypes allows} content insertion;
+ * otherwise returns false without any side effects.
+ */
+ public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
+ ClipDescription description = inputContentInfo.getDescription();
+ if (mTargetView.getReceiveContentMimeTypes() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Can't insert content from IME: content=" + description);
+ }
+ return false;
+ }
+ if ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+ try {
+ inputContentInfo.requestPermission();
+ } catch (Exception e) {
+ Log.w(TAG, "Can't insert content from IME; requestPermission() failed", e);
+ return false;
+ }
+ }
+ final ClipData clip = new ClipData(inputContentInfo.getDescription(),
+ new ClipData.Item(inputContentInfo.getContentUri()));
+ final ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_INPUT_METHOD)
+ .setLinkUri(inputContentInfo.getLinkUri())
+ .setExtras(opts)
+ .setInputContentInfo(inputContentInfo)
+ .build();
+ return mTargetView.performReceiveContent(payload) == null;
+ }
+}
diff --git a/android/view/inputmethod/CompletionInfo.java b/android/view/inputmethod/CompletionInfo.java
new file mode 100644
index 0000000..eee8a62
--- /dev/null
+++ b/android/view/inputmethod/CompletionInfo.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2007-2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Information about a single text completion that an editor has reported to
+ * an input method.
+ *
+ * <p>This class encapsulates a completion offered by an application
+ * that wants it to be presented to the user by the IME. Usually, apps
+ * present their completions directly in a scrolling list for example
+ * (UI developers will usually use or extend
+ * {@see android.widget.AutoCompleteTextView} to implement this).
+ * However, in some cases, the editor may not be visible, as in the
+ * case in extract mode where the IME has taken over the full
+ * screen. In this case, the editor can choose to send their
+ * completions to the IME for display.
+ *
+ * <p>Most applications who want to send completions to an IME should use
+ * {@link android.widget.AutoCompleteTextView} as this class makes this
+ * process easy. In this case, the application would not have to deal directly
+ * with this class.
+ *
+ * <p>An application who implements its own editor and wants direct control
+ * over this would create an array of CompletionInfo objects, and send it to the IME using
+ * {@link InputMethodManager#displayCompletions(View, CompletionInfo[])}.
+ * The IME would present the completions however they see fit, and
+ * call back to the editor through
+ * {@link InputConnection#commitCompletion(CompletionInfo)}.
+ * The application can then pick up the commit event by overriding
+ * {@link android.widget.TextView#onCommitCompletion(CompletionInfo)}.
+ */
+public final class CompletionInfo implements Parcelable {
+ private final long mId;
+ private final int mPosition;
+ private final CharSequence mText;
+ private final CharSequence mLabel;
+
+ /**
+ * Create a simple completion with just text, no label.
+ *
+ * @param id An id that get passed as is (to the editor's discretion)
+ * @param index An index that get passed as is. Typically this is the
+ * index in the list of completions inside the editor.
+ * @param text The text that should be inserted into the editor when
+ * this completion is chosen.
+ */
+ public CompletionInfo(long id, int index, CharSequence text) {
+ mId = id;
+ mPosition = index;
+ mText = text;
+ mLabel = null;
+ }
+
+ /**
+ * Create a full completion with both text and label. The text is
+ * what will get inserted into the editor, while the label is what
+ * the IME should display. If they are the same, use the version
+ * of the constructor without a `label' argument.
+ *
+ * @param id An id that get passed as is (to the editor's discretion)
+ * @param index An index that get passed as is. Typically this is the
+ * index in the list of completions inside the editor.
+ * @param text The text that should be inserted into the editor when
+ * this completion is chosen.
+ * @param label The text that the IME should be showing among the
+ * completions list.
+ */
+ public CompletionInfo(long id, int index, CharSequence text, CharSequence label) {
+ mId = id;
+ mPosition = index;
+ mText = text;
+ mLabel = label;
+ }
+
+ private CompletionInfo(Parcel source) {
+ mId = source.readLong();
+ mPosition = source.readInt();
+ mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ }
+
+ /**
+ * Return the abstract identifier for this completion, typically
+ * corresponding to the id associated with it in the original adapter.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Return the original position of this completion, typically
+ * corresponding to its position in the original adapter.
+ */
+ public int getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Return the actual text associated with this completion. This is the
+ * real text that will be inserted into the editor if the user selects it.
+ */
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Return the user-visible label for the completion, or null if the plain
+ * text should be shown. If non-null, this will be what the user sees as
+ * the completion option instead of the actual text.
+ */
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ @Override
+ public String toString() {
+ return "CompletionInfo{#" + mPosition + " \"" + mText
+ + "\" id=" + mId + " label=" + mLabel + "}";
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mId);
+ dest.writeInt(mPosition);
+ TextUtils.writeToParcel(mText, dest, flags);
+ TextUtils.writeToParcel(mLabel, dest, flags);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<CompletionInfo> CREATOR
+ = new Parcelable.Creator<CompletionInfo>() {
+ public CompletionInfo createFromParcel(Parcel source) {
+ return new CompletionInfo(source);
+ }
+
+ public CompletionInfo[] newArray(int size) {
+ return new CompletionInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/CorrectionInfo.java b/android/view/inputmethod/CorrectionInfo.java
new file mode 100644
index 0000000..6db5784
--- /dev/null
+++ b/android/view/inputmethod/CorrectionInfo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007-2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Information about a single text correction that an editor has reported to
+ * an input method.
+ */
+public final class CorrectionInfo implements Parcelable {
+ private final int mOffset;
+ private final CharSequence mOldText;
+ private final CharSequence mNewText;
+
+ /**
+ * @param offset The offset in the edited text where the old and new text start.
+ * @param oldText The old text that has been replaced.
+ * @param newText The replacement text.
+ */
+ public CorrectionInfo(int offset, CharSequence oldText, CharSequence newText) {
+ mOffset = offset;
+ mOldText = oldText;
+ mNewText = newText;
+ }
+
+ private CorrectionInfo(Parcel source) {
+ mOffset = source.readInt();
+ mOldText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mNewText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ }
+
+ /**
+ * Return the offset position of this correction in the text. Both the {@link #getOldText()} and
+ * {@link #getNewText()} start at this offset.
+ */
+ public int getOffset() {
+ return mOffset;
+ }
+
+ /**
+ * Return the text that has actually been typed by the user, and which has been corrected.
+ */
+ public CharSequence getOldText() {
+ return mOldText;
+ }
+
+ /**
+ * Return the new text that corrects what was typed by the user.
+ */
+ public CharSequence getNewText() {
+ return mNewText;
+ }
+
+ @Override
+ public String toString() {
+ return "CorrectionInfo{#" + mOffset + " \"" + mOldText + "\" -> \"" + mNewText + "\"}";
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mOffset);
+ TextUtils.writeToParcel(mOldText, dest, flags);
+ TextUtils.writeToParcel(mNewText, dest, flags);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<CorrectionInfo> CREATOR =
+ new Parcelable.Creator<CorrectionInfo>() {
+ public CorrectionInfo createFromParcel(Parcel source) {
+ return new CorrectionInfo(source);
+ }
+ public CorrectionInfo[] newArray(int size) {
+ return new CorrectionInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/CursorAnchorInfo.java b/android/view/inputmethod/CursorAnchorInfo.java
new file mode 100644
index 0000000..fbc9470
--- /dev/null
+++ b/android/view/inputmethod/CursorAnchorInfo.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Layout;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Positional information about the text insertion point and characters in the composition string.
+ *
+ * <p>This class encapsulates locations of the text insertion point and the composition string in
+ * the screen coordinates so that IMEs can render their UI components near where the text is
+ * actually inserted.</p>
+ */
+public final class CursorAnchorInfo implements Parcelable {
+ /**
+ * The pre-computed hash code.
+ */
+ private final int mHashCode;
+
+ /**
+ * The index of the first character of the selected text (inclusive). {@code -1} when there is
+ * no text selection.
+ */
+ private final int mSelectionStart;
+ /**
+ * The index of the first character of the selected text (exclusive). {@code -1} when there is
+ * no text selection.
+ */
+ private final int mSelectionEnd;
+
+ /**
+ * The index of the first character of the composing text (inclusive). {@code -1} when there is
+ * no composing text.
+ */
+ private final int mComposingTextStart;
+ /**
+ * The text, tracked as a composing region.
+ */
+ private final CharSequence mComposingText;
+
+ /**
+ * Flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for example.
+ */
+ private final int mInsertionMarkerFlags;
+ /**
+ * Horizontal position of the insertion marker, in the local coordinates that will be
+ * transformed with the transformation matrix when rendered on the screen. This should be
+ * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be
+ * {@code java.lang.Float.NaN} when no value is specified.
+ */
+ private final float mInsertionMarkerHorizontal;
+ /**
+ * Vertical position of the insertion marker, in the local coordinates that will be
+ * transformed with the transformation matrix when rendered on the screen. This should be
+ * calculated or compatible with {@link Layout#getLineTop(int)}. This can be
+ * {@code java.lang.Float.NaN} when no value is specified.
+ */
+ private final float mInsertionMarkerTop;
+ /**
+ * Vertical position of the insertion marker, in the local coordinates that will be
+ * transformed with the transformation matrix when rendered on the screen. This should be
+ * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be
+ * {@code java.lang.Float.NaN} when no value is specified.
+ */
+ private final float mInsertionMarkerBaseline;
+ /**
+ * Vertical position of the insertion marker, in the local coordinates that will be
+ * transformed with the transformation matrix when rendered on the screen. This should be
+ * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be
+ * {@code java.lang.Float.NaN} when no value is specified.
+ */
+ private final float mInsertionMarkerBottom;
+
+ /**
+ * Container of rectangular position of characters, keyed with character index in a unit of
+ * Java chars, in the local coordinates that will be transformed with the transformation matrix
+ * when rendered on the screen.
+ */
+ private final SparseRectFArray mCharacterBoundsArray;
+
+ /**
+ * Transformation matrix that is applied to any positional information of this class to
+ * transform local coordinates into screen coordinates.
+ */
+ @NonNull
+ private final float[] mMatrixValues;
+
+ /**
+ * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
+ * insertion marker or character bounds have at least one visible region.
+ */
+ public static final int FLAG_HAS_VISIBLE_REGION = 0x01;
+
+ /**
+ * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
+ * insertion marker or character bounds have at least one invisible (clipped) region.
+ */
+ public static final int FLAG_HAS_INVISIBLE_REGION = 0x02;
+
+ /**
+ * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
+ * insertion marker or character bounds is placed at right-to-left (RTL) character.
+ */
+ public static final int FLAG_IS_RTL = 0x04;
+
+ public CursorAnchorInfo(final Parcel source) {
+ mHashCode = source.readInt();
+ mSelectionStart = source.readInt();
+ mSelectionEnd = source.readInt();
+ mComposingTextStart = source.readInt();
+ mComposingText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mInsertionMarkerFlags = source.readInt();
+ mInsertionMarkerHorizontal = source.readFloat();
+ mInsertionMarkerTop = source.readFloat();
+ mInsertionMarkerBaseline = source.readFloat();
+ mInsertionMarkerBottom = source.readFloat();
+ mCharacterBoundsArray = source.readParcelable(SparseRectFArray.class.getClassLoader());
+ mMatrixValues = source.createFloatArray();
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mHashCode);
+ dest.writeInt(mSelectionStart);
+ dest.writeInt(mSelectionEnd);
+ dest.writeInt(mComposingTextStart);
+ TextUtils.writeToParcel(mComposingText, dest, flags);
+ dest.writeInt(mInsertionMarkerFlags);
+ dest.writeFloat(mInsertionMarkerHorizontal);
+ dest.writeFloat(mInsertionMarkerTop);
+ dest.writeFloat(mInsertionMarkerBaseline);
+ dest.writeFloat(mInsertionMarkerBottom);
+ dest.writeParcelable(mCharacterBoundsArray, flags);
+ dest.writeFloatArray(mMatrixValues);
+ }
+
+ @Override
+ public int hashCode(){
+ return mHashCode;
+ }
+
+ /**
+ * Compares two float values. Returns {@code true} if {@code a} and {@code b} are
+ * {@link Float#NaN} at the same time.
+ */
+ private static boolean areSameFloatImpl(final float a, final float b) {
+ if (Float.isNaN(a) && Float.isNaN(b)) {
+ return true;
+ }
+ return a == b;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj){
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof CursorAnchorInfo)) {
+ return false;
+ }
+ final CursorAnchorInfo that = (CursorAnchorInfo) obj;
+ if (hashCode() != that.hashCode()) {
+ return false;
+ }
+
+ // Check fields that are not covered by hashCode() first.
+
+ if (mSelectionStart != that.mSelectionStart || mSelectionEnd != that.mSelectionEnd) {
+ return false;
+ }
+
+ if (mInsertionMarkerFlags != that.mInsertionMarkerFlags
+ || !areSameFloatImpl(mInsertionMarkerHorizontal, that.mInsertionMarkerHorizontal)
+ || !areSameFloatImpl(mInsertionMarkerTop, that.mInsertionMarkerTop)
+ || !areSameFloatImpl(mInsertionMarkerBaseline, that.mInsertionMarkerBaseline)
+ || !areSameFloatImpl(mInsertionMarkerBottom, that.mInsertionMarkerBottom)) {
+ return false;
+ }
+
+ if (!Objects.equals(mCharacterBoundsArray, that.mCharacterBoundsArray)) {
+ return false;
+ }
+
+ // Following fields are (partially) covered by hashCode().
+
+ if (mComposingTextStart != that.mComposingTextStart
+ || !Objects.equals(mComposingText, that.mComposingText)) {
+ return false;
+ }
+
+ // We do not use Arrays.equals(float[], float[]) to keep the previous behavior regarding
+ // NaN, 0.0f, and -0.0f.
+ if (mMatrixValues.length != that.mMatrixValues.length) {
+ return false;
+ }
+ for (int i = 0; i < mMatrixValues.length; ++i) {
+ if (mMatrixValues[i] != that.mMatrixValues[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "CursorAnchorInfo{mHashCode=" + mHashCode
+ + " mSelection=" + mSelectionStart + "," + mSelectionEnd
+ + " mComposingTextStart=" + mComposingTextStart
+ + " mComposingText=" + Objects.toString(mComposingText)
+ + " mInsertionMarkerFlags=" + mInsertionMarkerFlags
+ + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal
+ + " mInsertionMarkerTop=" + mInsertionMarkerTop
+ + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline
+ + " mInsertionMarkerBottom=" + mInsertionMarkerBottom
+ + " mCharacterBoundsArray=" + Objects.toString(mCharacterBoundsArray)
+ + " mMatrix=" + Arrays.toString(mMatrixValues)
+ + "}";
+ }
+
+ /**
+ * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe.
+ */
+ public static final class Builder {
+ private int mSelectionStart = -1;
+ private int mSelectionEnd = -1;
+ private int mComposingTextStart = -1;
+ private CharSequence mComposingText = null;
+ private float mInsertionMarkerHorizontal = Float.NaN;
+ private float mInsertionMarkerTop = Float.NaN;
+ private float mInsertionMarkerBaseline = Float.NaN;
+ private float mInsertionMarkerBottom = Float.NaN;
+ private int mInsertionMarkerFlags = 0;
+ private SparseRectFArrayBuilder mCharacterBoundsArrayBuilder = null;
+ private float[] mMatrixValues = null;
+ private boolean mMatrixInitialized = false;
+
+ /**
+ * Sets the text range of the selection. Calling this can be skipped if there is no
+ * selection.
+ */
+ public Builder setSelectionRange(final int newStart, final int newEnd) {
+ mSelectionStart = newStart;
+ mSelectionEnd = newEnd;
+ return this;
+ }
+
+ /**
+ * Sets the text range of the composing text. Calling this can be skipped if there is
+ * no composing text.
+ * @param composingTextStart index where the composing text starts.
+ * @param composingText the entire composing text.
+ */
+ public Builder setComposingText(final int composingTextStart,
+ final CharSequence composingText) {
+ mComposingTextStart = composingTextStart;
+ if (composingText == null) {
+ mComposingText = null;
+ } else {
+ // Make a snapshot of the given char sequence.
+ mComposingText = new SpannedString(composingText);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the location of the text insertion point (zero width cursor) as a rectangle in
+ * local coordinates. Calling this can be skipped when there is no text insertion point;
+ * however if there is an insertion point, editors must call this method.
+ * @param horizontalPosition horizontal position of the insertion marker, in the local
+ * coordinates that will be transformed with the transformation matrix when rendered on the
+ * screen. This should be calculated or compatible with
+ * {@link Layout#getPrimaryHorizontal(int)}.
+ * @param lineTop vertical position of the insertion marker, in the local coordinates that
+ * will be transformed with the transformation matrix when rendered on the screen. This
+ * should be calculated or compatible with {@link Layout#getLineTop(int)}.
+ * @param lineBaseline vertical position of the insertion marker, in the local coordinates
+ * that will be transformed with the transformation matrix when rendered on the screen. This
+ * should be calculated or compatible with {@link Layout#getLineBaseline(int)}.
+ * @param lineBottom vertical position of the insertion marker, in the local coordinates
+ * that will be transformed with the transformation matrix when rendered on the screen. This
+ * should be calculated or compatible with {@link Layout#getLineBottom(int)}.
+ * @param flags flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for
+ * example.
+ */
+ public Builder setInsertionMarkerLocation(final float horizontalPosition,
+ final float lineTop, final float lineBaseline, final float lineBottom,
+ final int flags){
+ mInsertionMarkerHorizontal = horizontalPosition;
+ mInsertionMarkerTop = lineTop;
+ mInsertionMarkerBaseline = lineBaseline;
+ mInsertionMarkerBottom = lineBottom;
+ mInsertionMarkerFlags = flags;
+ return this;
+ }
+
+ /**
+ * Adds the bounding box of the character specified with the index.
+ *
+ * @param index index of the character in Java chars units. Must be specified in
+ * ascending order across successive calls.
+ * @param left x coordinate of the left edge of the character in local coordinates.
+ * @param top y coordinate of the top edge of the character in local coordinates.
+ * @param right x coordinate of the right edge of the character in local coordinates.
+ * @param bottom y coordinate of the bottom edge of the character in local coordinates.
+ * @param flags flags for this character bounds. See {@link #FLAG_HAS_VISIBLE_REGION},
+ * {@link #FLAG_HAS_INVISIBLE_REGION} and {@link #FLAG_IS_RTL}. These flags must be
+ * specified when necessary.
+ * @throws IllegalArgumentException If the index is a negative value, or not greater than
+ * all of the previously called indices.
+ */
+ public Builder addCharacterBounds(final int index, final float left, final float top,
+ final float right, final float bottom, final int flags) {
+ if (index < 0) {
+ throw new IllegalArgumentException("index must not be a negative integer.");
+ }
+ if (mCharacterBoundsArrayBuilder == null) {
+ mCharacterBoundsArrayBuilder = new SparseRectFArrayBuilder();
+ }
+ mCharacterBoundsArrayBuilder.append(index, left, top, right, bottom, flags);
+ return this;
+ }
+
+ /**
+ * Sets the matrix that transforms local coordinates into screen coordinates.
+ * @param matrix transformation matrix from local coordinates into screen coordinates. null
+ * is interpreted as an identity matrix.
+ */
+ public Builder setMatrix(final Matrix matrix) {
+ if (mMatrixValues == null) {
+ mMatrixValues = new float[9];
+ }
+ (matrix != null ? matrix : Matrix.IDENTITY_MATRIX).getValues(mMatrixValues);
+ mMatrixInitialized = true;
+ return this;
+ }
+
+ /**
+ * @return {@link CursorAnchorInfo} using parameters in this {@link Builder}.
+ * @throws IllegalArgumentException if one or more positional parameters are specified but
+ * the coordinate transformation matrix is not provided via {@link #setMatrix(Matrix)}.
+ */
+ public CursorAnchorInfo build() {
+ if (!mMatrixInitialized) {
+ // Coordinate transformation matrix is mandatory when at least one positional
+ // parameter is specified.
+ final boolean hasCharacterBounds = (mCharacterBoundsArrayBuilder != null
+ && !mCharacterBoundsArrayBuilder.isEmpty());
+ if (hasCharacterBounds
+ || !Float.isNaN(mInsertionMarkerHorizontal)
+ || !Float.isNaN(mInsertionMarkerTop)
+ || !Float.isNaN(mInsertionMarkerBaseline)
+ || !Float.isNaN(mInsertionMarkerBottom)) {
+ throw new IllegalArgumentException("Coordinate transformation matrix is " +
+ "required when positional parameters are specified.");
+ }
+ }
+ return new CursorAnchorInfo(this);
+ }
+
+ /**
+ * Resets the internal state so that this instance can be reused to build another
+ * instance of {@link CursorAnchorInfo}.
+ */
+ public void reset() {
+ mSelectionStart = -1;
+ mSelectionEnd = -1;
+ mComposingTextStart = -1;
+ mComposingText = null;
+ mInsertionMarkerFlags = 0;
+ mInsertionMarkerHorizontal = Float.NaN;
+ mInsertionMarkerTop = Float.NaN;
+ mInsertionMarkerBaseline = Float.NaN;
+ mInsertionMarkerBottom = Float.NaN;
+ mMatrixInitialized = false;
+ if (mCharacterBoundsArrayBuilder != null) {
+ mCharacterBoundsArrayBuilder.reset();
+ }
+ }
+ }
+
+ private CursorAnchorInfo(final Builder builder) {
+ mSelectionStart = builder.mSelectionStart;
+ mSelectionEnd = builder.mSelectionEnd;
+ mComposingTextStart = builder.mComposingTextStart;
+ mComposingText = builder.mComposingText;
+ mInsertionMarkerFlags = builder.mInsertionMarkerFlags;
+ mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal;
+ mInsertionMarkerTop = builder.mInsertionMarkerTop;
+ mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline;
+ mInsertionMarkerBottom = builder.mInsertionMarkerBottom;
+ mCharacterBoundsArray = builder.mCharacterBoundsArrayBuilder != null
+ ? builder.mCharacterBoundsArrayBuilder.build() : null;
+ mMatrixValues = new float[9];
+ if (builder.mMatrixInitialized) {
+ System.arraycopy(builder.mMatrixValues, 0, mMatrixValues, 0, 9);
+ } else {
+ Matrix.IDENTITY_MATRIX.getValues(mMatrixValues);
+ }
+
+ // To keep hash function simple, we only use some complex objects for hash.
+ int hash = Objects.hashCode(mComposingText);
+ hash *= 31;
+ hash += Arrays.hashCode(mMatrixValues);
+ mHashCode = hash;
+ }
+
+ /**
+ * Returns the index where the selection starts.
+ * @return {@code -1} if there is no selection.
+ */
+ public int getSelectionStart() {
+ return mSelectionStart;
+ }
+
+ /**
+ * Returns the index where the selection ends.
+ * @return {@code -1} if there is no selection.
+ */
+ public int getSelectionEnd() {
+ return mSelectionEnd;
+ }
+
+ /**
+ * Returns the index where the composing text starts.
+ * @return {@code -1} if there is no composing text.
+ */
+ public int getComposingTextStart() {
+ return mComposingTextStart;
+ }
+
+ /**
+ * Returns the entire composing text.
+ * @return {@code null} if there is no composition.
+ */
+ public CharSequence getComposingText() {
+ return mComposingText;
+ }
+
+ /**
+ * Returns the flag of the insertion marker.
+ * @return the flag of the insertion marker. {@code 0} if no flag is specified.
+ */
+ public int getInsertionMarkerFlags() {
+ return mInsertionMarkerFlags;
+ }
+
+ /**
+ * Returns the horizontal start of the insertion marker, in the local coordinates that will
+ * be transformed with {@link #getMatrix()} when rendered on the screen.
+ * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}.
+ * Pay special care to RTL/LTR handling.
+ * {@code java.lang.Float.NaN} if not specified.
+ * @see Layout#getPrimaryHorizontal(int)
+ */
+ public float getInsertionMarkerHorizontal() {
+ return mInsertionMarkerHorizontal;
+ }
+
+ /**
+ * Returns the vertical top position of the insertion marker, in the local coordinates that
+ * will be transformed with {@link #getMatrix()} when rendered on the screen.
+ * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}.
+ * {@code java.lang.Float.NaN} if not specified.
+ */
+ public float getInsertionMarkerTop() {
+ return mInsertionMarkerTop;
+ }
+
+ /**
+ * Returns the vertical baseline position of the insertion marker, in the local coordinates
+ * that will be transformed with {@link #getMatrix()} when rendered on the screen.
+ * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}.
+ * {@code java.lang.Float.NaN} if not specified.
+ */
+ public float getInsertionMarkerBaseline() {
+ return mInsertionMarkerBaseline;
+ }
+
+ /**
+ * Returns the vertical bottom position of the insertion marker, in the local coordinates
+ * that will be transformed with {@link #getMatrix()} when rendered on the screen.
+ * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}.
+ * {@code java.lang.Float.NaN} if not specified.
+ */
+ public float getInsertionMarkerBottom() {
+ return mInsertionMarkerBottom;
+ }
+
+ /**
+ * Returns a new instance of {@link RectF} that indicates the location of the character
+ * specified with the index.
+ * @param index index of the character in a Java chars.
+ * @return the character bounds in local coordinates as a new instance of {@link RectF}.
+ */
+ public RectF getCharacterBounds(final int index) {
+ if (mCharacterBoundsArray == null) {
+ return null;
+ }
+ return mCharacterBoundsArray.get(index);
+ }
+
+ /**
+ * Returns the flags associated with the character bounds specified with the index.
+ * @param index index of the character in a Java chars.
+ * @return {@code 0} if no flag is specified.
+ */
+ public int getCharacterBoundsFlags(final int index) {
+ if (mCharacterBoundsArray == null) {
+ return 0;
+ }
+ return mCharacterBoundsArray.getFlags(index, 0);
+ }
+
+ /**
+ * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
+ * matrix that is to be applied other positional data in this class.
+ * @return a new instance (copy) of the transformation matrix.
+ */
+ public Matrix getMatrix() {
+ final Matrix matrix = new Matrix();
+ matrix.setValues(mMatrixValues);
+ return matrix;
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<CursorAnchorInfo> CREATOR
+ = new Parcelable.Creator<CursorAnchorInfo>() {
+ @Override
+ public CursorAnchorInfo createFromParcel(Parcel source) {
+ return new CursorAnchorInfo(source);
+ }
+
+ @Override
+ public CursorAnchorInfo[] newArray(int size) {
+ return new CursorAnchorInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/DumpableInputConnection.java b/android/view/inputmethod/DumpableInputConnection.java
new file mode 100644
index 0000000..9819a57
--- /dev/null
+++ b/android/view/inputmethod/DumpableInputConnection.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.util.proto.ProtoOutputStream;
+
+/** @hide */
+public interface DumpableInputConnection {
+
+ /**
+ * Method used to dump state of InputConnection implementations of interest.
+ *
+ * @param proto Stream to write the state to
+ * @param fieldId FieldId of DumpableInputConnection as defined in the parent message
+ */
+ void dumpDebug(@NonNull ProtoOutputStream proto, long fieldId);
+}
diff --git a/android/view/inputmethod/EditorInfo.java b/android/view/inputmethod/EditorInfo.java
new file mode 100644
index 0000000..4a52b1f
--- /dev/null
+++ b/android/view/inputmethod/EditorInfo.java
@@ -0,0 +1,1057 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.view.inputmethod.EditorInfoProto.FIELD_ID;
+import static android.view.inputmethod.EditorInfoProto.IME_OPTIONS;
+import static android.view.inputmethod.EditorInfoProto.INPUT_TYPE;
+import static android.view.inputmethod.EditorInfoProto.PACKAGE_NAME;
+import static android.view.inputmethod.EditorInfoProto.PRIVATE_IME_OPTIONS;
+import static android.view.inputmethod.EditorInfoProto.TARGET_INPUT_METHOD_USER_ID;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.res.Configuration;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.Printer;
+import android.util.proto.ProtoOutputStream;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * An EditorInfo describes several attributes of a text editing object
+ * that an input method is communicating with (typically an EditText), most
+ * importantly the type of text content it contains and the current cursor position.
+ */
+public class EditorInfo implements InputType, Parcelable {
+ /**
+ * Masks for {@link inputType}
+ *
+ * <pre>
+ * |-------|-------|-------|-------|
+ * 1111 TYPE_MASK_CLASS
+ * 11111111 TYPE_MASK_VARIATION
+ * 111111111111 TYPE_MASK_FLAGS
+ * |-------|-------|-------|-------|
+ * TYPE_NULL
+ * |-------|-------|-------|-------|
+ * 1 TYPE_CLASS_TEXT
+ * 1 TYPE_TEXT_VARIATION_URI
+ * 1 TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ * 11 TYPE_TEXT_VARIATION_EMAIL_SUBJECT
+ * 1 TYPE_TEXT_VARIATION_SHORT_MESSAGE
+ * 1 1 TYPE_TEXT_VARIATION_LONG_MESSAGE
+ * 11 TYPE_TEXT_VARIATION_PERSON_NAME
+ * 111 TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ * 1 TYPE_TEXT_VARIATION_PASSWORD
+ * 1 1 TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ * 1 1 TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+ * 1 11 TYPE_TEXT_VARIATION_FILTER
+ * 11 TYPE_TEXT_VARIATION_PHONETIC
+ * 11 1 TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
+ * 111 TYPE_TEXT_VARIATION_WEB_PASSWORD
+ * 1 TYPE_TEXT_FLAG_CAP_CHARACTERS
+ * 1 TYPE_TEXT_FLAG_CAP_WORDS
+ * 1 TYPE_TEXT_FLAG_CAP_SENTENCES
+ * 1 TYPE_TEXT_FLAG_AUTO_CORRECT
+ * 1 TYPE_TEXT_FLAG_AUTO_COMPLETE
+ * 1 TYPE_TEXT_FLAG_MULTI_LINE
+ * 1 TYPE_TEXT_FLAG_IME_MULTI_LINE
+ * 1 TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ * |-------|-------|-------|-------|
+ * 1 TYPE_CLASS_NUMBER
+ * 1 TYPE_NUMBER_VARIATION_PASSWORD
+ * 1 TYPE_NUMBER_FLAG_SIGNED
+ * 1 TYPE_NUMBER_FLAG_DECIMAL
+ * |-------|-------|-------|-------|
+ * 11 TYPE_CLASS_PHONE
+ * |-------|-------|-------|-------|
+ * 1 TYPE_CLASS_DATETIME
+ * 1 TYPE_DATETIME_VARIATION_DATE
+ * 1 TYPE_DATETIME_VARIATION_TIME
+ * |-------|-------|-------|-------|</pre>
+ */
+
+ /**
+ * The content type of the text box, whose bits are defined by
+ * {@link InputType}.
+ *
+ * @see InputType
+ * @see #TYPE_MASK_CLASS
+ * @see #TYPE_MASK_VARIATION
+ * @see #TYPE_MASK_FLAGS
+ */
+ public int inputType = TYPE_NULL;
+
+ /**
+ * Set of bits in {@link #imeOptions} that provide alternative actions
+ * associated with the "enter" key. This both helps the IME provide
+ * better feedback about what the enter key will do, and also allows it
+ * to provide alternative mechanisms for providing that command.
+ */
+ public static final int IME_MASK_ACTION = 0x000000ff;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: no specific action has been
+ * associated with this editor, let the editor come up with its own if
+ * it can.
+ */
+ public static final int IME_ACTION_UNSPECIFIED = 0x00000000;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: there is no available action.
+ */
+ public static final int IME_ACTION_NONE = 0x00000001;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: the action key performs a "go"
+ * operation to take the user to the target of the text they typed.
+ * Typically used, for example, when entering a URL.
+ */
+ public static final int IME_ACTION_GO = 0x00000002;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: the action key performs a "search"
+ * operation, taking the user to the results of searching for the text
+ * they have typed (in whatever context is appropriate).
+ */
+ public static final int IME_ACTION_SEARCH = 0x00000003;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: the action key performs a "send"
+ * operation, delivering the text to its target. This is typically used
+ * when composing a message in IM or SMS where sending is immediate.
+ */
+ public static final int IME_ACTION_SEND = 0x00000004;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: the action key performs a "next"
+ * operation, taking the user to the next field that will accept text.
+ */
+ public static final int IME_ACTION_NEXT = 0x00000005;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: the action key performs a "done"
+ * operation, typically meaning there is nothing more to input and the
+ * IME will be closed.
+ */
+ public static final int IME_ACTION_DONE = 0x00000006;
+
+ /**
+ * Bits of {@link #IME_MASK_ACTION}: like {@link #IME_ACTION_NEXT}, but
+ * for moving to the previous field. This will normally not be used to
+ * specify an action (since it precludes {@link #IME_ACTION_NEXT}), but
+ * can be returned to the app if it sets {@link #IME_FLAG_NAVIGATE_PREVIOUS}.
+ */
+ public static final int IME_ACTION_PREVIOUS = 0x00000007;
+
+ /**
+ * Flag of {@link #imeOptions}: used to request that the IME should not update any personalized
+ * data such as typing history and personalized language model based on what the user typed on
+ * this text editing object. Typical use cases are:
+ * <ul>
+ * <li>When the application is in a special mode, where user's activities are expected to be
+ * not recorded in the application's history. Some web browsers and chat applications may
+ * have this kind of modes.</li>
+ * <li>When storing typing history does not make much sense. Specifying this flag in typing
+ * games may help to avoid typing history from being filled up with words that the user is
+ * less likely to type in their daily life. Another example is that when the application
+ * already knows that the expected input is not a valid word (e.g. a promotion code that is
+ * not a valid word in any natural language).</li>
+ * </ul>
+ *
+ * <p>Applications need to be aware that the flag is not a guarantee, and some IMEs may not
+ * respect it.</p>
+ */
+ public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000;
+
+ /**
+ * Flag of {@link #imeOptions}: used to request that the IME never go
+ * into fullscreen mode.
+ * By default, IMEs may go into full screen mode when they think
+ * it's appropriate, for example on small screens in landscape
+ * orientation where displaying a software keyboard may occlude
+ * such a large portion of the screen that the remaining part is
+ * too small to meaningfully display the application UI.
+ * If this flag is set, compliant IMEs will never go into full screen mode,
+ * and always leave some space to display the application UI.
+ * Applications need to be aware that the flag is not a guarantee, and
+ * some IMEs may ignore it.
+ */
+ public static final int IME_FLAG_NO_FULLSCREEN = 0x2000000;
+
+ /**
+ * Flag of {@link #imeOptions}: like {@link #IME_FLAG_NAVIGATE_NEXT}, but
+ * specifies there is something interesting that a backward navigation
+ * can focus on. If the user selects the IME's facility to backward
+ * navigate, this will show up in the application as an {@link #IME_ACTION_PREVIOUS}
+ * at {@link InputConnection#performEditorAction(int)
+ * InputConnection.performEditorAction(int)}.
+ */
+ public static final int IME_FLAG_NAVIGATE_PREVIOUS = 0x4000000;
+
+ /**
+ * Flag of {@link #imeOptions}: used to specify that there is something
+ * interesting that a forward navigation can focus on. This is like using
+ * {@link #IME_ACTION_NEXT}, except allows the IME to be multiline (with
+ * an enter key) as well as provide forward navigation. Note that some
+ * IMEs may not be able to do this, especially when running on a small
+ * screen where there is little space. In that case it does not need to
+ * present a UI for this option. Like {@link #IME_ACTION_NEXT}, if the
+ * user selects the IME's facility to forward navigate, this will show up
+ * in the application at {@link InputConnection#performEditorAction(int)
+ * InputConnection.performEditorAction(int)}.
+ */
+ public static final int IME_FLAG_NAVIGATE_NEXT = 0x8000000;
+
+ /**
+ * Flag of {@link #imeOptions}: used to specify that the IME does not need
+ * to show its extracted text UI. For input methods that may be fullscreen,
+ * often when in landscape mode, this allows them to be smaller and let part
+ * of the application be shown behind, through transparent UI parts in the
+ * fullscreen IME. The part of the UI visible to the user may not be responsive
+ * to touch because the IME will receive touch events, which may confuse the
+ * user; use {@link #IME_FLAG_NO_FULLSCREEN} instead for a better experience.
+ * Using this flag is discouraged and it may become deprecated in the future.
+ * Its meaning is unclear in some situations and it may not work appropriately
+ * on older versions of the platform.
+ */
+ public static final int IME_FLAG_NO_EXTRACT_UI = 0x10000000;
+
+ /**
+ * Flag of {@link #imeOptions}: used in conjunction with one of the actions
+ * masked by {@link #IME_MASK_ACTION}, this indicates that the action
+ * should not be available as an accessory button on the right of the extracted
+ * text when the input method is full-screen. Note that by setting this flag,
+ * there can be cases where the action is simply never available to the
+ * user. Setting this generally means that you think that in fullscreen mode,
+ * where there is little space to show the text, it's not worth taking some
+ * screen real estate to display the action and it should be used instead
+ * to show more text.
+ */
+ public static final int IME_FLAG_NO_ACCESSORY_ACTION = 0x20000000;
+
+ /**
+ * Flag of {@link #imeOptions}: used in conjunction with one of the actions
+ * masked by {@link #IME_MASK_ACTION}. If this flag is not set, IMEs will
+ * normally replace the "enter" key with the action supplied. This flag
+ * indicates that the action should not be available in-line as a replacement
+ * for the "enter" key. Typically this is because the action has such a
+ * significant impact or is not recoverable enough that accidentally hitting
+ * it should be avoided, such as sending a message. Note that
+ * {@link android.widget.TextView} will automatically set this flag for you
+ * on multi-line text views.
+ */
+ public static final int IME_FLAG_NO_ENTER_ACTION = 0x40000000;
+
+ /**
+ * Flag of {@link #imeOptions}: used to request an IME that is capable of
+ * inputting ASCII characters. The intention of this flag is to ensure that
+ * the user can type Roman alphabet characters in a {@link android.widget.TextView}.
+ * It is typically used for an account ID or password input. A lot of the time,
+ * IMEs are already able to input ASCII even without being told so (such IMEs
+ * already respect this flag in a sense), but there are cases when this is not
+ * the default. For instance, users of languages using a different script like
+ * Arabic, Greek, Hebrew or Russian typically have a keyboard that can't
+ * input ASCII characters by default. Applications need to be
+ * aware that the flag is not a guarantee, and some IMEs may not respect it.
+ * However, it is strongly recommended for IME authors to respect this flag
+ * especially when their IME could end up with a state where only languages
+ * using non-ASCII are enabled.
+ */
+ public static final int IME_FLAG_FORCE_ASCII = 0x80000000;
+
+ /**
+ * Flag of {@link #internalImeOptions}: flag is set when app window containing this
+ * {@link EditorInfo} is using {@link Configuration#ORIENTATION_PORTRAIT} mode.
+ * @hide
+ */
+ public static final int IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT = 0x00000001;
+
+ /**
+ * Generic unspecified type for {@link #imeOptions}.
+ */
+ public static final int IME_NULL = 0x00000000;
+
+ /**
+ * Masks for {@link imeOptions}
+ *
+ * <pre>
+ * |-------|-------|-------|-------|
+ * 1111 IME_MASK_ACTION
+ * |-------|-------|-------|-------|
+ * IME_ACTION_UNSPECIFIED
+ * 1 IME_ACTION_NONE
+ * 1 IME_ACTION_GO
+ * 11 IME_ACTION_SEARCH
+ * 1 IME_ACTION_SEND
+ * 1 1 IME_ACTION_NEXT
+ * 11 IME_ACTION_DONE
+ * 111 IME_ACTION_PREVIOUS
+ * 1 IME_FLAG_NO_PERSONALIZED_LEARNING
+ * 1 IME_FLAG_NO_FULLSCREEN
+ * 1 IME_FLAG_NAVIGATE_PREVIOUS
+ * 1 IME_FLAG_NAVIGATE_NEXT
+ * 1 IME_FLAG_NO_EXTRACT_UI
+ * 1 IME_FLAG_NO_ACCESSORY_ACTION
+ * 1 IME_FLAG_NO_ENTER_ACTION
+ * 1 IME_FLAG_FORCE_ASCII
+ * |-------|-------|-------|-------|</pre>
+ */
+
+ /**
+ * Extended type information for the editor, to help the IME better
+ * integrate with it.
+ */
+ public int imeOptions = IME_NULL;
+
+ /**
+ * A string supplying additional information options that are
+ * private to a particular IME implementation. The string must be
+ * scoped to a package owned by the implementation, to ensure there are
+ * no conflicts between implementations, but other than that you can put
+ * whatever you want in it to communicate with the IME. For example,
+ * you could have a string that supplies an argument like
+ * <code>"com.example.myapp.SpecialMode=3"</code>. This field is can be
+ * filled in from the {@link android.R.attr#privateImeOptions}
+ * attribute of a TextView.
+ */
+ public String privateImeOptions = null;
+
+ /**
+ * Masks for {@link internalImeOptions}
+ *
+ * <pre>
+ * 1 IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT
+ * |-------|-------|-------|-------|</pre>
+ */
+
+ /**
+ * Same as {@link android.R.attr#imeOptions} but for framework's internal-use only.
+ * @hide
+ */
+ public int internalImeOptions = IME_NULL;
+
+ /**
+ * In some cases an IME may be able to display an arbitrary label for
+ * a command the user can perform, which you can specify here. This is
+ * typically used as the label for the action to use in-line as a replacement
+ * for the "enter" key (see {@link #actionId}). Remember the key where
+ * this will be displayed is typically very small, and there are significant
+ * localization challenges to make this fit in all supported languages. Also
+ * you can not count absolutely on this being used, as some IMEs may
+ * ignore this.
+ */
+ public CharSequence actionLabel = null;
+
+ /**
+ * If {@link #actionLabel} has been given, this is the id for that command
+ * when the user presses its button that is delivered back with
+ * {@link InputConnection#performEditorAction(int)
+ * InputConnection.performEditorAction()}.
+ */
+ public int actionId = 0;
+
+ /**
+ * The text offset of the start of the selection at the time editing
+ * begins; -1 if not known. Keep in mind that, without knowing the cursor
+ * position, many IMEs will not be able to offer their full feature set and
+ * may even behave in unpredictable ways: pass the actual cursor position
+ * here if possible at all.
+ *
+ * <p>Also, this needs to be the cursor position <strong>right now</strong>,
+ * not at some point in the past, even if input is starting in the same text field
+ * as before. When the app is filling this object, input is about to start by
+ * definition, and this value will override any value the app may have passed to
+ * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)}
+ * before.</p>
+ */
+ public int initialSelStart = -1;
+
+ /**
+ * <p>The text offset of the end of the selection at the time editing
+ * begins; -1 if not known. Keep in mind that, without knowing the cursor
+ * position, many IMEs will not be able to offer their full feature set and
+ * may behave in unpredictable ways: pass the actual cursor position
+ * here if possible at all.</p>
+ *
+ * <p>Also, this needs to be the cursor position <strong>right now</strong>,
+ * not at some point in the past, even if input is starting in the same text field
+ * as before. When the app is filling this object, input is about to start by
+ * definition, and this value will override any value the app may have passed to
+ * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)}
+ * before.</p>
+ */
+ public int initialSelEnd = -1;
+
+ /**
+ * The capitalization mode of the first character being edited in the
+ * text. Values may be any combination of
+ * {@link TextUtils#CAP_MODE_CHARACTERS TextUtils.CAP_MODE_CHARACTERS},
+ * {@link TextUtils#CAP_MODE_WORDS TextUtils.CAP_MODE_WORDS}, and
+ * {@link TextUtils#CAP_MODE_SENTENCES TextUtils.CAP_MODE_SENTENCES}, though
+ * you should generally just take a non-zero value to mean "start out in
+ * caps mode".
+ */
+ public int initialCapsMode = 0;
+
+ /**
+ * The "hint" text of the text view, typically shown in-line when the
+ * text is empty to tell the user what to enter.
+ */
+ public CharSequence hintText;
+
+ /**
+ * A label to show to the user describing the text they are writing.
+ */
+ public CharSequence label;
+
+ /**
+ * Name of the package that owns this editor.
+ *
+ * <p><strong>IME authors:</strong> In API level 22
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1} and prior, do not trust this package
+ * name. The system had not verified the consistency between the package name here and
+ * application's uid. Consider to use {@link InputBinding#getUid()}, which is trustworthy.
+ * Starting from {@link android.os.Build.VERSION_CODES#M}, the system verifies the consistency
+ * between this package name and application uid before {@link EditorInfo} is passed to the
+ * input method.</p>
+ *
+ * <p><strong>Editor authors:</strong> Starting from {@link android.os.Build.VERSION_CODES#M},
+ * the application is no longer
+ * able to establish input connections if the package name provided here is inconsistent with
+ * application's uid.</p>
+ */
+ public String packageName;
+
+ /**
+ * Autofill Id for the field that's currently on focus.
+ *
+ * <p> Marked as hide since it's only used by framework.</p>
+ * @hide
+ */
+ public AutofillId autofillId;
+
+ /**
+ * Identifier for the editor's field. This is optional, and may be
+ * 0. By default it is filled in with the result of
+ * {@link android.view.View#getId() View.getId()} on the View that
+ * is being edited.
+ */
+ public int fieldId;
+
+ /**
+ * Additional name for the editor's field. This can supply additional
+ * name information for the field. By default it is null. The actual
+ * contents have no meaning.
+ */
+ public String fieldName;
+
+ /**
+ * Any extra data to supply to the input method. This is for extended
+ * communication with specific input methods; the name fields in the
+ * bundle should be scoped (such as "com.mydomain.im.SOME_FIELD") so
+ * that they don't conflict with others. This field can be
+ * filled in from the {@link android.R.attr#editorExtras}
+ * attribute of a TextView.
+ */
+ public Bundle extras;
+
+ /**
+ * List of the languages that the user is supposed to switch to no matter what input method
+ * subtype is currently used. This special "hint" can be used mainly for, but not limited to,
+ * multilingual users who want IMEs to switch language context automatically.
+ *
+ * <p>{@code null} means that no special language "hint" is needed.</p>
+ *
+ * <p><strong>Editor authors:</strong> Specify this only when you are confident that the user
+ * will switch to certain languages in this context no matter what input method subtype is
+ * currently selected. Otherwise, keep this {@code null}. Explicit user actions and/or
+ * preferences would be good signals to specify this special "hint", For example, a chat
+ * application may be able to put the last used language at the top of {@link #hintLocales}
+ * based on whom the user is going to talk, by remembering what language is used in the last
+ * conversation. Do not specify {@link android.widget.TextView#getTextLocales()} only because
+ * it is used for text rendering.</p>
+ *
+ * @see android.widget.TextView#setImeHintLocales(LocaleList)
+ * @see android.widget.TextView#getImeHintLocales()
+ */
+ @Nullable
+ public LocaleList hintLocales = null;
+
+
+ /**
+ * List of acceptable MIME types for
+ * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)}.
+ *
+ * <p>{@code null} or an empty array means that
+ * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} is not supported in this
+ * editor.</p>
+ */
+ @Nullable
+ public String[] contentMimeTypes = null;
+
+ /**
+ * If not {@code null}, this editor needs to talk to IMEs that run for the specified user, no
+ * matter what user ID the calling process has.
+ *
+ * <p>Note: This field will be silently ignored when
+ * {@link com.android.server.inputmethod.InputMethodSystemProperty#MULTI_CLIENT_IME_ENABLED} is
+ * {@code true}.</p>
+ *
+ * <p>Note also that pseudo handles such as {@link UserHandle#ALL} are not supported.</p>
+ *
+ * @hide
+ */
+ @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
+ @Nullable
+ public UserHandle targetInputMethodUser = null;
+
+ @IntDef({TrimPolicy.HEAD, TrimPolicy.TAIL})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TrimPolicy {
+ int HEAD = 0;
+ int TAIL = 1;
+ }
+
+ /**
+ * The maximum length of initialSurroundingText. When the input text from
+ * {@code setInitialSurroundingText(CharSequence)} is longer than this, trimming shall be
+ * performed to keep memory efficiency.
+ */
+ @VisibleForTesting
+ static final int MEMORY_EFFICIENT_TEXT_LENGTH = 2048;
+ /**
+ * When the input text is longer than {@code #MEMORY_EFFICIENT_TEXT_LENGTH}, we start trimming
+ * the input text into three parts: BeforeCursor, Selection, and AfterCursor. We don't want to
+ * trim the Selection but we also don't want it consumes all available space. Therefore, the
+ * maximum acceptable Selection length is half of {@code #MEMORY_EFFICIENT_TEXT_LENGTH}.
+ */
+ @VisibleForTesting
+ static final int MAX_INITIAL_SELECTION_LENGTH = MEMORY_EFFICIENT_TEXT_LENGTH / 2;
+
+ @Nullable
+ private SurroundingText mInitialSurroundingText = null;
+
+
+ /**
+ * Editors may use this method to provide initial input text to IMEs. As the surrounding text
+ * could be used to provide various input assistance, we recommend editors to provide the
+ * complete initial input text in its {@link View#onCreateInputConnection(EditorInfo)} callback.
+ * The supplied text will then be processed to serve {@code #getInitialTextBeforeCursor},
+ * {@code #getInitialSelectedText}, and {@code #getInitialTextBeforeCursor}. System is allowed
+ * to trim {@code sourceText} for various reasons while keeping the most valuable data to IMEs.
+ *
+ * Starting from {@link VERSION_CODES#S}, spans that do not implement {@link Parcelable} will
+ * be automatically dropped.
+ *
+ * <p><strong>Editor authors: </strong>Providing the initial input text helps reducing IPC calls
+ * for IMEs to provide many modern features right after the connection setup. We recommend
+ * calling this method in your implementation.
+ *
+ * @param sourceText The complete input text.
+ */
+ public void setInitialSurroundingText(@NonNull CharSequence sourceText) {
+ setInitialSurroundingSubText(sourceText, /* subTextStart = */ 0);
+ }
+
+ /**
+ * Editors may use this method to provide initial input text to IMEs. As the surrounding text
+ * could be used to provide various input assistance, we recommend editors to provide the
+ * complete initial input text in its {@link View#onCreateInputConnection(EditorInfo)} callback.
+ * When trimming the input text is needed, call this method instead of
+ * {@code setInitialSurroundingText(CharSequence)} and provide the trimmed position info. Always
+ * try to include the selected text within {@code subText} to give the system best flexibility
+ * to choose where and how to trim {@code subText} when necessary.
+ *
+ * Starting from {@link VERSION_CODES#S}, spans that do not implement {@link Parcelable} will
+ * be automatically dropped.
+ *
+ * @param subText The input text. When it was trimmed, {@code subTextStart} must be provided
+ * correctly.
+ * @param subTextStart The position that the input text got trimmed. For example, when the
+ * editor wants to trim out the first 10 chars, subTextStart should be 10.
+ */
+ public void setInitialSurroundingSubText(@NonNull CharSequence subText, int subTextStart) {
+ Objects.requireNonNull(subText);
+
+ // For privacy protection reason, we don't carry password inputs to IMEs.
+ if (isPasswordInputType(inputType)) {
+ mInitialSurroundingText = null;
+ return;
+ }
+
+ // Swap selection start and end if necessary.
+ final int subTextSelStart = initialSelStart > initialSelEnd
+ ? initialSelEnd - subTextStart : initialSelStart - subTextStart;
+ final int subTextSelEnd = initialSelStart > initialSelEnd
+ ? initialSelStart - subTextStart : initialSelEnd - subTextStart;
+
+ final int subTextLength = subText.length();
+ // Unknown or invalid selection.
+ if (subTextStart < 0 || subTextSelStart < 0 || subTextSelEnd > subTextLength) {
+ mInitialSurroundingText = null;
+ return;
+ }
+
+ if (subTextLength <= MEMORY_EFFICIENT_TEXT_LENGTH) {
+ mInitialSurroundingText = new SurroundingText(subText, subTextSelStart,
+ subTextSelEnd, subTextStart);
+ return;
+ }
+
+ trimLongSurroundingText(subText, subTextSelStart, subTextSelEnd, subTextStart);
+ }
+
+ /**
+ * Trims the initial surrounding text when it is over sized. Fundamental trimming rules are:
+ * - The text before the cursor is the most important information to IMEs.
+ * - The text after the cursor is the second important information to IMEs.
+ * - The selected text is the least important information but it shall NEVER be truncated. When
+ * it is too long, just drop it.
+ *<p><pre>
+ * For example, the subText can be viewed as
+ * TextBeforeCursor + Selection + TextAfterCursor
+ * The result could be
+ * 1. (maybeTrimmedAtHead)TextBeforeCursor + Selection + TextAfterCursor(maybeTrimmedAtTail)
+ * 2. (maybeTrimmedAtHead)TextBeforeCursor + TextAfterCursor(maybeTrimmedAtTail)</pre>
+ *
+ * @param subText The long text that needs to be trimmed.
+ * @param selStart The text offset of the start of the selection.
+ * @param selEnd The text offset of the end of the selection
+ * @param subTextStart The position that the input text got trimmed.
+ */
+ private void trimLongSurroundingText(CharSequence subText, int selStart, int selEnd,
+ int subTextStart) {
+ final int sourceSelLength = selEnd - selStart;
+ // When the selected text is too long, drop it.
+ final int newSelLength = (sourceSelLength > MAX_INITIAL_SELECTION_LENGTH)
+ ? 0 : sourceSelLength;
+
+ // Distribute rest of length quota to TextBeforeCursor and TextAfterCursor in 4:1 ratio.
+ final int subTextBeforeCursorLength = selStart;
+ final int subTextAfterCursorLength = subText.length() - selEnd;
+ final int maxLengthMinusSelection = MEMORY_EFFICIENT_TEXT_LENGTH - newSelLength;
+ final int possibleMaxBeforeCursorLength =
+ Math.min(subTextBeforeCursorLength, (int) (0.8 * maxLengthMinusSelection));
+ int newAfterCursorLength = Math.min(subTextAfterCursorLength,
+ maxLengthMinusSelection - possibleMaxBeforeCursorLength);
+ int newBeforeCursorLength = Math.min(subTextBeforeCursorLength,
+ maxLengthMinusSelection - newAfterCursorLength);
+
+ // As trimming may happen at the head of TextBeforeCursor, calculate new starting position.
+ int newBeforeCursorHead = subTextBeforeCursorLength - newBeforeCursorLength;
+
+ // We don't want to cut surrogate pairs in the middle. Exam that at the new head and tail.
+ if (isCutOnSurrogate(subText,
+ selStart - newBeforeCursorLength, TrimPolicy.HEAD)) {
+ newBeforeCursorHead = newBeforeCursorHead + 1;
+ newBeforeCursorLength = newBeforeCursorLength - 1;
+ }
+ if (isCutOnSurrogate(subText,
+ selEnd + newAfterCursorLength - 1, TrimPolicy.TAIL)) {
+ newAfterCursorLength = newAfterCursorLength - 1;
+ }
+
+ // Now we know where to trim, compose the initialSurroundingText.
+ final int newTextLength = newBeforeCursorLength + newSelLength + newAfterCursorLength;
+ final CharSequence newInitialSurroundingText;
+ if (newSelLength != sourceSelLength) {
+ final CharSequence beforeCursor = subText.subSequence(newBeforeCursorHead,
+ newBeforeCursorHead + newBeforeCursorLength);
+ final CharSequence afterCursor = subText.subSequence(selEnd,
+ selEnd + newAfterCursorLength);
+
+ newInitialSurroundingText = TextUtils.concat(beforeCursor, afterCursor);
+ } else {
+ newInitialSurroundingText = subText
+ .subSequence(newBeforeCursorHead, newBeforeCursorHead + newTextLength);
+ }
+
+ // As trimming may happen at the head, adjust cursor position in the initialSurroundingText
+ // obj.
+ newBeforeCursorHead = 0;
+ final int newSelHead = newBeforeCursorHead + newBeforeCursorLength;
+ final int newOffset = subTextStart + selStart - newSelHead;
+ mInitialSurroundingText = new SurroundingText(
+ newInitialSurroundingText, newSelHead, newSelHead + newSelLength,
+ newOffset);
+ }
+
+
+ /**
+ * Get <var>length</var> characters of text before the current cursor position. May be
+ * {@code null} when the protocol is not supported.
+ *
+ * @param length The expected length of the text.
+ * @param flags Supplies additional options controlling how the text is returned. May be
+ * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}.
+ * @return the text before the cursor position; the length of the returned text might be less
+ * than <var>length</var>. When there is no text before the cursor, an empty string will be
+ * returned. It could also be {@code null} when the editor or system could not support this
+ * protocol.
+ */
+ @Nullable
+ public CharSequence getInitialTextBeforeCursor(
+ @IntRange(from = 0) int length, @InputConnection.GetTextType int flags) {
+ if (mInitialSurroundingText == null) {
+ return null;
+ }
+
+ int selStart = Math.min(mInitialSurroundingText.getSelectionStart(),
+ mInitialSurroundingText.getSelectionEnd());
+ int n = Math.min(length, selStart);
+ return ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
+ ? mInitialSurroundingText.getText().subSequence(selStart - n, selStart)
+ : TextUtils.substring(mInitialSurroundingText.getText(), selStart - n,
+ selStart);
+ }
+
+ /**
+ * Gets the selected text, if any. May be {@code null} when the protocol is not supported or the
+ * selected text is way too long.
+ *
+ * @param flags Supplies additional options controlling how the text is returned. May be
+ * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}.
+ * @return the text that is currently selected, if any. It could be an empty string when there
+ * is no text selected. When {@code null} is returned, the selected text might be too long or
+ * this protocol is not supported.
+ */
+ @Nullable
+ public CharSequence getInitialSelectedText(@InputConnection.GetTextType int flags) {
+ if (mInitialSurroundingText == null) {
+ return null;
+ }
+
+ // Swap selection start and end if necessary.
+ final int correctedTextSelStart = initialSelStart > initialSelEnd
+ ? initialSelEnd : initialSelStart;
+ final int correctedTextSelEnd = initialSelStart > initialSelEnd
+ ? initialSelStart : initialSelEnd;
+
+ final int sourceSelLength = correctedTextSelEnd - correctedTextSelStart;
+ int selStart = mInitialSurroundingText.getSelectionStart();
+ int selEnd = mInitialSurroundingText.getSelectionEnd();
+ if (selStart > selEnd) {
+ int tmp = selStart;
+ selStart = selEnd;
+ selEnd = tmp;
+ }
+ final int selLength = selEnd - selStart;
+ if (initialSelStart < 0 || initialSelEnd < 0 || selLength != sourceSelLength) {
+ return null;
+ }
+
+ return ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
+ ? mInitialSurroundingText.getText().subSequence(selStart, selEnd)
+ : TextUtils.substring(mInitialSurroundingText.getText(), selStart, selEnd);
+ }
+
+ /**
+ * Get <var>length</var> characters of text after the current cursor position. May be
+ * {@code null} when the protocol is not supported.
+ *
+ * @param length The expected length of the text.
+ * @param flags Supplies additional options controlling how the text is returned. May be
+ * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}.
+ * @return the text after the cursor position; the length of the returned text might be less
+ * than <var>length</var>. When there is no text after the cursor, an empty string will be
+ * returned. It could also be {@code null} when the editor or system could not support this
+ * protocol.
+ */
+ @Nullable
+ public CharSequence getInitialTextAfterCursor(
+ @IntRange(from = 0) int length, @InputConnection.GetTextType int flags) {
+ if (mInitialSurroundingText == null) {
+ return null;
+ }
+
+ int surroundingTextLength = mInitialSurroundingText.getText().length();
+ int selEnd = Math.max(mInitialSurroundingText.getSelectionStart(),
+ mInitialSurroundingText.getSelectionEnd());
+ int n = Math.min(length, surroundingTextLength - selEnd);
+ return ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
+ ? mInitialSurroundingText.getText().subSequence(selEnd, selEnd + n)
+ : TextUtils.substring(mInitialSurroundingText.getText(), selEnd, selEnd + n);
+ }
+
+ /**
+ * Gets the surrounding text around the current cursor, with <var>beforeLength</var> characters
+ * of text before the cursor (start of the selection), <var>afterLength</var> characters of text
+ * after the cursor (end of the selection), and all of the selected text.
+ *
+ * <p>The initial surrounding text for return could be trimmed if oversize. Fundamental trimming
+ * rules are:</p>
+ * <ul>
+ * <li>The text before the cursor is the most important information to IMEs.</li>
+ * <li>The text after the cursor is the second important information to IMEs.</li>
+ * <li>The selected text is the least important information but it shall NEVER be truncated.
+ * When it is too long, just drop it.</li>
+ * </ul>
+ *
+ * <p>For example, the subText can be viewed as TextBeforeCursor + Selection + TextAfterCursor.
+ * The result could be:</p>
+ * <ol>
+ * <li>(maybeTrimmedAtHead)TextBeforeCursor + Selection
+ * + TextAfterCursor(maybeTrimmedAtTail)</li>
+ * <li>(maybeTrimmedAtHead)TextBeforeCursor + TextAfterCursor(maybeTrimmedAtTail)</li>
+ * </ol>
+ *
+ * @param beforeLength The expected length of the text before the cursor.
+ * @param afterLength The expected length of the text after the cursor.
+ * @param flags Supplies additional options controlling how the text is returned. May be either
+ * {@code 0} or {@link InputConnection#GET_TEXT_WITH_STYLES}.
+ * @return an {@link android.view.inputmethod.SurroundingText} object describing the surrounding
+ * text and state of selection, or {@code null} if the editor or system could not support this
+ * protocol.
+ * @throws IllegalArgumentException if {@code beforeLength} or {@code afterLength} is negative.
+ */
+ @Nullable
+ public SurroundingText getInitialSurroundingText(
+ @IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength,
+ @InputConnection.GetTextType int flags) {
+ Preconditions.checkArgumentNonnegative(beforeLength);
+ Preconditions.checkArgumentNonnegative(afterLength);
+
+ if (mInitialSurroundingText == null) {
+ return null;
+ }
+
+ int length = mInitialSurroundingText.getText().length();
+ int selStart = mInitialSurroundingText.getSelectionStart();
+ int selEnd = mInitialSurroundingText.getSelectionEnd();
+ if (selStart > selEnd) {
+ int tmp = selStart;
+ selStart = selEnd;
+ selEnd = tmp;
+ }
+
+ int before = Math.min(beforeLength, selStart);
+ int after = Math.min(selEnd + afterLength, length);
+ int offset = selStart - before;
+ CharSequence newText = ((flags & InputConnection.GET_TEXT_WITH_STYLES) != 0)
+ ? mInitialSurroundingText.getText().subSequence(offset, after)
+ : TextUtils.substring(mInitialSurroundingText.getText(), offset, after);
+ int newSelEnd = Math.min(selEnd - offset, length);
+ return new SurroundingText(newText, before, newSelEnd,
+ mInitialSurroundingText.getOffset() + offset);
+ }
+
+ private static boolean isCutOnSurrogate(CharSequence sourceText, int cutPosition,
+ @TrimPolicy int policy) {
+ switch (policy) {
+ case TrimPolicy.HEAD:
+ return Character.isLowSurrogate(sourceText.charAt(cutPosition));
+ case TrimPolicy.TAIL:
+ return Character.isHighSurrogate(sourceText.charAt(cutPosition));
+ default:
+ return false;
+ }
+ }
+
+ private static boolean isPasswordInputType(int inputType) {
+ final int variation =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return variation
+ == (TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD)
+ || variation
+ == (TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD)
+ || variation
+ == (TYPE_CLASS_NUMBER | TYPE_NUMBER_VARIATION_PASSWORD);
+ }
+
+ /**
+ * Ensure that the data in this EditorInfo is compatible with an application
+ * that was developed against the given target API version. This can
+ * impact the following input types:
+ * {@link InputType#TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS},
+ * {@link InputType#TYPE_TEXT_VARIATION_WEB_PASSWORD},
+ * {@link InputType#TYPE_NUMBER_VARIATION_NORMAL},
+ * {@link InputType#TYPE_NUMBER_VARIATION_PASSWORD}.
+ *
+ * <p>This is called by the framework for input method implementations;
+ * you should not generally need to call it yourself.
+ *
+ * @param targetSdkVersion The API version number that the compatible
+ * application was developed against.
+ */
+ public final void makeCompatible(int targetSdkVersion) {
+ if (targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
+ switch (inputType&(TYPE_MASK_CLASS|TYPE_MASK_VARIATION)) {
+ case TYPE_CLASS_TEXT|TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
+ inputType = TYPE_CLASS_TEXT|TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ | (inputType&TYPE_MASK_FLAGS);
+ break;
+ case TYPE_CLASS_TEXT|TYPE_TEXT_VARIATION_WEB_PASSWORD:
+ inputType = TYPE_CLASS_TEXT|TYPE_TEXT_VARIATION_PASSWORD
+ | (inputType&TYPE_MASK_FLAGS);
+ break;
+ case TYPE_CLASS_NUMBER|TYPE_NUMBER_VARIATION_NORMAL:
+ case TYPE_CLASS_NUMBER|TYPE_NUMBER_VARIATION_PASSWORD:
+ inputType = TYPE_CLASS_NUMBER
+ | (inputType&TYPE_MASK_FLAGS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Export the state of {@link EditorInfo} into a protocol buffer output stream.
+ *
+ * @param proto Stream to write the state to
+ * @param fieldId FieldId of ViewRootImpl as defined in the parent message
+ * @hide
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(INPUT_TYPE, inputType);
+ proto.write(IME_OPTIONS, imeOptions);
+ proto.write(PRIVATE_IME_OPTIONS, privateImeOptions);
+ proto.write(PACKAGE_NAME, packageName);
+ proto.write(FIELD_ID, this.fieldId);
+ if (targetInputMethodUser != null) {
+ proto.write(TARGET_INPUT_METHOD_USER_ID, targetInputMethodUser.getIdentifier());
+ }
+ proto.end(token);
+ }
+
+ /**
+ * Write debug output of this object.
+ */
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "inputType=0x" + Integer.toHexString(inputType)
+ + " imeOptions=0x" + Integer.toHexString(imeOptions)
+ + " privateImeOptions=" + privateImeOptions);
+ pw.println(prefix + "actionLabel=" + actionLabel
+ + " actionId=" + actionId);
+ pw.println(prefix + "initialSelStart=" + initialSelStart
+ + " initialSelEnd=" + initialSelEnd
+ + " initialCapsMode=0x"
+ + Integer.toHexString(initialCapsMode));
+ pw.println(prefix + "hintText=" + hintText
+ + " label=" + label);
+ pw.println(prefix + "packageName=" + packageName
+ + " autofillId=" + autofillId
+ + " fieldId=" + fieldId
+ + " fieldName=" + fieldName);
+ pw.println(prefix + "extras=" + extras);
+ pw.println(prefix + "hintLocales=" + hintLocales);
+ pw.println(prefix + "contentMimeTypes=" + Arrays.toString(contentMimeTypes));
+ if (targetInputMethodUser != null) {
+ pw.println(prefix + "targetInputMethodUserId=" + targetInputMethodUser.getIdentifier());
+ }
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(inputType);
+ dest.writeInt(imeOptions);
+ dest.writeString(privateImeOptions);
+ dest.writeInt(internalImeOptions);
+ TextUtils.writeToParcel(actionLabel, dest, flags);
+ dest.writeInt(actionId);
+ dest.writeInt(initialSelStart);
+ dest.writeInt(initialSelEnd);
+ dest.writeInt(initialCapsMode);
+ TextUtils.writeToParcel(hintText, dest, flags);
+ TextUtils.writeToParcel(label, dest, flags);
+ dest.writeString(packageName);
+ dest.writeParcelable(autofillId, flags);
+ dest.writeInt(fieldId);
+ dest.writeString(fieldName);
+ dest.writeBundle(extras);
+ dest.writeBoolean(mInitialSurroundingText != null);
+ if (mInitialSurroundingText != null) {
+ mInitialSurroundingText.writeToParcel(dest, flags);
+ }
+ if (hintLocales != null) {
+ hintLocales.writeToParcel(dest, flags);
+ } else {
+ LocaleList.getEmptyLocaleList().writeToParcel(dest, flags);
+ }
+ dest.writeStringArray(contentMimeTypes);
+ UserHandle.writeToParcel(targetInputMethodUser, dest);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<EditorInfo> CREATOR =
+ new Parcelable.Creator<EditorInfo>() {
+ public EditorInfo createFromParcel(Parcel source) {
+ EditorInfo res = new EditorInfo();
+ res.inputType = source.readInt();
+ res.imeOptions = source.readInt();
+ res.privateImeOptions = source.readString();
+ res.internalImeOptions = source.readInt();
+ res.actionLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ res.actionId = source.readInt();
+ res.initialSelStart = source.readInt();
+ res.initialSelEnd = source.readInt();
+ res.initialCapsMode = source.readInt();
+ res.hintText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ res.label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ res.packageName = source.readString();
+ res.autofillId = source.readParcelable(AutofillId.class.getClassLoader());
+ res.fieldId = source.readInt();
+ res.fieldName = source.readString();
+ res.extras = source.readBundle();
+ boolean hasInitialSurroundingText = source.readBoolean();
+ if (hasInitialSurroundingText) {
+ res.mInitialSurroundingText =
+ SurroundingText.CREATOR.createFromParcel(source);
+ }
+ LocaleList hintLocales = LocaleList.CREATOR.createFromParcel(source);
+ res.hintLocales = hintLocales.isEmpty() ? null : hintLocales;
+ res.contentMimeTypes = source.readStringArray();
+ res.targetInputMethodUser = UserHandle.readFromParcel(source);
+ return res;
+ }
+
+ public EditorInfo[] newArray(int size) {
+ return new EditorInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/android/view/inputmethod/ExtractedText.java b/android/view/inputmethod/ExtractedText.java
new file mode 100644
index 0000000..159d2aa
--- /dev/null
+++ b/android/view/inputmethod/ExtractedText.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Information about text that has been extracted for use by an input method.
+ *
+ * This contains information about a portion of the currently edited text,
+ * that the IME should display into its own interface while in extracted mode.
+ */
+public class ExtractedText implements Parcelable {
+ /**
+ * The text that has been extracted.
+ *
+ * @see android.widget.TextView#getText()
+ */
+ public CharSequence text;
+
+ /**
+ * The offset in the overall text at which the extracted text starts.
+ */
+ public int startOffset;
+
+ /**
+ * If the content is a report of a partial text change, this is the
+ * offset where the change starts and it runs until
+ * {@link #partialEndOffset}. If the content is the full text, this
+ * field is -1.
+ */
+ public int partialStartOffset;
+
+ /**
+ * If the content is a report of a partial text change, this is the offset
+ * where the change ends. Note that the actual text may be larger or
+ * smaller than the difference between this and {@link #partialStartOffset},
+ * meaning a reduction or increase, respectively, in the total text.
+ */
+ public int partialEndOffset;
+
+ /**
+ * The offset where the selection currently starts within the extracted
+ * text. The real selection start position is at
+ * <var>startOffset</var>+<var>selectionStart</var>.
+ */
+ public int selectionStart;
+
+ /**
+ * The offset where the selection currently ends within the extracted
+ * text. The real selection end position is at
+ * <var>startOffset</var>+<var>selectionEnd</var>.
+ */
+ public int selectionEnd;
+
+ /**
+ * Bit for {@link #flags}: set if the text being edited can only be on
+ * a single line.
+ */
+ public static final int FLAG_SINGLE_LINE = 0x0001;
+
+ /**
+ * Bit for {@link #flags}: set if the editor is currently in selection mode.
+ *
+ * This happens when a hardware keyboard with latched keys is attached and
+ * the shift key is currently latched.
+ */
+ public static final int FLAG_SELECTING = 0x0002;
+
+ /**
+ * Additional bit flags of information about the edited text.
+ */
+ public int flags;
+
+ /**
+ * The hint that has been extracted.
+ *
+ * @see android.widget.TextView#getHint()
+ */
+ public CharSequence hint;
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ TextUtils.writeToParcel(text, dest, flags);
+ dest.writeInt(startOffset);
+ dest.writeInt(partialStartOffset);
+ dest.writeInt(partialEndOffset);
+ dest.writeInt(selectionStart);
+ dest.writeInt(selectionEnd);
+ dest.writeInt(this.flags);
+ TextUtils.writeToParcel(hint, dest, flags);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<ExtractedText> CREATOR
+ = new Parcelable.Creator<ExtractedText>() {
+ public ExtractedText createFromParcel(Parcel source) {
+ ExtractedText res = new ExtractedText();
+ res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ res.startOffset = source.readInt();
+ res.partialStartOffset = source.readInt();
+ res.partialEndOffset = source.readInt();
+ res.selectionStart = source.readInt();
+ res.selectionEnd = source.readInt();
+ res.flags = source.readInt();
+ res.hint = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ return res;
+ }
+
+ public ExtractedText[] newArray(int size) {
+ return new ExtractedText[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/ExtractedTextRequest.java b/android/view/inputmethod/ExtractedTextRequest.java
new file mode 100644
index 0000000..e442185
--- /dev/null
+++ b/android/view/inputmethod/ExtractedTextRequest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Description of what an input method would like from an application when
+ * extract text from its input editor.
+ */
+public class ExtractedTextRequest implements Parcelable {
+ /**
+ * Arbitrary integer that can be supplied in the request, which will be
+ * delivered back when reporting updates.
+ */
+ public int token;
+
+ /**
+ * Additional request flags, having the same possible values as the
+ * flags parameter of {@link InputConnection#getTextBeforeCursor
+ * InputConnection.getTextBeforeCursor()}.
+ */
+ public int flags;
+
+ /**
+ * Hint for the maximum number of lines to return.
+ */
+ public int hintMaxLines;
+
+ /**
+ * Hint for the maximum number of characters to return.
+ */
+ public int hintMaxChars;
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(token);
+ dest.writeInt(this.flags);
+ dest.writeInt(hintMaxLines);
+ dest.writeInt(hintMaxChars);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<ExtractedTextRequest> CREATOR
+ = new Parcelable.Creator<ExtractedTextRequest>() {
+ public ExtractedTextRequest createFromParcel(Parcel source) {
+ ExtractedTextRequest res = new ExtractedTextRequest();
+ res.token = source.readInt();
+ res.flags = source.readInt();
+ res.hintMaxLines = source.readInt();
+ res.hintMaxChars = source.readInt();
+ return res;
+ }
+
+ public ExtractedTextRequest[] newArray(int size) {
+ return new ExtractedTextRequest[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/InlineSuggestion.java b/android/view/inputmethod/InlineSuggestion.java
new file mode 100644
index 0000000..27c637b
--- /dev/null
+++ b/android/view/inputmethod/InlineSuggestion.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.BinderThread;
+import android.annotation.CallbackExecutor;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Size;
+import android.util.Slog;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.inline.InlineContentView;
+
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Parcelling;
+import com.android.internal.view.inline.IInlineContentCallback;
+import com.android.internal.view.inline.IInlineContentProvider;
+import com.android.internal.view.inline.InlineTooltipUi;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * This class represents an inline suggestion which is made by one app and can be embedded into the
+ * UI of another. Suggestions may contain sensitive information not known to the host app which
+ * needs to be protected from spoofing. To address that the suggestion view inflated on demand for
+ * embedding is created in such a way that the hosting app cannot introspect its content and cannot
+ * interact with it.
+ */
+@DataClass(genEqualsHashCode = true, genToString = true, genHiddenConstDefs = true,
+ genHiddenConstructor = true)
+public final class InlineSuggestion implements Parcelable {
+
+ private static final String TAG = "InlineSuggestion";
+
+ @NonNull
+ private final InlineSuggestionInfo mInfo;
+
+ /**
+ * @hide
+ */
+ @Nullable
+ private final IInlineContentProvider mContentProvider;
+
+ /**
+ * Used to keep a strong reference to the callback so it doesn't get garbage collected.
+ *
+ * @hide
+ */
+ @DataClass.ParcelWith(InlineContentCallbackImplParceling.class)
+ @Nullable
+ private InlineContentCallbackImpl mInlineContentCallback;
+
+ /**
+ * Used to show up the inline suggestion tooltip.
+ *
+ * @hide
+ */
+ @Nullable
+ @DataClass.ParcelWith(InlineTooltipUiParceling.class)
+ private InlineTooltipUi mInlineTooltipUi;
+
+ /**
+ * Creates a new {@link InlineSuggestion}, for testing purpose.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public static InlineSuggestion newInlineSuggestion(@NonNull InlineSuggestionInfo info) {
+ return new InlineSuggestion(info, null, /* inlineContentCallback */ null,
+ /* inlineTooltipUi */ null);
+ }
+
+ /**
+ * Creates a new {@link InlineSuggestion}.
+ *
+ * @hide
+ */
+ public InlineSuggestion(@NonNull InlineSuggestionInfo info,
+ @Nullable IInlineContentProvider contentProvider) {
+ this(info, contentProvider, /* inlineContentCallback */ null, /* inlineTooltipUi */ null);
+ }
+
+ /**
+ * Inflates a view with the content of this suggestion at a specific size.
+ *
+ * <p> Each dimension of the size must satisfy one of the following conditions:
+ *
+ * <ol>
+ * <li>between {@link android.widget.inline.InlinePresentationSpec#getMinSize()} and
+ * {@link android.widget.inline.InlinePresentationSpec#getMaxSize()} of the presentation spec
+ * from {@code mInfo}
+ * <li>{@link ViewGroup.LayoutParams#WRAP_CONTENT}
+ * </ol>
+ *
+ * If the size is set to {@link
+ * ViewGroup.LayoutParams#WRAP_CONTENT}, then the size of the inflated view will be just large
+ * enough to fit the content, while still conforming to the min / max size specified by the
+ * {@link android.widget.inline.InlinePresentationSpec}.
+ *
+ * <p> The caller can attach an {@link android.view.View.OnClickListener} and/or an
+ * {@link android.view.View.OnLongClickListener} to the view in the {@code callback} to receive
+ * click and long click events on the view.
+ *
+ * @param context Context in which to inflate the view.
+ * @param size The size at which to inflate the suggestion. For each dimension, it maybe an
+ * exact value or {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
+ * @param callback Callback for receiving the inflated view, where the {@link
+ * ViewGroup.LayoutParams} of the view is set as the actual size of the
+ * underlying remote view.
+ * @throws IllegalArgumentException If an invalid argument is passed.
+ * @throws IllegalStateException If this method is already called.
+ */
+ public void inflate(@NonNull Context context, @NonNull Size size,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull Consumer<InlineContentView> callback) {
+ final Size minSize = mInfo.getInlinePresentationSpec().getMinSize();
+ final Size maxSize = mInfo.getInlinePresentationSpec().getMaxSize();
+ if (!isValid(size.getWidth(), minSize.getWidth(), maxSize.getWidth())
+ || !isValid(size.getHeight(), minSize.getHeight(), maxSize.getHeight())) {
+ throw new IllegalArgumentException(
+ "size is neither between min:" + minSize + " and max:" + maxSize
+ + ", nor wrap_content");
+ }
+
+ InlineSuggestion toolTip = mInfo.getTooltip();
+ if (toolTip != null) {
+ if (mInlineTooltipUi == null) {
+ mInlineTooltipUi = new InlineTooltipUi(context);
+ }
+ } else {
+ mInlineTooltipUi = null;
+ }
+
+ mInlineContentCallback = getInlineContentCallback(context, callbackExecutor, callback,
+ mInlineTooltipUi);
+ if (mContentProvider == null) {
+ callbackExecutor.execute(() -> callback.accept(/* view */ null));
+ mInlineTooltipUi = null;
+ return;
+ }
+ try {
+ mContentProvider.provideContent(size.getWidth(), size.getHeight(),
+ new InlineContentCallbackWrapper(mInlineContentCallback));
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error creating suggestion content surface: " + e);
+ callbackExecutor.execute(() -> callback.accept(/* view */ null));
+ }
+ if (toolTip == null) return;
+
+ final Size tooltipSize = new Size(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ mInfo.getTooltip().inflate(context, tooltipSize, callbackExecutor, view -> {
+ Handler.getMain().post(() -> mInlineTooltipUi.setTooltipView(view));
+ });
+ }
+
+ /**
+ * Returns true if the {@code actual} length is within [min, max] or is {@link
+ * ViewGroup.LayoutParams#WRAP_CONTENT}.
+ */
+ private static boolean isValid(int actual, int min, int max) {
+ if (actual == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ return true;
+ }
+ return actual >= min && actual <= max;
+ }
+
+ private synchronized InlineContentCallbackImpl getInlineContentCallback(Context context,
+ Executor callbackExecutor, Consumer<InlineContentView> callback,
+ InlineTooltipUi inlineTooltipUi) {
+ if (mInlineContentCallback != null) {
+ throw new IllegalStateException("Already called #inflate()");
+ }
+ return new InlineContentCallbackImpl(context, mContentProvider, callbackExecutor,
+ callback, inlineTooltipUi);
+ }
+
+ /**
+ * A wrapper class around the {@link InlineContentCallbackImpl} to ensure it's not strongly
+ * reference by the remote system server process.
+ */
+ private static final class InlineContentCallbackWrapper extends IInlineContentCallback.Stub {
+
+ private final WeakReference<InlineContentCallbackImpl> mCallbackImpl;
+
+ InlineContentCallbackWrapper(InlineContentCallbackImpl callbackImpl) {
+ mCallbackImpl = new WeakReference<>(callbackImpl);
+ }
+
+ @Override
+ @BinderThread
+ public void onContent(SurfaceControlViewHost.SurfacePackage content, int width,
+ int height) {
+ final InlineContentCallbackImpl callbackImpl = mCallbackImpl.get();
+ if (callbackImpl != null) {
+ callbackImpl.onContent(content, width, height);
+ }
+ }
+
+ @Override
+ @BinderThread
+ public void onClick() {
+ final InlineContentCallbackImpl callbackImpl = mCallbackImpl.get();
+ if (callbackImpl != null) {
+ callbackImpl.onClick();
+ }
+ }
+
+ @Override
+ @BinderThread
+ public void onLongClick() {
+ final InlineContentCallbackImpl callbackImpl = mCallbackImpl.get();
+ if (callbackImpl != null) {
+ callbackImpl.onLongClick();
+ }
+ }
+ }
+
+ /**
+ * Handles the communication between the inline suggestion view in current (IME) process and
+ * the remote view provided from the system server.
+ *
+ * <p>This class is thread safe, because all the outside calls are piped into a single
+ * handler thread to be processed.
+ */
+ private static final class InlineContentCallbackImpl {
+
+ @NonNull
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ @NonNull
+ private final Context mContext;
+ @Nullable
+ private final IInlineContentProvider mInlineContentProvider;
+ @NonNull
+ private final Executor mCallbackExecutor;
+
+ /**
+ * Callback from the client (IME) that will receive the inflated suggestion view. It'll
+ * only be called once when the view SurfacePackage is first sent back to the client. Any
+ * updates to the view due to attach to window and detach from window events will be
+ * handled under the hood, transparent from the client.
+ */
+ @NonNull
+ private final Consumer<InlineContentView> mCallback;
+
+ /**
+ * Indicates whether the first content has been received or not.
+ */
+ private boolean mFirstContentReceived = false;
+
+ /**
+ * The client (IME) side view which internally wraps a remote view. It'll be set when
+ * {@link #onContent(SurfaceControlViewHost.SurfacePackage, int, int)} is called, which
+ * should only happen once in the lifecycle of this inline suggestion instance.
+ */
+ @Nullable
+ private InlineContentView mView;
+
+ /**
+ * The SurfacePackage pointing to the remote view. It's cached here to be sent to the next
+ * available consumer.
+ */
+ @Nullable
+ private SurfaceControlViewHost.SurfacePackage mSurfacePackage;
+
+ /**
+ * The callback (from the {@link InlineContentView}) which consumes the surface package.
+ * It's cached here to be called when the SurfacePackage is returned from the remote
+ * view owning process.
+ */
+ @Nullable
+ private Consumer<SurfaceControlViewHost.SurfacePackage> mSurfacePackageConsumer;
+
+ @Nullable
+ private InlineTooltipUi mInlineTooltipUi;
+
+ InlineContentCallbackImpl(@NonNull Context context,
+ @Nullable IInlineContentProvider inlineContentProvider,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull Consumer<InlineContentView> callback,
+ @Nullable InlineTooltipUi inlineTooltipUi) {
+ mContext = context;
+ mInlineContentProvider = inlineContentProvider;
+ mCallbackExecutor = callbackExecutor;
+ mCallback = callback;
+ mInlineTooltipUi = inlineTooltipUi;
+ }
+
+ @BinderThread
+ public void onContent(SurfaceControlViewHost.SurfacePackage content, int width,
+ int height) {
+ mMainHandler.post(() -> handleOnContent(content, width, height));
+ }
+
+ @MainThread
+ private void handleOnContent(SurfaceControlViewHost.SurfacePackage content, int width,
+ int height) {
+ if (!mFirstContentReceived) {
+ handleOnFirstContentReceived(content, width, height);
+ mFirstContentReceived = true;
+ } else {
+ handleOnSurfacePackage(content);
+ }
+ }
+
+ /**
+ * Called when the view content is returned for the first time.
+ */
+ @MainThread
+ private void handleOnFirstContentReceived(SurfaceControlViewHost.SurfacePackage content,
+ int width, int height) {
+ mSurfacePackage = content;
+ if (mSurfacePackage == null) {
+ mCallbackExecutor.execute(() -> mCallback.accept(/* view */null));
+ } else {
+ mView = new InlineContentView(mContext);
+ if (mInlineTooltipUi != null) {
+ mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (mInlineTooltipUi != null) {
+ mInlineTooltipUi.update(mView);
+ }
+ }
+ });
+ }
+ mView.setLayoutParams(new ViewGroup.LayoutParams(width, height));
+ mView.setChildSurfacePackageUpdater(getSurfacePackageUpdater());
+ mCallbackExecutor.execute(() -> mCallback.accept(mView));
+ }
+ }
+
+ /**
+ * Called when any subsequent SurfacePackage is returned from the remote view owning
+ * process.
+ */
+ @MainThread
+ private void handleOnSurfacePackage(SurfaceControlViewHost.SurfacePackage surfacePackage) {
+ if (surfacePackage == null) {
+ return;
+ }
+ if (mSurfacePackage != null || mSurfacePackageConsumer == null) {
+ // The surface package is not consumed, release it immediately.
+ surfacePackage.release();
+ try {
+ mInlineContentProvider.onSurfacePackageReleased();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error calling onSurfacePackageReleased(): " + e);
+ }
+ return;
+ }
+ mSurfacePackage = surfacePackage;
+ if (mSurfacePackage == null) {
+ return;
+ }
+ if (mSurfacePackageConsumer != null) {
+ mSurfacePackageConsumer.accept(mSurfacePackage);
+ mSurfacePackageConsumer = null;
+ }
+ }
+
+ @MainThread
+ private void handleOnSurfacePackageReleased() {
+ if (mSurfacePackage != null) {
+ try {
+ mInlineContentProvider.onSurfacePackageReleased();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error calling onSurfacePackageReleased(): " + e);
+ }
+ mSurfacePackage = null;
+ }
+ // Clear the pending surface package consumer, if any. This can happen if the IME
+ // attaches the view to window and then quickly detaches it from the window, before
+ // the surface package requested upon attaching to window was returned.
+ mSurfacePackageConsumer = null;
+ }
+
+ @MainThread
+ private void handleGetSurfacePackage(
+ Consumer<SurfaceControlViewHost.SurfacePackage> consumer) {
+ if (mSurfacePackage != null) {
+ consumer.accept(mSurfacePackage);
+ } else {
+ mSurfacePackageConsumer = consumer;
+ try {
+ mInlineContentProvider.requestSurfacePackage();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error calling getSurfacePackage(): " + e);
+ consumer.accept(null);
+ mSurfacePackageConsumer = null;
+ }
+ }
+ }
+
+ private InlineContentView.SurfacePackageUpdater getSurfacePackageUpdater() {
+ return new InlineContentView.SurfacePackageUpdater() {
+ @Override
+ public void onSurfacePackageReleased() {
+ mMainHandler.post(
+ () -> InlineContentCallbackImpl.this.handleOnSurfacePackageReleased());
+ }
+
+ @Override
+ public void getSurfacePackage(
+ Consumer<SurfaceControlViewHost.SurfacePackage> consumer) {
+ mMainHandler.post(
+ () -> InlineContentCallbackImpl.this.handleGetSurfacePackage(consumer));
+ }
+ };
+ }
+
+ @BinderThread
+ public void onClick() {
+ mMainHandler.post(() -> {
+ if (mView != null && mView.hasOnClickListeners()) {
+ mView.callOnClick();
+ }
+ });
+ }
+
+ @BinderThread
+ public void onLongClick() {
+ mMainHandler.post(() -> {
+ if (mView != null && mView.hasOnLongClickListeners()) {
+ mView.performLongClick();
+ }
+ });
+ }
+ }
+
+ /**
+ * This class used to provide parcelling logic for InlineContentCallbackImpl. It's intended to
+ * make this parcelling a no-op, since it can't be parceled and we don't need to parcel it.
+ */
+ private static class InlineContentCallbackImplParceling implements
+ Parcelling<InlineContentCallbackImpl> {
+ @Override
+ public void parcel(InlineContentCallbackImpl item, Parcel dest, int parcelFlags) {
+ }
+
+ @Override
+ public InlineContentCallbackImpl unparcel(Parcel source) {
+ return null;
+ }
+ }
+
+ /**
+ * This class used to provide parcelling logic for InlineContentCallbackImpl. It's intended to
+ * make this parcelling a no-op, since it can't be parceled and we don't need to parcel it.
+ */
+ private static class InlineTooltipUiParceling implements
+ Parcelling<InlineTooltipUi> {
+ @Override
+ public void parcel(InlineTooltipUi item, Parcel dest, int parcelFlags) {
+ }
+
+ @Override
+ public InlineTooltipUi unparcel(Parcel source) {
+ return null;
+ }
+ }
+
+
+
+ // Code below generated by codegen v1.0.22.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/inputmethod/InlineSuggestion.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new InlineSuggestion.
+ *
+ * @param inlineContentCallback
+ * Used to keep a strong reference to the callback so it doesn't get garbage collected.
+ * @param inlineTooltipUi
+ * Used to show up the inline suggestion tooltip.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public InlineSuggestion(
+ @NonNull InlineSuggestionInfo info,
+ @Nullable IInlineContentProvider contentProvider,
+ @Nullable InlineContentCallbackImpl inlineContentCallback,
+ @Nullable InlineTooltipUi inlineTooltipUi) {
+ this.mInfo = info;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInfo);
+ this.mContentProvider = contentProvider;
+ this.mInlineContentCallback = inlineContentCallback;
+ this.mInlineTooltipUi = inlineTooltipUi;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public @NonNull InlineSuggestionInfo getInfo() {
+ return mInfo;
+ }
+
+ /**
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable IInlineContentProvider getContentProvider() {
+ return mContentProvider;
+ }
+
+ /**
+ * Used to keep a strong reference to the callback so it doesn't get garbage collected.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineContentCallbackImpl getInlineContentCallback() {
+ return mInlineContentCallback;
+ }
+
+ /**
+ * Used to show up the inline suggestion tooltip.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineTooltipUi getInlineTooltipUi() {
+ return mInlineTooltipUi;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InlineSuggestion { " +
+ "info = " + mInfo + ", " +
+ "contentProvider = " + mContentProvider + ", " +
+ "inlineContentCallback = " + mInlineContentCallback + ", " +
+ "inlineTooltipUi = " + mInlineTooltipUi +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(InlineSuggestion other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InlineSuggestion that = (InlineSuggestion) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Objects.equals(mInfo, that.mInfo)
+ && java.util.Objects.equals(mContentProvider, that.mContentProvider)
+ && java.util.Objects.equals(mInlineContentCallback, that.mInlineContentCallback)
+ && java.util.Objects.equals(mInlineTooltipUi, that.mInlineTooltipUi);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInfo);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mContentProvider);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlineContentCallback);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlineTooltipUi);
+ return _hash;
+ }
+
+ @DataClass.Generated.Member
+ static Parcelling<InlineContentCallbackImpl> sParcellingForInlineContentCallback =
+ Parcelling.Cache.get(
+ InlineContentCallbackImplParceling.class);
+ static {
+ if (sParcellingForInlineContentCallback == null) {
+ sParcellingForInlineContentCallback = Parcelling.Cache.put(
+ new InlineContentCallbackImplParceling());
+ }
+ }
+
+ @DataClass.Generated.Member
+ static Parcelling<InlineTooltipUi> sParcellingForInlineTooltipUi =
+ Parcelling.Cache.get(
+ InlineTooltipUiParceling.class);
+ static {
+ if (sParcellingForInlineTooltipUi == null) {
+ sParcellingForInlineTooltipUi = Parcelling.Cache.put(
+ new InlineTooltipUiParceling());
+ }
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mContentProvider != null) flg |= 0x2;
+ if (mInlineContentCallback != null) flg |= 0x4;
+ if (mInlineTooltipUi != null) flg |= 0x8;
+ dest.writeByte(flg);
+ dest.writeTypedObject(mInfo, flags);
+ if (mContentProvider != null) dest.writeStrongInterface(mContentProvider);
+ sParcellingForInlineContentCallback.parcel(mInlineContentCallback, dest, flags);
+ sParcellingForInlineTooltipUi.parcel(mInlineTooltipUi, dest, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InlineSuggestion(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ InlineSuggestionInfo info = (InlineSuggestionInfo) in.readTypedObject(InlineSuggestionInfo.CREATOR);
+ IInlineContentProvider contentProvider = (flg & 0x2) == 0 ? null : IInlineContentProvider.Stub.asInterface(in.readStrongBinder());
+ InlineContentCallbackImpl inlineContentCallback = sParcellingForInlineContentCallback.unparcel(in);
+ InlineTooltipUi inlineTooltipUi = sParcellingForInlineTooltipUi.unparcel(in);
+
+ this.mInfo = info;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInfo);
+ this.mContentProvider = contentProvider;
+ this.mInlineContentCallback = inlineContentCallback;
+ this.mInlineTooltipUi = inlineTooltipUi;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InlineSuggestion> CREATOR
+ = new Parcelable.Creator<InlineSuggestion>() {
+ @Override
+ public InlineSuggestion[] newArray(int size) {
+ return new InlineSuggestion[size];
+ }
+
+ @Override
+ public InlineSuggestion createFromParcel(@NonNull Parcel in) {
+ return new InlineSuggestion(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1615562097666L,
+ codegenVersion = "1.0.22",
+ sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestion.java",
+ inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\nprivate @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineContentCallbackImplParceling.class) @android.annotation.Nullable android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl mInlineContentCallback\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineTooltipUiParceling.class) com.android.internal.view.inline.InlineTooltipUi mInlineTooltipUi\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestion newInlineSuggestion(android.view.inputmethod.InlineSuggestionInfo)\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer<android.widget.inline.InlineContentView>)\nprivate static boolean isValid(int,int,int)\nprivate synchronized android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl getInlineContentCallback(android.content.Context,java.util.concurrent.Executor,java.util.function.Consumer<android.widget.inline.InlineContentView>,com.android.internal.view.inline.InlineTooltipUi)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\[email protected](genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/inputmethod/InlineSuggestionInfo.java b/android/view/inputmethod/InlineSuggestionInfo.java
new file mode 100644
index 0000000..5798614
--- /dev/null
+++ b/android/view/inputmethod/InlineSuggestionInfo.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.widget.inline.InlinePresentationSpec;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * This class represents the description of an inline suggestion. It contains information to help
+ * the IME to decide where and when to show the suggestions. See {@link InlineSuggestion} for more
+ * information.
+ */
+@DataClass(
+ genEqualsHashCode = true,
+ genToString = true,
+ genHiddenConstDefs = true,
+ genHiddenConstructor = true)
+public final class InlineSuggestionInfo implements Parcelable {
+
+ /**
+ * Suggestion source: the suggestion is made by the user selected autofill service.
+ */
+ public static final @Source String SOURCE_AUTOFILL = "android:autofill";
+ /**
+ * Suggestion source: the suggestion is made by the platform.
+ */
+ public static final @Source String SOURCE_PLATFORM = "android:platform";
+
+ /**
+ * UI type: the UI contains an Autofill suggestion that will autofill the fields when tapped.
+ */
+ public static final @Type String TYPE_SUGGESTION = "android:autofill:suggestion";
+
+ /**
+ * UI type: the UI contains an widget that will launch an intent when tapped.
+ */
+ @SuppressLint({"IntentName"})
+ public static final @Type String TYPE_ACTION = "android:autofill:action";
+
+ /** The presentation spec to which the inflated suggestion view abides. */
+ private final @NonNull InlinePresentationSpec mInlinePresentationSpec;
+
+ /** The source from which the suggestion is provided. */
+ private final @NonNull @Source String mSource;
+
+ /** Hints for the type of data being suggested. */
+ private final @Nullable String[] mAutofillHints;
+
+ /** The type of the UI. */
+ private final @NonNull @Type String mType;
+
+ /** Whether the suggestion should be pinned or not. */
+ private final boolean mPinned;
+
+ /**
+ * @hide
+ */
+ private final @Nullable InlineSuggestion mTooltip;
+
+ /**
+ * Creates a new {@link InlineSuggestionInfo}, for testing purpose.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public static InlineSuggestionInfo newInlineSuggestionInfo(
+ @NonNull InlinePresentationSpec presentationSpec,
+ @NonNull @Source String source,
+ @SuppressLint("NullableCollection")
+ @Nullable String[] autofillHints, @NonNull @Type String type, boolean isPinned) {
+ return new InlineSuggestionInfo(presentationSpec, source, autofillHints, type, isPinned,
+ null);
+ }
+
+ /**
+ * Creates a new {@link InlineSuggestionInfo}, for testing purpose.
+ *
+ * @hide
+ */
+ @NonNull
+ public static InlineSuggestionInfo newInlineSuggestionInfo(
+ @NonNull InlinePresentationSpec presentationSpec,
+ @NonNull @Source String source,
+ @Nullable String[] autofillHints, @NonNull @Type String type, boolean isPinned,
+ @Nullable InlineSuggestion tooltip) {
+ return new InlineSuggestionInfo(presentationSpec, source, autofillHints, type, isPinned,
+ tooltip);
+ }
+
+
+
+
+
+ // Code below generated by codegen v1.0.22.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/inputmethod/InlineSuggestionInfo.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @android.annotation.StringDef(prefix = "SOURCE_", value = {
+ SOURCE_AUTOFILL,
+ SOURCE_PLATFORM
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface Source {}
+
+ /** @hide */
+ @android.annotation.StringDef(prefix = "TYPE_", value = {
+ TYPE_SUGGESTION,
+ TYPE_ACTION
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface Type {}
+
+ /**
+ * Creates a new InlineSuggestionInfo.
+ *
+ * @param inlinePresentationSpec
+ * The presentation spec to which the inflated suggestion view abides.
+ * @param source
+ * The source from which the suggestion is provided.
+ * @param autofillHints
+ * Hints for the type of data being suggested.
+ * @param type
+ * The type of the UI.
+ * @param pinned
+ * Whether the suggestion should be pinned or not.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public InlineSuggestionInfo(
+ @NonNull InlinePresentationSpec inlinePresentationSpec,
+ @NonNull @Source String source,
+ @Nullable String[] autofillHints,
+ @NonNull @Type String type,
+ boolean pinned,
+ @Nullable InlineSuggestion tooltip) {
+ this.mInlinePresentationSpec = inlinePresentationSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpec);
+ this.mSource = source;
+
+ if (!(java.util.Objects.equals(mSource, SOURCE_AUTOFILL))
+ && !(java.util.Objects.equals(mSource, SOURCE_PLATFORM))) {
+ throw new java.lang.IllegalArgumentException(
+ "source was " + mSource + " but must be one of: "
+ + "SOURCE_AUTOFILL(" + SOURCE_AUTOFILL + "), "
+ + "SOURCE_PLATFORM(" + SOURCE_PLATFORM + ")");
+ }
+
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSource);
+ this.mAutofillHints = autofillHints;
+ this.mType = type;
+
+ if (!(java.util.Objects.equals(mType, TYPE_SUGGESTION))
+ && !(java.util.Objects.equals(mType, TYPE_ACTION))) {
+ throw new java.lang.IllegalArgumentException(
+ "type was " + mType + " but must be one of: "
+ + "TYPE_SUGGESTION(" + TYPE_SUGGESTION + "), "
+ + "TYPE_ACTION(" + TYPE_ACTION + ")");
+ }
+
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mType);
+ this.mPinned = pinned;
+ this.mTooltip = tooltip;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The presentation spec to which the inflated suggestion view abides.
+ */
+ @DataClass.Generated.Member
+ public @NonNull InlinePresentationSpec getInlinePresentationSpec() {
+ return mInlinePresentationSpec;
+ }
+
+ /**
+ * The source from which the suggestion is provided.
+ */
+ @DataClass.Generated.Member
+ public @NonNull @Source String getSource() {
+ return mSource;
+ }
+
+ /**
+ * Hints for the type of data being suggested.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String[] getAutofillHints() {
+ return mAutofillHints;
+ }
+
+ /**
+ * The type of the UI.
+ */
+ @DataClass.Generated.Member
+ public @NonNull @Type String getType() {
+ return mType;
+ }
+
+ /**
+ * Whether the suggestion should be pinned or not.
+ */
+ @DataClass.Generated.Member
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ /**
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineSuggestion getTooltip() {
+ return mTooltip;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InlineSuggestionInfo { " +
+ "inlinePresentationSpec = " + mInlinePresentationSpec + ", " +
+ "source = " + mSource + ", " +
+ "autofillHints = " + java.util.Arrays.toString(mAutofillHints) + ", " +
+ "type = " + mType + ", " +
+ "pinned = " + mPinned + ", " +
+ "tooltip = " + mTooltip +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(InlineSuggestionInfo other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InlineSuggestionInfo that = (InlineSuggestionInfo) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Objects.equals(mInlinePresentationSpec, that.mInlinePresentationSpec)
+ && java.util.Objects.equals(mSource, that.mSource)
+ && java.util.Arrays.equals(mAutofillHints, that.mAutofillHints)
+ && java.util.Objects.equals(mType, that.mType)
+ && mPinned == that.mPinned
+ && java.util.Objects.equals(mTooltip, that.mTooltip);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlinePresentationSpec);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mSource);
+ _hash = 31 * _hash + java.util.Arrays.hashCode(mAutofillHints);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mType);
+ _hash = 31 * _hash + Boolean.hashCode(mPinned);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mTooltip);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mPinned) flg |= 0x10;
+ if (mAutofillHints != null) flg |= 0x4;
+ if (mTooltip != null) flg |= 0x20;
+ dest.writeByte(flg);
+ dest.writeTypedObject(mInlinePresentationSpec, flags);
+ dest.writeString(mSource);
+ if (mAutofillHints != null) dest.writeStringArray(mAutofillHints);
+ dest.writeString(mType);
+ if (mTooltip != null) dest.writeTypedObject(mTooltip, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InlineSuggestionInfo(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean pinned = (flg & 0x10) != 0;
+ InlinePresentationSpec inlinePresentationSpec = (InlinePresentationSpec) in.readTypedObject(InlinePresentationSpec.CREATOR);
+ String source = in.readString();
+ String[] autofillHints = (flg & 0x4) == 0 ? null : in.createStringArray();
+ String type = in.readString();
+ InlineSuggestion tooltip = (flg & 0x20) == 0 ? null : (InlineSuggestion) in.readTypedObject(InlineSuggestion.CREATOR);
+
+ this.mInlinePresentationSpec = inlinePresentationSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpec);
+ this.mSource = source;
+
+ if (!(java.util.Objects.equals(mSource, SOURCE_AUTOFILL))
+ && !(java.util.Objects.equals(mSource, SOURCE_PLATFORM))) {
+ throw new java.lang.IllegalArgumentException(
+ "source was " + mSource + " but must be one of: "
+ + "SOURCE_AUTOFILL(" + SOURCE_AUTOFILL + "), "
+ + "SOURCE_PLATFORM(" + SOURCE_PLATFORM + ")");
+ }
+
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSource);
+ this.mAutofillHints = autofillHints;
+ this.mType = type;
+
+ if (!(java.util.Objects.equals(mType, TYPE_SUGGESTION))
+ && !(java.util.Objects.equals(mType, TYPE_ACTION))) {
+ throw new java.lang.IllegalArgumentException(
+ "type was " + mType + " but must be one of: "
+ + "TYPE_SUGGESTION(" + TYPE_SUGGESTION + "), "
+ + "TYPE_ACTION(" + TYPE_ACTION + ")");
+ }
+
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mType);
+ this.mPinned = pinned;
+ this.mTooltip = tooltip;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InlineSuggestionInfo> CREATOR
+ = new Parcelable.Creator<InlineSuggestionInfo>() {
+ @Override
+ public InlineSuggestionInfo[] newArray(int size) {
+ return new InlineSuggestionInfo[size];
+ }
+
+ @Override
+ public InlineSuggestionInfo createFromParcel(@NonNull android.os.Parcel in) {
+ return new InlineSuggestionInfo(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1614287616672L,
+ codegenVersion = "1.0.22",
+ sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionInfo.java",
+ inputSignatures = "public static final @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String SOURCE_AUTOFILL\npublic static final @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String SOURCE_PLATFORM\npublic static final @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String TYPE_SUGGESTION\npublic static final @android.annotation.SuppressLint @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String TYPE_ACTION\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final @android.annotation.NonNull @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String mSource\nprivate final @android.annotation.Nullable java.lang.String[] mAutofillHints\nprivate final @android.annotation.NonNull @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String mType\nprivate final boolean mPinned\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestion mTooltip\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(android.widget.inline.InlinePresentationSpec,java.lang.String,java.lang.String[],java.lang.String,boolean)\npublic static @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(android.widget.inline.InlinePresentationSpec,java.lang.String,java.lang.String[],java.lang.String,boolean,android.view.inputmethod.InlineSuggestion)\nclass InlineSuggestionInfo extends java.lang.Object implements [android.os.Parcelable]\[email protected](genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/inputmethod/InlineSuggestionsRequest.java b/android/view/inputmethod/InlineSuggestionsRequest.java
new file mode 100644
index 0000000..1eb1a93
--- /dev/null
+++ b/android/view/inputmethod/InlineSuggestionsRequest.java
@@ -0,0 +1,700 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.Display;
+import android.widget.inline.InlinePresentationSpec;
+
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Preconditions;
+import com.android.internal.widget.InlinePresentationStyleUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents an inline suggestion request made by one app to get suggestions from the
+ * other source. See {@link InlineSuggestion} for more information.
+ */
+@DataClass(genEqualsHashCode = true, genToString = true, genBuilder = true)
+public final class InlineSuggestionsRequest implements Parcelable {
+
+ /** Constant used to indicate not putting a cap on the number of suggestions to return. */
+ public static final int SUGGESTION_COUNT_UNLIMITED = Integer.MAX_VALUE;
+
+ /**
+ * Max number of suggestions expected from the response. It must be a positive value.
+ * Defaults to {@code SUGGESTION_COUNT_UNLIMITED} if not set.
+ *
+ * <p>In practice, it is recommended that the max suggestion count does not exceed <b>5</b>
+ * for performance reasons.</p>
+ */
+ private final int mMaxSuggestionCount;
+
+ /**
+ * The {@link InlinePresentationSpec} for each suggestion in the response. If the max suggestion
+ * count is larger than the number of specs in the list, then the last spec is used for the
+ * remainder of the suggestions. The list should not be empty.
+ */
+ private final @NonNull List<InlinePresentationSpec> mInlinePresentationSpecs;
+
+ /**
+ * The package name of the app that requests for the inline suggestions and will host the
+ * embedded suggestion views. The app does not have to set the value for the field because
+ * it'll be set by the system for safety reasons.
+ */
+ private @NonNull String mHostPackageName;
+
+ /**
+ * The IME provided locales for the request. If non-empty, the inline suggestions should
+ * return languages from the supported locales. If not provided, it'll default to be empty if
+ * target SDK is S or above, and default to system locale otherwise.
+ *
+ * <p>Note for Autofill Providers: It is <b>recommended</b> for the returned inline suggestions
+ * to have one locale to guarantee consistent UI rendering.</p>
+ */
+ private @NonNull LocaleList mSupportedLocales;
+
+ /**
+ * The extras state propagated from the IME to pass extra data.
+ *
+ * <p>Note: There should be no remote objects in the bundle, all included remote objects will
+ * be removed from the bundle before transmission.</p>
+ */
+ private @NonNull Bundle mExtras;
+
+ /**
+ * The host input token of the IME that made the request. This will be set by the system for
+ * safety reasons.
+ *
+ * @hide
+ */
+ private @Nullable IBinder mHostInputToken;
+
+ /**
+ * The host display id of the IME that made the request. This will be set by the system for
+ * safety reasons.
+ *
+ * @hide
+ */
+ private int mHostDisplayId;
+
+ /**
+ * Specifies the UI specification for the inline suggestion tooltip in the response.
+ */
+ private @Nullable InlinePresentationSpec mInlineTooltipPresentationSpec;
+
+ /**
+ * @hide
+ * @see {@link #mHostInputToken}.
+ */
+ public void setHostInputToken(IBinder hostInputToken) {
+ mHostInputToken = hostInputToken;
+ }
+
+ private boolean extrasEquals(@NonNull Bundle extras) {
+ return InlinePresentationStyleUtils.bundleEquals(mExtras, extras);
+ }
+
+ // TODO(b/149609075): remove once IBinder parcelling is natively supported
+ private void parcelHostInputToken(@NonNull Parcel parcel, int flags) {
+ parcel.writeStrongBinder(mHostInputToken);
+ }
+
+ // TODO(b/149609075): remove once IBinder parcelling is natively supported
+ private @Nullable IBinder unparcelHostInputToken(Parcel parcel) {
+ return parcel.readStrongBinder();
+ }
+
+ /**
+ * @hide
+ * @see {@link #mHostDisplayId}.
+ */
+ public void setHostDisplayId(int hostDisplayId) {
+ mHostDisplayId = hostDisplayId;
+ }
+
+ private void onConstructed() {
+ Preconditions.checkState(!mInlinePresentationSpecs.isEmpty());
+ Preconditions.checkState(mMaxSuggestionCount >= mInlinePresentationSpecs.size());
+ }
+
+ /**
+ * Removes the remote objects from the bundles within the {@Code mExtras} and the
+ * {@code mInlinePresentationSpecs}.
+ *
+ * @hide
+ */
+ public void filterContentTypes() {
+ InlinePresentationStyleUtils.filterContentTypes(mExtras);
+ for (int i = 0; i < mInlinePresentationSpecs.size(); i++) {
+ mInlinePresentationSpecs.get(i).filterContentTypes();
+ }
+
+ if (mInlineTooltipPresentationSpec != null) {
+ mInlineTooltipPresentationSpec.filterContentTypes();
+ }
+ }
+
+ private static int defaultMaxSuggestionCount() {
+ return SUGGESTION_COUNT_UNLIMITED;
+ }
+
+ private static String defaultHostPackageName() {
+ return ActivityThread.currentPackageName();
+ }
+
+ private static InlinePresentationSpec defaultInlineTooltipPresentationSpec() {
+ return null;
+ }
+
+ /**
+ * The {@link InlineSuggestionsRequest#getSupportedLocales()} now returns empty locale list when
+ * it's not set, instead of the default system locale.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
+ private static final long IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY = 169273070L;
+
+ private static LocaleList defaultSupportedLocales() {
+ if (CompatChanges.isChangeEnabled(IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY)) {
+ return LocaleList.getEmptyLocaleList();
+ }
+ return LocaleList.getDefault();
+ }
+
+ @Nullable
+ private static IBinder defaultHostInputToken() {
+ return null;
+ }
+
+ @Nullable
+ private static int defaultHostDisplayId() {
+ return Display.INVALID_DISPLAY;
+ }
+
+ @NonNull
+ private static Bundle defaultExtras() {
+ return Bundle.EMPTY;
+ }
+
+ /** @hide */
+ abstract static class BaseBuilder {
+ abstract Builder setInlinePresentationSpecs(
+ @NonNull List<android.widget.inline.InlinePresentationSpec> specs);
+
+ abstract Builder setHostPackageName(@Nullable String value);
+
+ abstract Builder setHostInputToken(IBinder hostInputToken);
+
+ abstract Builder setHostDisplayId(int value);
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/./frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ /* package-private */ InlineSuggestionsRequest(
+ int maxSuggestionCount,
+ @NonNull List<InlinePresentationSpec> inlinePresentationSpecs,
+ @NonNull String hostPackageName,
+ @NonNull LocaleList supportedLocales,
+ @NonNull Bundle extras,
+ @Nullable IBinder hostInputToken,
+ int hostDisplayId,
+ @Nullable InlinePresentationSpec inlineTooltipPresentationSpec) {
+ this.mMaxSuggestionCount = maxSuggestionCount;
+ this.mInlinePresentationSpecs = inlinePresentationSpecs;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpecs);
+ this.mHostPackageName = hostPackageName;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHostPackageName);
+ this.mSupportedLocales = supportedLocales;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSupportedLocales);
+ this.mExtras = extras;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mExtras);
+ this.mHostInputToken = hostInputToken;
+ this.mHostDisplayId = hostDisplayId;
+ this.mInlineTooltipPresentationSpec = inlineTooltipPresentationSpec;
+
+ onConstructed();
+ }
+
+ /**
+ * Max number of suggestions expected from the response. It must be a positive value.
+ * Defaults to {@code SUGGESTION_COUNT_UNLIMITED} if not set.
+ *
+ * <p>In practice, it is recommended that the max suggestion count does not exceed <b>5</b>
+ * for performance reasons.</p>
+ */
+ @DataClass.Generated.Member
+ public int getMaxSuggestionCount() {
+ return mMaxSuggestionCount;
+ }
+
+ /**
+ * The {@link InlinePresentationSpec} for each suggestion in the response. If the max suggestion
+ * count is larger than the number of specs in the list, then the last spec is used for the
+ * remainder of the suggestions. The list should not be empty.
+ */
+ @DataClass.Generated.Member
+ public @NonNull List<InlinePresentationSpec> getInlinePresentationSpecs() {
+ return mInlinePresentationSpecs;
+ }
+
+ /**
+ * The package name of the app that requests for the inline suggestions and will host the
+ * embedded suggestion views. The app does not have to set the value for the field because
+ * it'll be set by the system for safety reasons.
+ */
+ @DataClass.Generated.Member
+ public @NonNull String getHostPackageName() {
+ return mHostPackageName;
+ }
+
+ /**
+ * The IME provided locales for the request. If non-empty, the inline suggestions should
+ * return languages from the supported locales. If not provided, it'll default to be empty if
+ * target SDK is S or above, and default to system locale otherwise.
+ *
+ * <p>Note for Autofill Providers: It is <b>recommended</b> for the returned inline suggestions
+ * to have one locale to guarantee consistent UI rendering.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull LocaleList getSupportedLocales() {
+ return mSupportedLocales;
+ }
+
+ /**
+ * The extras state propagated from the IME to pass extra data.
+ *
+ * <p>Note: There should be no remote objects in the bundle, all included remote objects will
+ * be removed from the bundle before transmission.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * The host input token of the IME that made the request. This will be set by the system for
+ * safety reasons.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable IBinder getHostInputToken() {
+ return mHostInputToken;
+ }
+
+ /**
+ * The host display id of the IME that made the request. This will be set by the system for
+ * safety reasons.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public int getHostDisplayId() {
+ return mHostDisplayId;
+ }
+
+ /**
+ * Specifies the UI specification for the inline suggestion tooltip in the response.
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlinePresentationSpec getInlineTooltipPresentationSpec() {
+ return mInlineTooltipPresentationSpec;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InlineSuggestionsRequest { " +
+ "maxSuggestionCount = " + mMaxSuggestionCount + ", " +
+ "inlinePresentationSpecs = " + mInlinePresentationSpecs + ", " +
+ "hostPackageName = " + mHostPackageName + ", " +
+ "supportedLocales = " + mSupportedLocales + ", " +
+ "extras = " + mExtras + ", " +
+ "hostInputToken = " + mHostInputToken + ", " +
+ "hostDisplayId = " + mHostDisplayId + ", " +
+ "inlineTooltipPresentationSpec = " + mInlineTooltipPresentationSpec +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(InlineSuggestionsRequest other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InlineSuggestionsRequest that = (InlineSuggestionsRequest) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && mMaxSuggestionCount == that.mMaxSuggestionCount
+ && java.util.Objects.equals(mInlinePresentationSpecs, that.mInlinePresentationSpecs)
+ && java.util.Objects.equals(mHostPackageName, that.mHostPackageName)
+ && java.util.Objects.equals(mSupportedLocales, that.mSupportedLocales)
+ && extrasEquals(that.mExtras)
+ && java.util.Objects.equals(mHostInputToken, that.mHostInputToken)
+ && mHostDisplayId == that.mHostDisplayId
+ && java.util.Objects.equals(mInlineTooltipPresentationSpec, that.mInlineTooltipPresentationSpec);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + mMaxSuggestionCount;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlinePresentationSpecs);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mHostPackageName);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mSupportedLocales);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mExtras);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mHostInputToken);
+ _hash = 31 * _hash + mHostDisplayId;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlineTooltipPresentationSpec);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ int flg = 0;
+ if (mHostInputToken != null) flg |= 0x20;
+ if (mInlineTooltipPresentationSpec != null) flg |= 0x80;
+ dest.writeInt(flg);
+ dest.writeInt(mMaxSuggestionCount);
+ dest.writeParcelableList(mInlinePresentationSpecs, flags);
+ dest.writeString(mHostPackageName);
+ dest.writeTypedObject(mSupportedLocales, flags);
+ dest.writeBundle(mExtras);
+ parcelHostInputToken(dest, flags);
+ dest.writeInt(mHostDisplayId);
+ if (mInlineTooltipPresentationSpec != null) dest.writeTypedObject(mInlineTooltipPresentationSpec, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InlineSuggestionsRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ int flg = in.readInt();
+ int maxSuggestionCount = in.readInt();
+ List<InlinePresentationSpec> inlinePresentationSpecs = new ArrayList<>();
+ in.readParcelableList(inlinePresentationSpecs, InlinePresentationSpec.class.getClassLoader());
+ String hostPackageName = in.readString();
+ LocaleList supportedLocales = (LocaleList) in.readTypedObject(LocaleList.CREATOR);
+ Bundle extras = in.readBundle();
+ IBinder hostInputToken = unparcelHostInputToken(in);
+ int hostDisplayId = in.readInt();
+ InlinePresentationSpec inlineTooltipPresentationSpec = (flg & 0x80) == 0 ? null : (InlinePresentationSpec) in.readTypedObject(InlinePresentationSpec.CREATOR);
+
+ this.mMaxSuggestionCount = maxSuggestionCount;
+ this.mInlinePresentationSpecs = inlinePresentationSpecs;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpecs);
+ this.mHostPackageName = hostPackageName;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mHostPackageName);
+ this.mSupportedLocales = supportedLocales;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSupportedLocales);
+ this.mExtras = extras;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mExtras);
+ this.mHostInputToken = hostInputToken;
+ this.mHostDisplayId = hostDisplayId;
+ this.mInlineTooltipPresentationSpec = inlineTooltipPresentationSpec;
+
+ onConstructed();
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InlineSuggestionsRequest> CREATOR
+ = new Parcelable.Creator<InlineSuggestionsRequest>() {
+ @Override
+ public InlineSuggestionsRequest[] newArray(int size) {
+ return new InlineSuggestionsRequest[size];
+ }
+
+ @Override
+ public InlineSuggestionsRequest createFromParcel(@NonNull Parcel in) {
+ return new InlineSuggestionsRequest(in);
+ }
+ };
+
+ /**
+ * A builder for {@link InlineSuggestionsRequest}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private int mMaxSuggestionCount;
+ private @NonNull List<InlinePresentationSpec> mInlinePresentationSpecs;
+ private @NonNull String mHostPackageName;
+ private @NonNull LocaleList mSupportedLocales;
+ private @NonNull Bundle mExtras;
+ private @Nullable IBinder mHostInputToken;
+ private int mHostDisplayId;
+ private @Nullable InlinePresentationSpec mInlineTooltipPresentationSpec;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param inlinePresentationSpecs
+ * The {@link InlinePresentationSpec} for each suggestion in the response. If the max suggestion
+ * count is larger than the number of specs in the list, then the last spec is used for the
+ * remainder of the suggestions. The list should not be empty.
+ */
+ public Builder(
+ @NonNull List<InlinePresentationSpec> inlinePresentationSpecs) {
+ mInlinePresentationSpecs = inlinePresentationSpecs;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpecs);
+ }
+
+ /**
+ * Max number of suggestions expected from the response. It must be a positive value.
+ * Defaults to {@code SUGGESTION_COUNT_UNLIMITED} if not set.
+ *
+ * <p>In practice, it is recommended that the max suggestion count does not exceed <b>5</b>
+ * for performance reasons.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setMaxSuggestionCount(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mMaxSuggestionCount = value;
+ return this;
+ }
+
+ /**
+ * The {@link InlinePresentationSpec} for each suggestion in the response. If the max suggestion
+ * count is larger than the number of specs in the list, then the last spec is used for the
+ * remainder of the suggestions. The list should not be empty.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setInlinePresentationSpecs(@NonNull List<InlinePresentationSpec> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mInlinePresentationSpecs = value;
+ return this;
+ }
+
+ /** @see #setInlinePresentationSpecs */
+ @DataClass.Generated.Member
+ public @NonNull Builder addInlinePresentationSpecs(@NonNull InlinePresentationSpec value) {
+ // You can refine this method's name by providing item's singular name, e.g.:
+ // @DataClass.PluralOf("item")) mItems = ...
+
+ if (mInlinePresentationSpecs == null) setInlinePresentationSpecs(new ArrayList<>());
+ mInlinePresentationSpecs.add(value);
+ return this;
+ }
+
+ /**
+ * The package name of the app that requests for the inline suggestions and will host the
+ * embedded suggestion views. The app does not have to set the value for the field because
+ * it'll be set by the system for safety reasons.
+ */
+ @DataClass.Generated.Member
+ @Override
+ @NonNull Builder setHostPackageName(@NonNull String value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mHostPackageName = value;
+ return this;
+ }
+
+ /**
+ * The IME provided locales for the request. If non-empty, the inline suggestions should
+ * return languages from the supported locales. If not provided, it'll default to be empty if
+ * target SDK is S or above, and default to system locale otherwise.
+ *
+ * <p>Note for Autofill Providers: It is <b>recommended</b> for the returned inline suggestions
+ * to have one locale to guarantee consistent UI rendering.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setSupportedLocales(@NonNull LocaleList value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8;
+ mSupportedLocales = value;
+ return this;
+ }
+
+ /**
+ * The extras state propagated from the IME to pass extra data.
+ *
+ * <p>Note: There should be no remote objects in the bundle, all included remote objects will
+ * be removed from the bundle before transmission.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setExtras(@NonNull Bundle value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x10;
+ mExtras = value;
+ return this;
+ }
+
+ /**
+ * The host input token of the IME that made the request. This will be set by the system for
+ * safety reasons.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ @Override
+ @NonNull Builder setHostInputToken(@NonNull IBinder value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x20;
+ mHostInputToken = value;
+ return this;
+ }
+
+ /**
+ * The host display id of the IME that made the request. This will be set by the system for
+ * safety reasons.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ @Override
+ @NonNull Builder setHostDisplayId(int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x40;
+ mHostDisplayId = value;
+ return this;
+ }
+
+ /**
+ * Specifies the UI specification for the inline suggestion tooltip in the response.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setInlineTooltipPresentationSpec(@NonNull InlinePresentationSpec value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x80;
+ mInlineTooltipPresentationSpec = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull InlineSuggestionsRequest build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x100; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ mMaxSuggestionCount = defaultMaxSuggestionCount();
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mHostPackageName = defaultHostPackageName();
+ }
+ if ((mBuilderFieldsSet & 0x8) == 0) {
+ mSupportedLocales = defaultSupportedLocales();
+ }
+ if ((mBuilderFieldsSet & 0x10) == 0) {
+ mExtras = defaultExtras();
+ }
+ if ((mBuilderFieldsSet & 0x20) == 0) {
+ mHostInputToken = defaultHostInputToken();
+ }
+ if ((mBuilderFieldsSet & 0x40) == 0) {
+ mHostDisplayId = defaultHostDisplayId();
+ }
+ if ((mBuilderFieldsSet & 0x80) == 0) {
+ mInlineTooltipPresentationSpec = defaultInlineTooltipPresentationSpec();
+ }
+ InlineSuggestionsRequest o = new InlineSuggestionsRequest(
+ mMaxSuggestionCount,
+ mInlinePresentationSpecs,
+ mHostPackageName,
+ mSupportedLocales,
+ mExtras,
+ mHostInputToken,
+ mHostDisplayId,
+ mInlineTooltipPresentationSpec);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x100) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1621415989607L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java",
+ inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List<android.widget.inline.InlinePresentationSpec> mInlinePresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.NonNull android.os.Bundle mExtras\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate int mHostDisplayId\nprivate @android.annotation.Nullable android.widget.inline.InlinePresentationSpec mInlineTooltipPresentationSpec\nprivate static final @android.compat.annotation.ChangeId @android.compat.annotation.EnabledSince long IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY\npublic void setHostInputToken(android.os.IBinder)\nprivate boolean extrasEquals(android.os.Bundle)\nprivate void parcelHostInputToken(android.os.Parcel,int)\nprivate @android.annotation.Nullable android.os.IBinder unparcelHostInputToken(android.os.Parcel)\npublic void setHostDisplayId(int)\nprivate void onConstructed()\npublic void filterContentTypes()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.widget.inline.InlinePresentationSpec defaultInlineTooltipPresentationSpec()\nprivate static android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable int defaultHostDisplayId()\nprivate static @android.annotation.NonNull android.os.Bundle defaultExtras()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []\[email protected](genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List<android.widget.inline.InlinePresentationSpec>)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/inputmethod/InlineSuggestionsResponse.java b/android/view/inputmethod/InlineSuggestionsResponse.java
new file mode 100644
index 0000000..b393c67
--- /dev/null
+++ b/android/view/inputmethod/InlineSuggestionsResponse.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents an inline suggestion response. See {@link InlineSuggestion} for more
+ * information.
+ */
+@DataClass(genEqualsHashCode = true, genToString = true, genHiddenConstructor = true)
+public final class InlineSuggestionsResponse implements Parcelable {
+ /**
+ * List of {@link InlineSuggestion}s returned as a part of this response.
+ *
+ * <p>When the host app requests to inflate this <b>ordered</b> list of inline suggestions by
+ * calling {@link InlineSuggestion#inflate}, it is the host's responsibility to track the
+ * order of the inflated {@link android.view.View}s. These views are to be added in
+ * order to the view hierarchy, because the inflation calls will return asynchronously.</p>
+ *
+ * <p>The inflation ordering does not apply to the pinned icon.</p>
+ */
+ @NonNull
+ private final List<InlineSuggestion> mInlineSuggestions;
+
+ /**
+ * Creates a new {@link InlineSuggestionsResponse}, for testing purpose.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public static InlineSuggestionsResponse newInlineSuggestionsResponse(
+ @NonNull List<InlineSuggestion> inlineSuggestions) {
+ return new InlineSuggestionsResponse(inlineSuggestions);
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new InlineSuggestionsResponse.
+ *
+ * @param inlineSuggestions
+ * List of {@link InlineSuggestion}s returned as a part of this response.
+ *
+ * <p>When the host app requests to inflate this <b>ordered</b> list of inline suggestions by
+ * calling {@link InlineSuggestion#inflate}, it is the host's responsibility to track the
+ * order of the inflated {@link android.view.View}s. These views are to be added in
+ * order to the view hierarchy, because the inflation calls will return asynchronously.</p>
+ *
+ * <p>The inflation ordering does not apply to the pinned icon.</p>
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public InlineSuggestionsResponse(
+ @NonNull List<InlineSuggestion> inlineSuggestions) {
+ this.mInlineSuggestions = inlineSuggestions;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlineSuggestions);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * List of {@link InlineSuggestion}s returned as a part of this response.
+ *
+ * <p>When the host app requests to inflate this <b>ordered</b> list of inline suggestions by
+ * calling {@link InlineSuggestion#inflate}, it is the host's responsibility to track the
+ * order of the inflated {@link android.view.View}s. These views are to be added in
+ * order to the view hierarchy, because the inflation calls will return asynchronously.</p>
+ *
+ * <p>The inflation ordering does not apply to the pinned icon.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull List<InlineSuggestion> getInlineSuggestions() {
+ return mInlineSuggestions;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InlineSuggestionsResponse { " +
+ "inlineSuggestions = " + mInlineSuggestions +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(InlineSuggestionsResponse other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InlineSuggestionsResponse that = (InlineSuggestionsResponse) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Objects.equals(mInlineSuggestions, that.mInlineSuggestions);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlineSuggestions);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeParcelableList(mInlineSuggestions, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InlineSuggestionsResponse(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ List<InlineSuggestion> inlineSuggestions = new ArrayList<>();
+ in.readParcelableList(inlineSuggestions, InlineSuggestion.class.getClassLoader());
+
+ this.mInlineSuggestions = inlineSuggestions;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlineSuggestions);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InlineSuggestionsResponse> CREATOR
+ = new Parcelable.Creator<InlineSuggestionsResponse>() {
+ @Override
+ public InlineSuggestionsResponse[] newArray(int size) {
+ return new InlineSuggestionsResponse[size];
+ }
+
+ @Override
+ public InlineSuggestionsResponse createFromParcel(@NonNull android.os.Parcel in) {
+ return new InlineSuggestionsResponse(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1595891876037L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsResponse.java",
+ inputSignatures = "private final @android.annotation.NonNull java.util.List<android.view.inputmethod.InlineSuggestion> mInlineSuggestions\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestionsResponse newInlineSuggestionsResponse(java.util.List<android.view.inputmethod.InlineSuggestion>)\nclass InlineSuggestionsResponse extends java.lang.Object implements [android.os.Parcelable]\[email protected](genEqualsHashCode=true, genToString=true, genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/inputmethod/InputBinding.java b/android/view/inputmethod/InputBinding.java
new file mode 100644
index 0000000..2bfeb5a
--- /dev/null
+++ b/android/view/inputmethod/InputBinding.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2007-2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information given to an {@link InputMethod} about a client connecting
+ * to it.
+ */
+public final class InputBinding implements Parcelable {
+ static final String TAG = "InputBinding";
+
+ /**
+ * The connection back to the client.
+ */
+ final InputConnection mConnection;
+
+ /**
+ * A remotable token for the connection back to the client.
+ */
+ final IBinder mConnectionToken;
+
+ /**
+ * The UID where this binding came from.
+ */
+ final int mUid;
+
+ /**
+ * The PID where this binding came from.
+ */
+ final int mPid;
+
+ /**
+ * Constructor.
+ *
+ * @param conn The interface for communicating back with the application.
+ * @param connToken A remoteable token for communicating across processes.
+ * @param uid The user id of the client of this binding.
+ * @param pid The process id of where the binding came from.
+ */
+ public InputBinding(InputConnection conn, IBinder connToken,
+ int uid, int pid) {
+ mConnection = conn;
+ mConnectionToken = connToken;
+ mUid = uid;
+ mPid = pid;
+ }
+
+ /**
+ * Constructor from an existing InputBinding taking a new local input
+ * connection interface.
+ *
+ * @param conn The new connection interface.
+ * @param binding Existing binding to copy.
+ */
+ public InputBinding(InputConnection conn, InputBinding binding) {
+ mConnection = conn;
+ mConnectionToken = binding.getConnectionToken();
+ mUid = binding.getUid();
+ mPid = binding.getPid();
+ }
+
+ InputBinding(Parcel source) {
+ mConnection = null;
+ mConnectionToken = source.readStrongBinder();
+ mUid = source.readInt();
+ mPid = source.readInt();
+ }
+
+ /**
+ * Return the connection for interacting back with the application.
+ */
+ public InputConnection getConnection() {
+ return mConnection;
+ }
+
+ /**
+ * Return the token for the connection back to the application. You can
+ * not use this directly, it must be converted to a {@link InputConnection}
+ * for you.
+ */
+ public IBinder getConnectionToken() {
+ return mConnectionToken;
+ }
+
+ /**
+ * Return the user id of the client associated with this binding.
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Return the process id where this binding came from.
+ */
+ public int getPid() {
+ return mPid;
+ }
+
+ @Override
+ public String toString() {
+ return "InputBinding{" + mConnectionToken
+ + " / uid " + mUid + " / pid " + mPid + "}";
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mConnectionToken);
+ dest.writeInt(mUid);
+ dest.writeInt(mPid);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<InputBinding> CREATOR = new Parcelable.Creator<InputBinding>() {
+ public InputBinding createFromParcel(Parcel source) {
+ return new InputBinding(source);
+ }
+
+ public InputBinding[] newArray(int size) {
+ return new InputBinding[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/InputConnection.java b/android/view/inputmethod/InputConnection.java
new file mode 100644
index 0000000..5f036a3
--- /dev/null
+++ b/android/view/inputmethod/InputConnection.java
@@ -0,0 +1,1024 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.inputmethodservice.InputMethodService;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The InputConnection interface is the communication channel from an
+ * {@link InputMethod} back to the application that is receiving its
+ * input. It is used to perform such things as reading text around the
+ * cursor, committing text to the text box, and sending raw key events
+ * to the application.
+ *
+ * <p>Starting from API Level {@link android.os.Build.VERSION_CODES#N},
+ * the system can deal with the situation where the application directly
+ * implements this class but one or more of the following methods are
+ * not implemented.</p>
+ * <ul>
+ * <li>{@link #getSelectedText(int)}, which was introduced in
+ * {@link android.os.Build.VERSION_CODES#GINGERBREAD}.</li>
+ * <li>{@link #setComposingRegion(int, int)}, which was introduced
+ * in {@link android.os.Build.VERSION_CODES#GINGERBREAD}.</li>
+ * <li>{@link #commitCorrection(CorrectionInfo)}, which was introduced
+ * in {@link android.os.Build.VERSION_CODES#HONEYCOMB}.</li>
+ * <li>{@link #requestCursorUpdates(int)}, which was introduced in
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP}.</li>
+ * <li>{@link #deleteSurroundingTextInCodePoints(int, int)}, which
+ * was introduced in {@link android.os.Build.VERSION_CODES#N}.</li>
+ * <li>{@link #getHandler()}, which was introduced in
+ * {@link android.os.Build.VERSION_CODES#N}.</li>
+ * <li>{@link #closeConnection()}, which was introduced in
+ * {@link android.os.Build.VERSION_CODES#N}.</li>
+ * <li>{@link #commitContent(InputContentInfo, int, Bundle)}, which was
+ * introduced in {@link android.os.Build.VERSION_CODES#N_MR1}.</li>
+ * </ul>
+ *
+ * <h3>Implementing an IME or an editor</h3>
+ * <p>Text input is the result of the synergy of two essential components:
+ * an Input Method Engine (IME) and an editor. The IME can be a
+ * software keyboard, a handwriting interface, an emoji palette, a
+ * speech-to-text engine, and so on. There are typically several IMEs
+ * installed on any given Android device. In Android, IMEs extend
+ * {@link android.inputmethodservice.InputMethodService}.
+ * For more information about how to create an IME, see the
+ * <a href="{@docRoot}guide/topics/text/creating-input-method.html">
+ * Creating an input method</a> guide.
+ *
+ * The editor is the component that receives text and displays it.
+ * Typically, this is an {@link android.widget.EditText} instance, but
+ * some applications may choose to implement their own editor for
+ * various reasons. This is a large and complicated task, and an
+ * application that does this needs to make sure the behavior is
+ * consistent with standard EditText behavior in Android. An editor
+ * needs to interact with the IME, receiving commands through
+ * this InputConnection interface, and sending commands through
+ * {@link android.view.inputmethod.InputMethodManager}. An editor
+ * should start by implementing
+ * {@link android.view.View#onCreateInputConnection(EditorInfo)}
+ * to return its own input connection.</p>
+ *
+ * <p>If you are implementing your own IME, you will need to call the
+ * methods in this interface to interact with the application. Be sure
+ * to test your IME with a wide range of applications, including
+ * browsers and rich text editors, as some may have peculiarities you
+ * need to deal with. Remember your IME may not be the only source of
+ * changes on the text, and try to be as conservative as possible in
+ * the data you send and as liberal as possible in the data you
+ * receive.</p>
+ *
+ * <p>If you are implementing your own editor, you will probably need
+ * to provide your own subclass of {@link BaseInputConnection} to
+ * answer to the commands from IMEs. Please be sure to test your
+ * editor with as many IMEs as you can as their behavior can vary a
+ * lot. Also be sure to test with various languages, including CJK
+ * languages and right-to-left languages like Arabic, as these may
+ * have different input requirements. When in doubt about the
+ * behavior you should adopt for a particular call, please mimic the
+ * default TextView implementation in the latest Android version, and
+ * if you decide to drift from it, please consider carefully that
+ * inconsistencies in text editor behavior is almost universally felt
+ * as a bad thing by users.</p>
+ *
+ * <h3>Cursors, selections and compositions</h3>
+ * <p>In Android, the cursor and the selection are one and the same
+ * thing. A "cursor" is just the special case of a zero-sized
+ * selection. As such, this documentation uses them
+ * interchangeably. Any method acting "before the cursor" would act
+ * before the start of the selection if there is one, and any method
+ * acting "after the cursor" would act after the end of the
+ * selection.</p>
+ *
+ * <p>An editor needs to be able to keep track of a currently
+ * "composing" region, like the standard edition widgets do. The
+ * composition is marked in a specific style: see
+ * {@link android.text.Spanned#SPAN_COMPOSING}. IMEs use this to help
+ * the user keep track of what part of the text they are currently
+ * focusing on, and interact with the editor using
+ * {@link InputConnection#setComposingText(CharSequence, int)},
+ * {@link InputConnection#setComposingRegion(int, int)} and
+ * {@link InputConnection#finishComposingText()}.
+ * The composing region and the selection are completely independent
+ * of each other, and the IME may use them however they see fit.</p>
+ */
+public interface InputConnection {
+ /** @hide */
+ @IntDef(flag = true, prefix = { "GET_TEXT_" }, value = {
+ GET_TEXT_WITH_STYLES,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface GetTextType {}
+
+ /**
+ * Flag for use with {@link #getTextAfterCursor}, {@link #getTextBeforeCursor} and
+ * {@link #getSurroundingText} to have style information returned along with the text. If not
+ * set, {@link #getTextAfterCursor} sends only the raw text, without style or other spans. If
+ * set, it may return a complex CharSequence of both text and style spans.
+ * <strong>Editor authors</strong>: you should strive to send text with styles if possible, but
+ * it is not required.
+ */
+ int GET_TEXT_WITH_STYLES = 0x0001;
+
+ /**
+ * Flag for use with {@link #getExtractedText} to indicate you
+ * would like to receive updates when the extracted text changes.
+ */
+ int GET_EXTRACTED_TEXT_MONITOR = 0x0001;
+
+ /**
+ * Get <var>n</var> characters of text before the current cursor
+ * position.
+ *
+ * <p>This method may fail either if the input connection has
+ * become invalid (such as its process crashing) or the editor is
+ * taking too long to respond with the text (it is given a couple
+ * seconds to return). In either case, null is returned. This
+ * method does not affect the text in the editor in any way, nor
+ * does it affect the selection or composing spans.</p>
+ *
+ * <p>If {@link #GET_TEXT_WITH_STYLES} is supplied as flags, the
+ * editor should return a {@link android.text.SpannableString}
+ * with all the spans set on the text.</p>
+ *
+ * <p><strong>IME authors:</strong> please consider this will
+ * trigger an IPC round-trip that will take some time. Assume this
+ * method consumes a lot of time. Also, please keep in mind the
+ * Editor may choose to return less characters than requested even
+ * if they are available for performance reasons. If you are using
+ * this to get the initial text around the cursor, you may consider
+ * using {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
+ * {@link EditorInfo#getInitialSelectedText(int)}, and
+ * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
+ *
+ * <p><strong>Editor authors:</strong> please be careful of race
+ * conditions in implementing this call. An IME can make a change
+ * to the text and use this method right away; you need to make
+ * sure the returned value is consistent with the result of the
+ * latest edits. Also, you may return less than n characters if performance
+ * dictates so, but keep in mind IMEs are relying on this for many
+ * functions: you should not, for example, limit the returned value to
+ * the current line, and specifically do not return 0 characters unless
+ * the cursor is really at the start of the text.</p>
+ *
+ * @param n The expected length of the text. This must be non-negative.
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or {@link #GET_TEXT_WITH_STYLES}.
+ * @return the text before the cursor position; the length of the
+ * returned text might be less than <var>n</var>.
+ * @throws IllegalArgumentException if {@code n} is negative.
+ */
+ @Nullable
+ CharSequence getTextBeforeCursor(@IntRange(from = 0) int n, int flags);
+
+ /**
+ * Get <var>n</var> characters of text after the current cursor
+ * position.
+ *
+ * <p>This method may fail either if the input connection has
+ * become invalid (such as its process crashing) or the client is
+ * taking too long to respond with the text (it is given a couple
+ * seconds to return). In either case, null is returned.
+ *
+ * <p>This method does not affect the text in the editor in any
+ * way, nor does it affect the selection or composing spans.</p>
+ *
+ * <p>If {@link #GET_TEXT_WITH_STYLES} is supplied as flags, the
+ * editor should return a {@link android.text.SpannableString}
+ * with all the spans set on the text.</p>
+ *
+ * <p><strong>IME authors:</strong> please consider this will
+ * trigger an IPC round-trip that will take some time. Assume this
+ * method consumes a lot of time. If you are using this to get the
+ * initial text around the cursor, you may consider using
+ * {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
+ * {@link EditorInfo#getInitialSelectedText(int)}, and
+ * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
+ *
+ * <p><strong>Editor authors:</strong> please be careful of race
+ * conditions in implementing this call. An IME can make a change
+ * to the text and use this method right away; you need to make
+ * sure the returned value is consistent with the result of the
+ * latest edits. Also, you may return less than n characters if performance
+ * dictates so, but keep in mind IMEs are relying on this for many
+ * functions: you should not, for example, limit the returned value to
+ * the current line, and specifically do not return 0 characters unless
+ * the cursor is really at the end of the text.</p>
+ *
+ * @param n The expected length of the text. This must be non-negative.
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or {@link #GET_TEXT_WITH_STYLES}.
+ *
+ * @return the text after the cursor position; the length of the
+ * returned text might be less than <var>n</var>.
+ * @throws IllegalArgumentException if {@code n} is negative.
+ */
+ @Nullable
+ CharSequence getTextAfterCursor(@IntRange(from = 0) int n, int flags);
+
+ /**
+ * Gets the selected text, if any.
+ *
+ * <p>This method may fail if either the input connection has
+ * become invalid (such as its process crashing) or the client is
+ * taking too long to respond with the text (it is given a couple
+ * of seconds to return). In either case, null is returned.</p>
+ *
+ * <p>This method must not cause any changes in the editor's
+ * state.</p>
+ *
+ * <p>If {@link #GET_TEXT_WITH_STYLES} is supplied as flags, the
+ * editor should return a {@link android.text.SpannableString}
+ * with all the spans set on the text.</p>
+ *
+ * <p><strong>IME authors:</strong> please consider this will
+ * trigger an IPC round-trip that will take some time. Assume this
+ * method consumes a lot of time. If you are using this to get the
+ * initial text around the cursor, you may consider using
+ * {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
+ * {@link EditorInfo#getInitialSelectedText(int)}, and
+ * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
+ *
+ * <p><strong>Editor authors:</strong> please be careful of race
+ * conditions in implementing this call. An IME can make a change
+ * to the text or change the selection position and use this
+ * method right away; you need to make sure the returned value is
+ * consistent with the results of the latest edits.</p>
+ *
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or {@link #GET_TEXT_WITH_STYLES}.
+ * @return the text that is currently selected, if any, or null if
+ * no text is selected. In {@link android.os.Build.VERSION_CODES#N} and
+ * later, returns false when the target application does not implement
+ * this method.
+ */
+ CharSequence getSelectedText(int flags);
+
+ /**
+ * Gets the surrounding text around the current cursor, with <var>beforeLength</var> characters
+ * of text before the cursor (start of the selection), <var>afterLength</var> characters of text
+ * after the cursor (end of the selection), and all of the selected text. The range are for java
+ * characters, not glyphs that can be multiple characters.
+ *
+ * <p>This method may fail either if the input connection has become invalid (such as its
+ * process crashing), or the client is taking too long to respond with the text (it is given a
+ * couple seconds to return), or the protocol is not supported. In any of these cases, null is
+ * returned.
+ *
+ * <p>This method does not affect the text in the editor in any way, nor does it affect the
+ * selection or composing spans.</p>
+ *
+ * <p>If {@link #GET_TEXT_WITH_STYLES} is supplied as flags, the editor should return a
+ * {@link android.text.Spanned} with all the spans set on the text.</p>
+ *
+ * <p><strong>IME authors:</strong> please consider this will trigger an IPC round-trip that
+ * will take some time. Assume this method consumes a lot of time. If you are using this to get
+ * the initial surrounding text around the cursor, you may consider using
+ * {@link EditorInfo#getInitialTextBeforeCursor(int, int)},
+ * {@link EditorInfo#getInitialSelectedText(int)}, and
+ * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.</p>
+ *
+ * @param beforeLength The expected length of the text before the cursor.
+ * @param afterLength The expected length of the text after the cursor.
+ * @param flags Supplies additional options controlling how the text is returned. May be either
+ * {@code 0} or {@link #GET_TEXT_WITH_STYLES}.
+ * @return an {@link android.view.inputmethod.SurroundingText} object describing the surrounding
+ * text and state of selection, or null if the input connection is no longer valid, or the
+ * editor can't comply with the request for some reason, or the application does not implement
+ * this method. The length of the returned text might be less than the sum of
+ * <var>beforeLength</var> and <var>afterLength</var> .
+ * @throws IllegalArgumentException if {@code beforeLength} or {@code afterLength} is negative.
+ */
+ @Nullable
+ default SurroundingText getSurroundingText(
+ @IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength,
+ @GetTextType int flags) {
+ Preconditions.checkArgumentNonnegative(beforeLength);
+ Preconditions.checkArgumentNonnegative(afterLength);
+
+ CharSequence textBeforeCursor = getTextBeforeCursor(beforeLength, flags);
+ if (textBeforeCursor == null) {
+ return null;
+ }
+ CharSequence textAfterCursor = getTextAfterCursor(afterLength, flags);
+ if (textAfterCursor == null) {
+ return null;
+ }
+ CharSequence selectedText = getSelectedText(flags);
+ if (selectedText == null) {
+ selectedText = "";
+ }
+ CharSequence surroundingText =
+ TextUtils.concat(textBeforeCursor, selectedText, textAfterCursor);
+ return new SurroundingText(surroundingText, textBeforeCursor.length(),
+ textBeforeCursor.length() + selectedText.length(), -1);
+ }
+
+ /**
+ * Retrieve the current capitalization mode in effect at the
+ * current cursor position in the text. See
+ * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode}
+ * for more information.
+ *
+ * <p>This method may fail either if the input connection has
+ * become invalid (such as its process crashing) or the client is
+ * taking too long to respond with the text (it is given a couple
+ * seconds to return). In either case, 0 is returned.</p>
+ *
+ * <p>This method does not affect the text in the editor in any
+ * way, nor does it affect the selection or composing spans.</p>
+ *
+ * <p><strong>Editor authors:</strong> please be careful of race
+ * conditions in implementing this call. An IME can change the
+ * cursor position and use this method right away; you need to make
+ * sure the returned value is consistent with the results of the
+ * latest edits and changes to the cursor position.</p>
+ *
+ * @param reqModes The desired modes to retrieve, as defined by
+ * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode}. These
+ * constants are defined so that you can simply pass the current
+ * {@link EditorInfo#inputType TextBoxAttribute.contentType} value
+ * directly in to here.
+ * @return the caps mode flags that are in effect at the current
+ * cursor position. See TYPE_TEXT_FLAG_CAPS_* in {@link android.text.InputType}.
+ */
+ int getCursorCapsMode(int reqModes);
+
+ /**
+ * Retrieve the current text in the input connection's editor, and
+ * monitor for any changes to it. This function returns with the
+ * current text, and optionally the input connection can send
+ * updates to the input method when its text changes.
+ *
+ * <p>This method may fail either if the input connection has
+ * become invalid (such as its process crashing) or the client is
+ * taking too long to respond with the text (it is given a couple
+ * seconds to return). In either case, null is returned.</p>
+ *
+ * <p>Editor authors: as a general rule, try to comply with the
+ * fields in <code>request</code> for how many chars to return,
+ * but if performance or convenience dictates otherwise, please
+ * feel free to do what is most appropriate for your case. Also,
+ * if the
+ * {@link #GET_EXTRACTED_TEXT_MONITOR} flag is set, you should be
+ * calling
+ * {@link InputMethodManager#updateExtractedText(View, int, ExtractedText)}
+ * whenever you call
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)}.</p>
+ *
+ * @param request Description of how the text should be returned.
+ * {@link android.view.inputmethod.ExtractedTextRequest}
+ * @param flags Additional options to control the client, either {@code 0} or
+ * {@link #GET_EXTRACTED_TEXT_MONITOR}.
+
+ * @return an {@link android.view.inputmethod.ExtractedText}
+ * object describing the state of the text view and containing the
+ * extracted text itself, or null if the input connection is no
+ * longer valid of the editor can't comply with the request for
+ * some reason.
+ */
+ ExtractedText getExtractedText(ExtractedTextRequest request, int flags);
+
+ /**
+ * Delete <var>beforeLength</var> characters of text before the
+ * current cursor position, and delete <var>afterLength</var>
+ * characters of text after the current cursor position, excluding
+ * the selection. Before and after refer to the order of the
+ * characters in the string, not to their visual representation:
+ * this means you don't have to figure out the direction of the
+ * text and can just use the indices as-is.
+ *
+ * <p>The lengths are supplied in Java chars, not in code points
+ * or in glyphs.</p>
+ *
+ * <p>Since this method only operates on text before and after the
+ * selection, it can't affect the contents of the selection. This
+ * may affect the composing span if the span includes characters
+ * that are to be deleted, but otherwise will not change it. If
+ * some characters in the composing span are deleted, the
+ * composing span will persist but get shortened by however many
+ * chars inside it have been removed.</p>
+ *
+ * <p><strong>IME authors:</strong> please be careful not to
+ * delete only half of a surrogate pair. Also take care not to
+ * delete more characters than are in the editor, as that may have
+ * ill effects on the application. Calling this method will cause
+ * the editor to call
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on your service after the batch input is over.</p>
+ *
+ * <p><strong>Editor authors:</strong> please be careful of race
+ * conditions in implementing this call. An IME can make a change
+ * to the text or change the selection position and use this
+ * method right away; you need to make sure the effects are
+ * consistent with the results of the latest edits. Also, although
+ * the IME should not send lengths bigger than the contents of the
+ * string, you should check the values for overflows and trim the
+ * indices to the size of the contents to avoid crashes. Since
+ * this changes the contents of the editor, you need to make the
+ * changes known to the input method by calling
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)},
+ * but be careful to wait until the batch edit is over if one is
+ * in progress.</p>
+ *
+ * @param beforeLength The number of characters before the cursor to be deleted, in code unit.
+ * If this is greater than the number of existing characters between the beginning of the
+ * text and the cursor, then this method does not fail but deletes all the characters in
+ * that range.
+ * @param afterLength The number of characters after the cursor to be deleted, in code unit.
+ * If this is greater than the number of existing characters between the cursor and
+ * the end of the text, then this method does not fail but deletes all the characters in
+ * that range.
+ * @return true on success, false if the input connection is no longer valid.
+ */
+ boolean deleteSurroundingText(int beforeLength, int afterLength);
+
+ /**
+ * A variant of {@link #deleteSurroundingText(int, int)}. Major differences are:
+ *
+ * <ul>
+ * <li>The lengths are supplied in code points, not in Java chars or in glyphs.</>
+ * <li>This method does nothing if there are one or more invalid surrogate pairs in the
+ * requested range.</li>
+ * </ul>
+ *
+ * <p><strong>Editor authors:</strong> In addition to the requirement in
+ * {@link #deleteSurroundingText(int, int)}, make sure to do nothing when one ore more invalid
+ * surrogate pairs are found in the requested range.</p>
+ *
+ * @see #deleteSurroundingText(int, int)
+ *
+ * @param beforeLength The number of characters before the cursor to be deleted, in code points.
+ * If this is greater than the number of existing characters between the beginning of the
+ * text and the cursor, then this method does not fail but deletes all the characters in
+ * that range.
+ * @param afterLength The number of characters after the cursor to be deleted, in code points.
+ * If this is greater than the number of existing characters between the cursor and
+ * the end of the text, then this method does not fail but deletes all the characters in
+ * that range.
+ * @return true on success, false if the input connection is no longer valid. Returns
+ * {@code false} when the target application does not implement this method.
+ */
+ boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
+
+ /**
+ * Replace the currently composing text with the given text, and
+ * set the new cursor position. Any composing text set previously
+ * will be removed automatically.
+ *
+ * <p>If there is any composing span currently active, all
+ * characters that it comprises are removed. The passed text is
+ * added in its place, and a composing span is added to this
+ * text. If there is no composing span active, the passed text is
+ * added at the cursor position (removing selected characters
+ * first if any), and a composing span is added on the new text.
+ * Finally, the cursor is moved to the location specified by
+ * <code>newCursorPosition</code>.</p>
+ *
+ * <p>This is usually called by IMEs to add or remove or change
+ * characters in the composing span. Calling this method will
+ * cause the editor to call
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.</p>
+ *
+ * <p><strong>Editor authors:</strong> please keep in mind the
+ * text may be very similar or completely different than what was
+ * in the composing span at call time, or there may not be a
+ * composing span at all. Please note that although it's not
+ * typical use, the string may be empty. Treat this normally,
+ * replacing the currently composing text with an empty string.
+ * Also, be careful with the cursor position. IMEs rely on this
+ * working exactly as described above. Since this changes the
+ * contents of the editor, you need to make the changes known to
+ * the input method by calling
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)},
+ * but be careful to wait until the batch edit is over if one is
+ * in progress. Note that this method can set the cursor position
+ * on either edge of the composing text or entirely outside it,
+ * but the IME may also go on to move the cursor position to
+ * within the composing text in a subsequent call so you should
+ * make no assumption at all: the composing text and the selection
+ * are entirely independent.</p>
+ *
+ * @param text The composing text with styles if necessary. If no style
+ * object attached to the text, the default style for composing text
+ * is used. See {@link android.text.Spanned} for how to attach style
+ * object to the text. {@link android.text.SpannableString} and
+ * {@link android.text.SpannableStringBuilder} are two
+ * implementations of the interface {@link android.text.Spanned}.
+ * @param newCursorPosition The new cursor position around the text. If
+ * > 0, this is relative to the end of the text - 1; if <= 0, this
+ * is relative to the start of the text. So a value of 1 will
+ * always advance you to the position after the full text being
+ * inserted. Note that this means you can't position the cursor
+ * within the text, because the editor can make modifications to
+ * the text you are providing so it is not possible to correctly
+ * specify locations there.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ */
+ boolean setComposingText(CharSequence text, int newCursorPosition);
+
+ /**
+ * Mark a certain region of text as composing text. If there was a
+ * composing region, the characters are left as they were and the
+ * composing span removed, as if {@link #finishComposingText()}
+ * has been called. The default style for composing text is used.
+ *
+ * <p>The passed indices are clipped to the contents bounds. If
+ * the resulting region is zero-sized, no region is marked and the
+ * effect is the same as that of calling {@link #finishComposingText()}.
+ * The order of start and end is not important. In effect, the
+ * region from start to end and the region from end to start is
+ * the same. Editor authors, be ready to accept a start that is
+ * greater than end.</p>
+ *
+ * <p>Since this does not change the contents of the text, editors should not call
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)} and
+ * IMEs should not receive
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)}.</p>
+ *
+ * <p>This has no impact on the cursor/selection position. It may
+ * result in the cursor being anywhere inside or outside the
+ * composing region, including cases where the selection and the
+ * composing region overlap partially or entirely.</p>
+ *
+ * @param start the position in the text at which the composing region begins
+ * @param end the position in the text at which the composing region ends
+ * @return true on success, false if the input connection is no longer
+ * valid. In {@link android.os.Build.VERSION_CODES#N} and later, false is returned when the
+ * target application does not implement this method.
+ */
+ boolean setComposingRegion(int start, int end);
+
+ /**
+ * Have the text editor finish whatever composing text is
+ * currently active. This simply leaves the text as-is, removing
+ * any special composing styling or other state that was around
+ * it. The cursor position remains unchanged.
+ *
+ * <p><strong>IME authors:</strong> be aware that this call may be
+ * expensive with some editors.</p>
+ *
+ * <p><strong>Editor authors:</strong> please note that the cursor
+ * may be anywhere in the contents when this is called, including
+ * in the middle of the composing span or in a completely
+ * unrelated place. It must not move.</p>
+ *
+ * @return true on success, false if the input connection
+ * is no longer valid.
+ */
+ boolean finishComposingText();
+
+ /**
+ * Commit text to the text box and set the new cursor position.
+ *
+ * <p>This method removes the contents of the currently composing
+ * text and replaces it with the passed CharSequence, and then
+ * moves the cursor according to {@code newCursorPosition}. If there
+ * is no composing text when this method is called, the new text is
+ * inserted at the cursor position, removing text inside the selection
+ * if any. This behaves like calling
+ * {@link #setComposingText(CharSequence, int) setComposingText(text, newCursorPosition)}
+ * then {@link #finishComposingText()}.</p>
+ *
+ * <p>Calling this method will cause the editor to call
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
+ * <strong>Editor authors</strong>, for this to happen you need to
+ * make the changes known to the input method by calling
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)},
+ * but be careful to wait until the batch edit is over if one is
+ * in progress.</p>
+ *
+ * @param text The text to commit. This may include styles.
+ * @param newCursorPosition The new cursor position around the text,
+ * in Java characters. If > 0, this is relative to the end
+ * of the text - 1; if <= 0, this is relative to the start
+ * of the text. So a value of 1 will always advance the cursor
+ * to the position after the full text being inserted. Note that
+ * this means you can't position the cursor within the text,
+ * because the editor can make modifications to the text
+ * you are providing so it is not possible to correctly specify
+ * locations there.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ */
+ boolean commitText(CharSequence text, int newCursorPosition);
+
+ /**
+ * Commit a completion the user has selected from the possible ones
+ * previously reported to {@link InputMethodSession#displayCompletions
+ * InputMethodSession#displayCompletions(CompletionInfo[])} or
+ * {@link InputMethodManager#displayCompletions
+ * InputMethodManager#displayCompletions(View, CompletionInfo[])}.
+ * This will result in the same behavior as if the user had
+ * selected the completion from the actual UI. In all other
+ * respects, this behaves like {@link #commitText(CharSequence, int)}.
+ *
+ * <p><strong>IME authors:</strong> please take care to send the
+ * same object that you received through
+ * {@link android.inputmethodservice.InputMethodService#onDisplayCompletions(CompletionInfo[])}.
+ * </p>
+ *
+ * <p><strong>Editor authors:</strong> if you never call
+ * {@link InputMethodSession#displayCompletions(CompletionInfo[])} or
+ * {@link InputMethodManager#displayCompletions(View, CompletionInfo[])} then
+ * a well-behaved IME should never call this on your input
+ * connection, but be ready to deal with misbehaving IMEs without
+ * crashing.</p>
+ *
+ * <p>Calling this method (with a valid {@link CompletionInfo} object)
+ * will cause the editor to call
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
+ * <strong>Editor authors</strong>, for this to happen you need to
+ * make the changes known to the input method by calling
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)},
+ * but be careful to wait until the batch edit is over if one is
+ * in progress.</p>
+ *
+ * @param text The committed completion.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ */
+ boolean commitCompletion(CompletionInfo text);
+
+ /**
+ * Commit a correction automatically performed on the raw user's input. A
+ * typical example would be to correct typos using a dictionary.
+ *
+ * <p>Calling this method will cause the editor to call
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
+ * <strong>Editor authors</strong>, for this to happen you need to
+ * make the changes known to the input method by calling
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)},
+ * but be careful to wait until the batch edit is over if one is
+ * in progress.</p>
+ *
+ * @param correctionInfo Detailed information about the correction.
+ * @return true on success, false if the input connection is no longer valid.
+ * In {@link android.os.Build.VERSION_CODES#N} and later, returns false
+ * when the target application does not implement this method.
+ */
+ boolean commitCorrection(CorrectionInfo correctionInfo);
+
+ /**
+ * Set the selection of the text editor. To set the cursor
+ * position, start and end should have the same value.
+ *
+ * <p>Since this moves the cursor, calling this method will cause
+ * the editor to call
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
+ * <strong>Editor authors</strong>, for this to happen you need to
+ * make the changes known to the input method by calling
+ * {@link InputMethodManager#updateSelection(View, int, int, int, int)},
+ * but be careful to wait until the batch edit is over if one is
+ * in progress.</p>
+ *
+ * <p>This has no effect on the composing region which must stay
+ * unchanged. The order of start and end is not important. In
+ * effect, the region from start to end and the region from end to
+ * start is the same. Editor authors, be ready to accept a start
+ * that is greater than end.</p>
+ *
+ * @param start the character index where the selection should start.
+ * @param end the character index where the selection should end.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ */
+ boolean setSelection(int start, int end);
+
+ /**
+ * Have the editor perform an action it has said it can do.
+ *
+ * <p>This is typically used by IMEs when the user presses the key
+ * associated with the action.</p>
+ *
+ * @param editorAction This must be one of the action constants for
+ * {@link EditorInfo#imeOptions EditorInfo.editorType}, such as
+ * {@link EditorInfo#IME_ACTION_GO EditorInfo.EDITOR_ACTION_GO}.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ */
+ boolean performEditorAction(int editorAction);
+
+ /**
+ * Perform a context menu action on the field. The given id may be one of:
+ * {@link android.R.id#selectAll},
+ * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText},
+ * {@link android.R.id#cut}, {@link android.R.id#copy},
+ * {@link android.R.id#paste}, {@link android.R.id#copyUrl},
+ * or {@link android.R.id#switchInputMethod}
+ */
+ boolean performContextMenuAction(int id);
+
+ /**
+ * Tell the editor that you are starting a batch of editor
+ * operations. The editor will try to avoid sending you updates
+ * about its state until {@link #endBatchEdit} is called. Batch
+ * edits nest.
+ *
+ * <p><strong>IME authors:</strong> use this to avoid getting
+ * calls to
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} corresponding to intermediate state. Also, use this to avoid
+ * flickers that may arise from displaying intermediate state. Be
+ * sure to call {@link #endBatchEdit} for each call to this, or
+ * you may block updates in the editor.</p>
+ *
+ * <p><strong>Editor authors:</strong> while a batch edit is in
+ * progress, take care not to send updates to the input method and
+ * not to update the display. IMEs use this intensively to this
+ * effect. Also please note that batch edits need to nest
+ * correctly.</p>
+ *
+ * @return true if a batch edit is now in progress, false otherwise. Since
+ * this method starts a batch edit, that means it will always return true
+ * unless the input connection is no longer valid.
+ */
+ boolean beginBatchEdit();
+
+ /**
+ * Tell the editor that you are done with a batch edit previously
+ * initiated with {@link #beginBatchEdit}. This ends the latest
+ * batch only.
+ *
+ * <p><strong>IME authors:</strong> make sure you call this
+ * exactly once for each call to {@link #beginBatchEdit}.</p>
+ *
+ * <p><strong>Editor authors:</strong> please be careful about
+ * batch edit nesting. Updates still to be held back until the end
+ * of the last batch edit.</p>
+ *
+ * @return true if there is still a batch edit in progress after closing
+ * the latest one (in other words, if the nesting count is > 0), false
+ * otherwise or if the input connection is no longer valid.
+ */
+ boolean endBatchEdit();
+
+ /**
+ * Send a key event to the process that is currently attached
+ * through this input connection. The event will be dispatched
+ * like a normal key event, to the currently focused view; this
+ * generally is the view that is providing this InputConnection,
+ * but due to the asynchronous nature of this protocol that can
+ * not be guaranteed and the focus may have changed by the time
+ * the event is received.
+ *
+ * <p>This method can be used to send key events to the
+ * application. For example, an on-screen keyboard may use this
+ * method to simulate a hardware keyboard. There are three types
+ * of standard keyboards, numeric (12-key), predictive (20-key)
+ * and ALPHA (QWERTY). You can specify the keyboard type by
+ * specify the device id of the key event.</p>
+ *
+ * <p>You will usually want to set the flag
+ * {@link KeyEvent#FLAG_SOFT_KEYBOARD KeyEvent.FLAG_SOFT_KEYBOARD}
+ * on all key event objects you give to this API; the flag will
+ * not be set for you.</p>
+ *
+ * <p>Note that it's discouraged to send such key events in normal
+ * operation; this is mainly for use with
+ * {@link android.text.InputType#TYPE_NULL} type text fields. Use
+ * the {@link #commitText} family of methods to send text to the
+ * application instead.</p>
+ *
+ * @param event The key event.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ *
+ * @see KeyEvent
+ * @see KeyCharacterMap#NUMERIC
+ * @see KeyCharacterMap#PREDICTIVE
+ * @see KeyCharacterMap#ALPHA
+ */
+ boolean sendKeyEvent(KeyEvent event);
+
+ /**
+ * Clear the given meta key pressed states in the given input
+ * connection.
+ *
+ * <p>This can be used by the IME to clear the meta key states set
+ * by a hardware keyboard with latched meta keys, if the editor
+ * keeps track of these.</p>
+ *
+ * @param states The states to be cleared, may be one or more bits as
+ * per {@link KeyEvent#getMetaState() KeyEvent.getMetaState()}.
+ * @return true on success, false if the input connection is no longer
+ * valid.
+ */
+ boolean clearMetaKeyStates(int states);
+
+ /**
+ * Called back when the connected IME switches between fullscreen and normal modes.
+ *
+ * <p>Note: On {@link android.os.Build.VERSION_CODES#O} and later devices, input methods are no
+ * longer allowed to directly call this method at any time. To signal this event in the target
+ * application, input methods should always call
+ * {@link InputMethodService#updateFullscreenMode()} instead. This approach should work on API
+ * {@link android.os.Build.VERSION_CODES#N_MR1} and prior devices.</p>
+ *
+ * @return For editor authors, the return value will always be ignored. For IME authors, this
+ * always returns {@code true} on {@link android.os.Build.VERSION_CODES#N_MR1} and prior
+ * devices and {@code false} on {@link android.os.Build.VERSION_CODES#O} and later
+ * devices.
+ * @see InputMethodManager#isFullscreenMode()
+ */
+ boolean reportFullscreenMode(boolean enabled);
+
+ /**
+ * Have the editor perform spell checking for the full content.
+ *
+ * <p>The editor can ignore this method call if it does not support spell checking.
+ *
+ * @return For editor authors, the return value will always be ignored. For IME authors, this
+ * method returns true if the spell check request was sent (whether or not the
+ * associated editor supports spell checking), false if the input connection is no
+ * longer valid.
+ */
+ default boolean performSpellCheck() {
+ return false;
+ }
+
+ /**
+ * API to send private commands from an input method to its
+ * connected editor. This can be used to provide domain-specific
+ * features that are only known between certain input methods and
+ * their clients. Note that because the InputConnection protocol
+ * is asynchronous, you have no way to get a result back or know
+ * if the client understood the command; you can use the
+ * information in {@link EditorInfo} to determine if a client
+ * supports a particular command.
+ *
+ * @param action Name of the command to be performed. This <em>must</em>
+ * be a scoped name, i.e. prefixed with a package name you own, so that
+ * different developers will not create conflicting commands.
+ * @param data Any data to include with the command.
+ * @return true if the command was sent (whether or not the
+ * associated editor understood it), false if the input connection is no longer
+ * valid.
+ */
+ boolean performPrivateCommand(String action, Bundle data);
+
+ /**
+ * The editor is requested to call
+ * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)} at
+ * once, as soon as possible, regardless of cursor/anchor position changes. This flag can be
+ * used together with {@link #CURSOR_UPDATE_MONITOR}.
+ */
+ int CURSOR_UPDATE_IMMEDIATE = 1 << 0;
+
+ /**
+ * The editor is requested to call
+ * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)}
+ * whenever cursor/anchor position is changed. To disable monitoring, call
+ * {@link InputConnection#requestCursorUpdates(int)} again with this flag off.
+ * <p>
+ * This flag can be used together with {@link #CURSOR_UPDATE_IMMEDIATE}.
+ * </p>
+ */
+ int CURSOR_UPDATE_MONITOR = 1 << 1;
+
+ /**
+ * Called by the input method to ask the editor for calling back
+ * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)} to
+ * notify cursor/anchor locations.
+ *
+ * @param cursorUpdateMode {@link #CURSOR_UPDATE_IMMEDIATE} and/or
+ * {@link #CURSOR_UPDATE_MONITOR}. Pass {@code 0} to disable the effect of
+ * {@link #CURSOR_UPDATE_MONITOR}.
+ * @return {@code true} if the request is scheduled. {@code false} to indicate that when the
+ * application will not call
+ * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)}.
+ * In {@link android.os.Build.VERSION_CODES#N} and later, returns {@code false} also when the
+ * target application does not implement this method.
+ */
+ boolean requestCursorUpdates(int cursorUpdateMode);
+
+ /**
+ * Called by the {@link InputMethodManager} to enable application developers to specify a
+ * dedicated {@link Handler} on which incoming IPC method calls from input methods will be
+ * dispatched.
+ *
+ * <p>Note: This does nothing when called from input methods.</p>
+ *
+ * @return {@code null} to use the default {@link Handler}.
+ */
+ Handler getHandler();
+
+ /**
+ * Called by the system up to only once to notify that the system is about to invalidate
+ * connection between the input method and the application.
+ *
+ * <p><strong>Editor authors</strong>: You can clear all the nested batch edit right now and
+ * you no longer need to handle subsequent callbacks on this connection, including
+ * {@link #beginBatchEdit()}}. Note that although the system tries to call this method whenever
+ * possible, there may be a chance that this method is not called in some exceptional
+ * situations.</p>
+ *
+ * <p>Note: This does nothing when called from input methods.</p>
+ */
+ void closeConnection();
+
+ /**
+ * When this flag is used, the editor will be able to request read access to the content URI
+ * contained in the {@link InputContentInfo} object.
+ *
+ * <p>Make sure that the content provider owning the Uri sets the
+ * {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions
+ * grantUriPermissions} attribute in its manifest or included the
+ * {@link android.R.styleable#AndroidManifestGrantUriPermission
+ * <grant-uri-permissions>} tag. Otherwise {@link InputContentInfo#requestPermission()}
+ * can fail.</p>
+ *
+ * <p>Although calling this API is allowed only for the IME that is currently selected, the
+ * client is able to request a temporary read-only access even after the current IME is switched
+ * to any other IME as long as the client keeps {@link InputContentInfo} object.</p>
+ **/
+ int INPUT_CONTENT_GRANT_READ_URI_PERMISSION =
+ android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; // 0x00000001
+
+ /**
+ * Called by the input method to commit content such as a PNG image to the editor.
+ *
+ * <p>In order to avoid a variety of compatibility issues, this focuses on a simple use case,
+ * where editors and IMEs are expected to work cooperatively as follows:</p>
+ * <ul>
+ * <li>Editor must keep {@link EditorInfo#contentMimeTypes} equal to {@code null} if it does
+ * not support this method at all.</li>
+ * <li>Editor can ignore this request when the MIME type specified in
+ * {@code inputContentInfo} does not match any of {@link EditorInfo#contentMimeTypes}.
+ * </li>
+ * <li>Editor can ignore the cursor position when inserting the provided content.</li>
+ * <li>Editor can return {@code true} asynchronously, even before it starts loading the
+ * content.</li>
+ * <li>Editor should provide a way to delete the content inserted by this method or to
+ * revert the effect caused by this method.</li>
+ * <li>IME should not call this method when there is any composing text, in case calling
+ * this method causes a focus change.</li>
+ * <li>IME should grant a permission for the editor to read the content. See
+ * {@link EditorInfo#packageName} about how to obtain the package name of the editor.</li>
+ * </ul>
+ *
+ * @param inputContentInfo Content to be inserted.
+ * @param flags {@link #INPUT_CONTENT_GRANT_READ_URI_PERMISSION} if the content provider
+ * allows {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions
+ * grantUriPermissions} or {@code 0} if the application does not need to call
+ * {@link InputContentInfo#requestPermission()}.
+ * @param opts optional bundle data. This can be {@code null}.
+ * @return {@code true} if this request is accepted by the application, whether the request
+ * is already handled or still being handled in background, {@code false} otherwise.
+ */
+ boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags,
+ @Nullable Bundle opts);
+
+ /**
+ * Called by the input method to indicate that it consumes all input for itself, or no longer
+ * does so.
+ *
+ * <p>Editors should reflect that they are not receiving input by hiding the cursor if
+ * {@code imeConsumesInput} is {@code true}, and resume showing the cursor if it is
+ * {@code false}.
+ *
+ * @param imeConsumesInput {@code true} when the IME is consuming input and the cursor should be
+ * hidden, {@code false} when input to the editor resumes and the cursor should be shown again.
+ * @return For editor authors, the return value will always be ignored. For IME authors, this
+ * method returns {@code true} if the request was sent (whether or not the associated
+ * editor does something based on this request), {@code false} if the input connection
+ * is no longer valid.
+ */
+ default boolean setImeConsumesInput(boolean imeConsumesInput) {
+ return false;
+ }
+}
diff --git a/android/view/inputmethod/InputConnectionInspector.java b/android/view/inputmethod/InputConnectionInspector.java
new file mode 100644
index 0000000..7621da7
--- /dev/null
+++ b/android/view/inputmethod/InputConnectionInspector.java
@@ -0,0 +1,293 @@
+/*
+ * 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.view.inputmethod;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * @hide
+ */
+public final class InputConnectionInspector {
+
+ @Retention(SOURCE)
+ @IntDef({MissingMethodFlags.GET_SELECTED_TEXT,
+ MissingMethodFlags.SET_COMPOSING_REGION,
+ MissingMethodFlags.COMMIT_CORRECTION,
+ MissingMethodFlags.REQUEST_CURSOR_UPDATES,
+ MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS,
+ MissingMethodFlags.GET_HANDLER,
+ MissingMethodFlags.CLOSE_CONNECTION,
+ MissingMethodFlags.COMMIT_CONTENT,
+ MissingMethodFlags.GET_SURROUNDING_TEXT
+ })
+ public @interface MissingMethodFlags {
+ /**
+ * {@link InputConnection#getSelectedText(int)} is available in
+ * {@link android.os.Build.VERSION_CODES#GINGERBREAD} and later.
+ */
+ int GET_SELECTED_TEXT = 1 << 0;
+ /**
+ * {@link InputConnection#setComposingRegion(int, int)} is available in
+ * {@link android.os.Build.VERSION_CODES#GINGERBREAD} and later.
+ */
+ int SET_COMPOSING_REGION = 1 << 1;
+ /**
+ * {@link InputConnection#commitCorrection(CorrectionInfo)} is available in
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and later.
+ */
+ int COMMIT_CORRECTION = 1 << 2;
+ /**
+ * {@link InputConnection#requestCursorUpdates(int)} is available in
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later.
+ */
+ int REQUEST_CURSOR_UPDATES = 1 << 3;
+ /**
+ * {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}} is available in
+ * {@link android.os.Build.VERSION_CODES#N} and later.
+ */
+ int DELETE_SURROUNDING_TEXT_IN_CODE_POINTS = 1 << 4;
+ /**
+ * {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}} is available in
+ * {@link android.os.Build.VERSION_CODES#N} and later.
+ */
+ int GET_HANDLER = 1 << 5;
+ /**
+ * {@link InputConnection#closeConnection()}} is available in
+ * {@link android.os.Build.VERSION_CODES#N} and later.
+ */
+ int CLOSE_CONNECTION = 1 << 6;
+ /**
+ * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} is available in
+ * {@link android.os.Build.VERSION_CODES#N} MR-1 and later.
+ */
+ int COMMIT_CONTENT = 1 << 7;
+ /**
+ * {@link InputConnection#getSurroundingText(int, int, int)} is available in
+ * {@link android.os.Build.VERSION_CODES#S} and later.
+ */
+ int GET_SURROUNDING_TEXT = 1 << 8;
+ }
+
+ private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap(
+ new WeakHashMap<>());
+
+ @MissingMethodFlags
+ public static int getMissingMethodFlags(@Nullable final InputConnection ic) {
+ if (ic == null) {
+ return 0;
+ }
+ // Optimization for a known class.
+ if (ic instanceof BaseInputConnection) {
+ return 0;
+ }
+ // Optimization for a known class.
+ if (ic instanceof InputConnectionWrapper) {
+ return ((InputConnectionWrapper) ic).getMissingMethodFlags();
+ }
+ return getMissingMethodFlagsInternal(ic.getClass());
+ }
+
+ @MissingMethodFlags
+ public static int getMissingMethodFlagsInternal(@NonNull final Class clazz) {
+ final Integer cachedFlags = sMissingMethodsMap.get(clazz);
+ if (cachedFlags != null) {
+ return cachedFlags;
+ }
+ int flags = 0;
+ if (!hasGetSelectedText(clazz)) {
+ flags |= MissingMethodFlags.GET_SELECTED_TEXT;
+ }
+ if (!hasSetComposingRegion(clazz)) {
+ flags |= MissingMethodFlags.SET_COMPOSING_REGION;
+ }
+ if (!hasCommitCorrection(clazz)) {
+ flags |= MissingMethodFlags.COMMIT_CORRECTION;
+ }
+ if (!hasRequestCursorUpdate(clazz)) {
+ flags |= MissingMethodFlags.REQUEST_CURSOR_UPDATES;
+ }
+ if (!hasDeleteSurroundingTextInCodePoints(clazz)) {
+ flags |= MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS;
+ }
+ if (!hasGetHandler(clazz)) {
+ flags |= MissingMethodFlags.GET_HANDLER;
+ }
+ if (!hasCloseConnection(clazz)) {
+ flags |= MissingMethodFlags.CLOSE_CONNECTION;
+ }
+ if (!hasCommitContent(clazz)) {
+ flags |= MissingMethodFlags.COMMIT_CONTENT;
+ }
+ if (!hasGetSurroundingText(clazz)) {
+ flags |= MissingMethodFlags.GET_SURROUNDING_TEXT;
+ }
+ sMissingMethodsMap.put(clazz, flags);
+ return flags;
+ }
+
+ private static boolean hasGetSelectedText(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("getSelectedText", int.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasSetComposingRegion(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("setComposingRegion", int.class, int.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasCommitCorrection(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("commitCorrection", CorrectionInfo.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasRequestCursorUpdate(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("requestCursorUpdates", int.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasDeleteSurroundingTextInCodePoints(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("deleteSurroundingTextInCodePoints", int.class,
+ int.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasGetHandler(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("getHandler");
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasCloseConnection(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("closeConnection");
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasCommitContent(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("commitContent", InputContentInfo.class,
+ int.class, Bundle.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static boolean hasGetSurroundingText(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("getSurroundingText", int.class, int.class,
+ int.class);
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) {
+ final StringBuilder sb = new StringBuilder();
+ boolean isEmpty = true;
+ if ((flags & MissingMethodFlags.GET_SELECTED_TEXT) != 0) {
+ sb.append("getSelectedText(int)");
+ isEmpty = false;
+ }
+ if ((flags & MissingMethodFlags.SET_COMPOSING_REGION) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("setComposingRegion(int, int)");
+ isEmpty = false;
+ }
+ if ((flags & MissingMethodFlags.COMMIT_CORRECTION) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("commitCorrection(CorrectionInfo)");
+ isEmpty = false;
+ }
+ if ((flags & MissingMethodFlags.REQUEST_CURSOR_UPDATES) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("requestCursorUpdate(int)");
+ isEmpty = false;
+ }
+ if ((flags & MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("deleteSurroundingTextInCodePoints(int, int)");
+ isEmpty = false;
+ }
+ if ((flags & MissingMethodFlags.GET_HANDLER) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("getHandler()");
+ }
+ if ((flags & MissingMethodFlags.CLOSE_CONNECTION) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("closeConnection()");
+ }
+ if ((flags & MissingMethodFlags.COMMIT_CONTENT) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("commitContent(InputContentInfo, Bundle)");
+ }
+ return sb.toString();
+ }
+}
diff --git a/android/view/inputmethod/InputConnectionWrapper.java b/android/view/inputmethod/InputConnectionWrapper.java
new file mode 100644
index 0000000..b1501a4
--- /dev/null
+++ b/android/view/inputmethod/InputConnectionWrapper.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+import android.annotation.IntRange;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * <p>Wrapper class for proxying calls to another InputConnection. Subclass and have fun!
+ */
+public class InputConnectionWrapper implements InputConnection {
+ private InputConnection mTarget;
+ final boolean mMutable;
+ @InputConnectionInspector.MissingMethodFlags
+ private int mMissingMethodFlags;
+
+ /**
+ * Initializes a wrapper.
+ *
+ * <p><b>Caveat:</b> Although the system can accept {@code (InputConnection) null} in some
+ * places, you cannot emulate such a behavior by non-null {@link InputConnectionWrapper} that
+ * has {@code null} in {@code target}.</p>
+ * @param target the {@link InputConnection} to be proxied.
+ * @param mutable set {@code true} to protect this object from being reconfigured to target
+ * another {@link InputConnection}. Note that this is ignored while the target is {@code null}.
+ */
+ public InputConnectionWrapper(InputConnection target, boolean mutable) {
+ mMutable = mutable;
+ mTarget = target;
+ mMissingMethodFlags = InputConnectionInspector.getMissingMethodFlags(target);
+ }
+
+ /**
+ * Change the target of the input connection.
+ *
+ * <p><b>Caveat:</b> Although the system can accept {@code (InputConnection) null} in some
+ * places, you cannot emulate such a behavior by non-null {@link InputConnectionWrapper} that
+ * has {@code null} in {@code target}.</p>
+ * @param target the {@link InputConnection} to be proxied.
+ * @throws SecurityException when this wrapper has non-null target and is immutable.
+ */
+ public void setTarget(InputConnection target) {
+ if (mTarget != null && !mMutable) {
+ throw new SecurityException("not mutable");
+ }
+ mTarget = target;
+ mMissingMethodFlags = InputConnectionInspector.getMissingMethodFlags(target);
+ }
+
+ /**
+ * @hide
+ */
+ @InputConnectionInspector.MissingMethodFlags
+ public int getMissingMethodFlags() {
+ return mMissingMethodFlags;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ * @throws IllegalArgumentException if {@code length} is negative.
+ */
+ @Nullable
+ @Override
+ public CharSequence getTextBeforeCursor(@IntRange(from = 0) int n, int flags) {
+ Preconditions.checkArgumentNonnegative(n);
+ return mTarget.getTextBeforeCursor(n, flags);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ * @throws IllegalArgumentException if {@code length} is negative.
+ */
+ @Nullable
+ @Override
+ public CharSequence getTextAfterCursor(@IntRange(from = 0) int n, int flags) {
+ Preconditions.checkArgumentNonnegative(n);
+ return mTarget.getTextAfterCursor(n, flags);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public CharSequence getSelectedText(int flags) {
+ return mTarget.getSelectedText(flags);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ * @throws IllegalArgumentException if {@code beforeLength} or {@code afterLength} is negative.
+ */
+ @Nullable
+ @Override
+ public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) {
+ Preconditions.checkArgumentNonnegative(beforeLength);
+ Preconditions.checkArgumentNonnegative(afterLength);
+ return mTarget.getSurroundingText(beforeLength, afterLength, flags);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public int getCursorCapsMode(int reqModes) {
+ return mTarget.getCursorCapsMode(reqModes);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+ return mTarget.getExtractedText(request, flags);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
+ return mTarget.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ return mTarget.deleteSurroundingText(beforeLength, afterLength);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean setComposingText(CharSequence text, int newCursorPosition) {
+ return mTarget.setComposingText(text, newCursorPosition);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean setComposingRegion(int start, int end) {
+ return mTarget.setComposingRegion(start, end);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean finishComposingText() {
+ return mTarget.finishComposingText();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ return mTarget.commitText(text, newCursorPosition);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean commitCompletion(CompletionInfo text) {
+ return mTarget.commitCompletion(text);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean commitCorrection(CorrectionInfo correctionInfo) {
+ return mTarget.commitCorrection(correctionInfo);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean setSelection(int start, int end) {
+ return mTarget.setSelection(start, end);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean performEditorAction(int editorAction) {
+ return mTarget.performEditorAction(editorAction);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean performContextMenuAction(int id) {
+ return mTarget.performContextMenuAction(id);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean beginBatchEdit() {
+ return mTarget.beginBatchEdit();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean endBatchEdit() {
+ return mTarget.endBatchEdit();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ return mTarget.sendKeyEvent(event);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean clearMetaKeyStates(int states) {
+ return mTarget.clearMetaKeyStates(states);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean reportFullscreenMode(boolean enabled) {
+ return mTarget.reportFullscreenMode(enabled);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean performSpellCheck() {
+ return mTarget.performSpellCheck();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean performPrivateCommand(String action, Bundle data) {
+ return mTarget.performPrivateCommand(action, data);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+ return mTarget.requestCursorUpdates(cursorUpdateMode);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public Handler getHandler() {
+ return mTarget.getHandler();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public void closeConnection() {
+ mTarget.closeConnection();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
+ return mTarget.commitContent(inputContentInfo, flags, opts);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public boolean setImeConsumesInput(boolean imeConsumesInput) {
+ return mTarget.setImeConsumesInput(imeConsumesInput);
+ }
+}
diff --git a/android/view/inputmethod/InputContentInfo.java b/android/view/inputmethod/InputContentInfo.java
new file mode 100644
index 0000000..5ebd9c1
--- /dev/null
+++ b/android/view/inputmethod/InputContentInfo.java
@@ -0,0 +1,293 @@
+/*
+ * 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.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ClipDescription;
+import android.content.ContentProvider;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.inputmethod.IInputContentUriToken;
+
+import java.security.InvalidParameterException;
+
+/**
+ * A container object with which input methods can send content files to the target application.
+ */
+public final class InputContentInfo implements Parcelable {
+
+ /**
+ * The content URI that may or may not have a user ID embedded by
+ * {@link ContentProvider#maybeAddUserId(Uri, int)}. This always preserves the exact value
+ * specified to a constructor. In other words, if it had user ID embedded when it was passed
+ * to the constructor, it still has the same user ID no matter if it is valid or not.
+ */
+ @NonNull
+ private final Uri mContentUri;
+ /**
+ * The user ID to which {@link #mContentUri} belongs to. If {@link #mContentUri} already
+ * embedded the user ID when it was specified then this fields has the same user ID. Otherwise
+ * the user ID is determined based on the process ID when the constructor is called.
+ *
+ * <p>CAUTION: If you received {@link InputContentInfo} from a different process, there is no
+ * guarantee that this value is correct and valid. Never use this for any security purpose</p>
+ */
+ @UserIdInt
+ private final int mContentUriOwnerUserId;
+ @NonNull
+ private final ClipDescription mDescription;
+ @Nullable
+ private final Uri mLinkUri;
+ @NonNull
+ private IInputContentUriToken mUriToken;
+
+ /**
+ * Constructs {@link InputContentInfo} object only with mandatory data.
+ *
+ * @param contentUri Content URI to be exported from the input method.
+ * This cannot be {@code null}.
+ * @param description A {@link ClipDescription} object that contains the metadata of
+ * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
+ * {@link ClipDescription#getLabel()} should be describing the content specified by
+ * {@code contentUri} for accessibility reasons.
+ */
+ public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description) {
+ this(contentUri, description, null /* link Uri */);
+ }
+
+ /**
+ * Constructs {@link InputContentInfo} object with additional link URI.
+ *
+ * @param contentUri Content URI to be exported from the input method.
+ * This cannot be {@code null}.
+ * @param description A {@link ClipDescription} object that contains the metadata of
+ * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
+ * {@link ClipDescription#getLabel()} should be describing the content specified by
+ * {@code contentUri} for accessibility reasons.
+ * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide
+ * a way to navigate the user to the specified web page if this is not {@code null}.
+ * @throws InvalidParameterException if any invalid parameter is specified.
+ */
+ public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description,
+ @Nullable Uri linkUri) {
+ validateInternal(contentUri, description, linkUri, true /* throwException */);
+ mContentUri = contentUri;
+ mContentUriOwnerUserId =
+ ContentProvider.getUserIdFromUri(mContentUri, UserHandle.myUserId());
+ mDescription = description;
+ mLinkUri = linkUri;
+ }
+
+ /**
+ * @return {@code true} if all the fields are valid.
+ * @hide
+ */
+ public boolean validate() {
+ return validateInternal(mContentUri, mDescription, mLinkUri, false /* throwException */);
+ }
+
+ /**
+ * Constructs {@link InputContentInfo} object with additional link URI.
+ *
+ * @param contentUri Content URI to be exported from the input method.
+ * This cannot be {@code null}.
+ * @param description A {@link ClipDescription} object that contains the metadata of
+ * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also
+ * {@link ClipDescription#getLabel()} should be describing the content specified by
+ * {@code contentUri} for accessibility reasons.
+ * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide
+ * a way to navigate the user to the specified web page if this is not {@code null}.
+ * @param throwException {@code true} if this method should throw an
+ * {@link InvalidParameterException}.
+ * @throws InvalidParameterException if any invalid parameter is specified.
+ */
+ private static boolean validateInternal(@NonNull Uri contentUri,
+ @NonNull ClipDescription description, @Nullable Uri linkUri, boolean throwException) {
+ if (contentUri == null) {
+ if (throwException) {
+ throw new NullPointerException("contentUri");
+ }
+ return false;
+ }
+ if (description == null) {
+ if (throwException) {
+ throw new NullPointerException("description");
+ }
+ return false;
+ }
+ final String contentUriScheme = contentUri.getScheme();
+ if (!"content".equals(contentUriScheme)) {
+ if (throwException) {
+ throw new InvalidParameterException("contentUri must have content scheme");
+ }
+ return false;
+ }
+ if (linkUri != null) {
+ final String scheme = linkUri.getScheme();
+ if (scheme == null ||
+ (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
+ if (throwException) {
+ throw new InvalidParameterException(
+ "linkUri must have either http or https scheme");
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return Content URI with which the content can be obtained.
+ */
+ @NonNull
+ public Uri getContentUri() {
+ // Fix up the content URI when and only when the caller's user ID does not match the owner's
+ // user ID.
+ if (mContentUriOwnerUserId != UserHandle.myUserId()) {
+ return ContentProvider.maybeAddUserId(mContentUri, mContentUriOwnerUserId);
+ }
+ return mContentUri;
+ }
+
+ /**
+ * @return {@link ClipDescription} object that contains the metadata of {@code #getContentUri()}
+ * such as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility
+ * purpose.
+ */
+ @NonNull
+ public ClipDescription getDescription() { return mDescription; }
+
+ /**
+ * @return An optional {@code http} or {@code https} URI that is related to this content.
+ */
+ @Nullable
+ public Uri getLinkUri() { return mLinkUri; }
+
+ /**
+ * Update the internal state of this object to be associated with the given token.
+ *
+ * <p>TODO(yukawa): Come up with an idea to make {@link InputContentInfo} immutable.</p>
+ *
+ * @param token special URI token obtained from the system.
+ * @hide
+ */
+ public void setUriToken(IInputContentUriToken token) {
+ if (mUriToken != null) {
+ throw new IllegalStateException("URI token is already set");
+ }
+ mUriToken = token;
+ }
+
+ /**
+ * Requests a temporary {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION read-only}
+ * access permission for the content URI associated with this object.
+ *
+ * <p>The lifecycle of the permission granted here is tied to this object instance. If the
+ * permission is not released explicitly via {@link #releasePermission()}, it will be
+ * released automatically when there are no more references to this object.</p>
+ *
+ * <p>Does nothing if the temporary permission is already granted.</p>
+ */
+ public void requestPermission() {
+ if (mUriToken == null) {
+ return;
+ }
+ try {
+ mUriToken.take();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Releases a temporary read-only access permission for content URI associated with this object.
+ *
+ * <p>Does nothing if the temporary permission is not granted.</p>
+ */
+ public void releasePermission() {
+ if (mUriToken == null) {
+ return;
+ }
+ try {
+ mUriToken.release();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Uri.writeToParcel(dest, mContentUri);
+ dest.writeInt(mContentUriOwnerUserId);
+ mDescription.writeToParcel(dest, flags);
+ Uri.writeToParcel(dest, mLinkUri);
+ if (mUriToken != null) {
+ dest.writeInt(1);
+ dest.writeStrongBinder(mUriToken.asBinder());
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ private InputContentInfo(@NonNull Parcel source) {
+ mContentUri = Uri.CREATOR.createFromParcel(source);
+ mContentUriOwnerUserId = source.readInt();
+ mDescription = ClipDescription.CREATOR.createFromParcel(source);
+ mLinkUri = Uri.CREATOR.createFromParcel(source);
+ if (source.readInt() == 1) {
+ mUriToken = IInputContentUriToken.Stub.asInterface(source.readStrongBinder());
+ } else {
+ mUriToken = null;
+ }
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<InputContentInfo> CREATOR
+ = new Parcelable.Creator<InputContentInfo>() {
+ @Override
+ public InputContentInfo createFromParcel(Parcel source) {
+ return new InputContentInfo(source);
+ }
+
+ @Override
+ public InputContentInfo[] newArray(int size) {
+ return new InputContentInfo[size];
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/InputMethod.java b/android/view/inputmethod/InputMethod.java
new file mode 100644
index 0000000..d2db0df
--- /dev/null
+++ b/android/view/inputmethod/InputMethod.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2007-2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.inputmethodservice.InputMethodService;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.View;
+
+import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
+import com.android.internal.view.IInlineSuggestionsRequestCallback;
+import com.android.internal.view.InlineSuggestionsRequestInfo;
+
+/**
+ * The InputMethod interface represents an input method which can generate key
+ * events and text, such as digital, email addresses, CJK characters, other
+ * language characters, and etc., while handling various input events, and send
+ * the text back to the application that requests text input. See
+ * {@link InputMethodManager} for more general information about the
+ * architecture.
+ *
+ * <p>Applications will not normally use this interface themselves, instead
+ * relying on the standard interaction provided by
+ * {@link android.widget.TextView} and {@link android.widget.EditText}.
+ *
+ * <p>Those implementing input methods should normally do so by deriving from
+ * {@link InputMethodService} or one of its subclasses. When implementing
+ * an input method, the service component containing it must also supply
+ * a {@link #SERVICE_META_DATA} meta-data field, referencing an XML resource
+ * providing details about the input method. All input methods also must
+ * require that clients hold the
+ * {@link android.Manifest.permission#BIND_INPUT_METHOD} in order to interact
+ * with the service; if this is not required, the system will not use that
+ * input method, because it can not trust that it is not compromised.
+ *
+ * <p>The InputMethod interface is actually split into two parts: the interface
+ * here is the top-level interface to the input method, providing all
+ * access to it, which only the system can access (due to the BIND_INPUT_METHOD
+ * permission requirement). In addition its method
+ * {@link #createSession(android.view.inputmethod.InputMethod.SessionCallback)}
+ * can be called to instantate a secondary {@link InputMethodSession} interface
+ * which is what clients use to communicate with the input method.
+ */
+public interface InputMethod {
+ /** @hide **/
+ public static final String TAG = "InputMethod";
+ /**
+ * This is the interface name that a service implementing an input
+ * method should say that it supports -- that is, this is the action it
+ * uses for its intent filter.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_INPUT_METHOD} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.view.InputMethod";
+
+ /**
+ * Name under which an InputMethod service component publishes information
+ * about itself. This meta-data must reference an XML resource containing
+ * an
+ * <code><{@link android.R.styleable#InputMethod input-method}></code>
+ * tag.
+ */
+ public static final String SERVICE_META_DATA = "android.view.im";
+
+ public interface SessionCallback {
+ public void sessionCreated(InputMethodSession session);
+ }
+
+ /**
+ * Called first thing after an input method is created, this supplies a
+ * unique token for the session it has with the system service as well as
+ * IPC endpoint to do some other privileged operations.
+ *
+ * @param token special token for the system to identify
+ * {@link InputMethodService}
+ * @param displayId The id of the display that current IME shown.
+ * Used for {{@link #updateInputMethodDisplay(int)}}
+ * @param privilegedOperations IPC endpoint to do some privileged
+ * operations that are allowed only to the
+ * current IME.
+ * @param configChanges {@link InputMethodInfo#getConfigChanges()} declared by IME.
+ * @hide
+ */
+ @MainThread
+ default void initializeInternal(IBinder token, int displayId,
+ IInputMethodPrivilegedOperations privilegedOperations, int configChanges) {
+ updateInputMethodDisplay(displayId);
+ attachToken(token);
+ }
+
+ /**
+ * Called to notify the IME that Autofill Frameworks requested an inline suggestions request.
+ *
+ * @param requestInfo information needed to create an {@link InlineSuggestionsRequest}.
+ * @param cb {@link IInlineSuggestionsRequestCallback} used to pass back the request object.
+ *
+ * @hide
+ */
+ default void onCreateInlineSuggestionsRequest(InlineSuggestionsRequestInfo requestInfo,
+ IInlineSuggestionsRequestCallback cb) {
+ try {
+ cb.onInlineSuggestionsUnsupported();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to call onInlineSuggestionsUnsupported.", e);
+ }
+ }
+
+ /**
+ * Called first thing after an input method is created, this supplies a
+ * unique token for the session it has with the system service. It is
+ * needed to identify itself with the service to validate its operations.
+ * This token <strong>must not</strong> be passed to applications, since
+ * it grants special priviledges that should not be given to applications.
+ *
+ * <p>The system guarantees that this method is called back between
+ * {@link InputMethodService#onCreate()} and {@link InputMethodService#onDestroy()}
+ * at most once.
+ */
+ @MainThread
+ public void attachToken(IBinder token);
+
+ /**
+ * Update context display according to given displayId.
+ *
+ * @param displayId The id of the display that need to update for context.
+ * @hide
+ */
+ @MainThread
+ default void updateInputMethodDisplay(int displayId) {
+ }
+
+ /**
+ * Bind a new application environment in to the input method, so that it
+ * can later start and stop input processing.
+ * Typically this method is called when this input method is enabled in an
+ * application for the first time.
+ *
+ * @param binding Information about the application window that is binding
+ * to the input method.
+ *
+ * @see InputBinding
+ * @see #unbindInput()
+ */
+ @MainThread
+ public void bindInput(InputBinding binding);
+
+ /**
+ * Unbind an application environment, called when the information previously
+ * set by {@link #bindInput} is no longer valid for this input method.
+ *
+ * <p>
+ * Typically this method is called when the application changes to be
+ * non-foreground.
+ */
+ @MainThread
+ public void unbindInput();
+
+ /**
+ * This method is called when the application starts to receive text and it
+ * is ready for this input method to process received events and send result
+ * text back to the application.
+ *
+ * @param inputConnection Optional specific input connection for
+ * communicating with the text box; if null, you should use the generic
+ * bound input connection.
+ * @param info Information about the text box (typically, an EditText)
+ * that requests input.
+ *
+ * @see EditorInfo
+ */
+ @MainThread
+ public void startInput(InputConnection inputConnection, EditorInfo info);
+
+ /**
+ * This method is called when the state of this input method needs to be
+ * reset.
+ *
+ * <p>
+ * Typically, this method is called when the input focus is moved from one
+ * text box to another.
+ *
+ * @param inputConnection Optional specific input connection for
+ * communicating with the text box; if null, you should use the generic
+ * bound input connection.
+ * @param attribute The attribute of the text box (typically, a EditText)
+ * that requests input.
+ *
+ * @see EditorInfo
+ */
+ @MainThread
+ public void restartInput(InputConnection inputConnection, EditorInfo attribute);
+
+ /**
+ * This method is called when {@code {@link #startInput(InputConnection, EditorInfo)} or
+ * {@code {@link #restartInput(InputConnection, EditorInfo)} needs to be dispatched.
+ *
+ * <p>Note: This method is hidden because the {@code startInputToken} that this method is
+ * dealing with is one of internal details, which should not be exposed to the IME developers.
+ * If you override this method, you are responsible for not breaking existing IMEs that expect
+ * {@link #startInput(InputConnection, EditorInfo)} to be still called back.</p>
+ *
+ * @param inputConnection optional specific input connection for communicating with the text
+ * box; if {@code null}, you should use the generic bound input
+ * connection
+ * @param editorInfo information about the text box (typically, an EditText) that requests input
+ * @param restarting {@code false} if this corresponds to
+ * {@link #startInput(InputConnection, EditorInfo)}. Otherwise this
+ * corresponds to {@link #restartInput(InputConnection, EditorInfo)}.
+ * @param startInputToken a token that identifies a logical session that starts with this method
+ * call. Some internal IPCs such as {@link
+ * InputMethodManager#setImeWindowStatus(IBinder, IBinder, int, int)}
+ * require this token to work, and you have to keep the token alive until
+ * the next {@link #startInput(InputConnection, EditorInfo, IBinder)} as
+ * long as your implementation of {@link InputMethod} relies on such
+ * IPCs
+ * @see #startInput(InputConnection, EditorInfo)
+ * @see #restartInput(InputConnection, EditorInfo)
+ * @see EditorInfo
+ * @hide
+ */
+ @MainThread
+ default void dispatchStartInputWithToken(@Nullable InputConnection inputConnection,
+ @NonNull EditorInfo editorInfo, boolean restarting,
+ @NonNull IBinder startInputToken) {
+ if (restarting) {
+ restartInput(inputConnection, editorInfo);
+ } else {
+ startInput(inputConnection, editorInfo);
+ }
+ }
+
+ /**
+ * Create a new {@link InputMethodSession} that can be handed to client
+ * applications for interacting with the input method. You can later
+ * use {@link #revokeSession(InputMethodSession)} to destroy the session
+ * so that it can no longer be used by any clients.
+ *
+ * @param callback Interface that is called with the newly created session.
+ */
+ @MainThread
+ public void createSession(SessionCallback callback);
+
+ /**
+ * Control whether a particular input method session is active.
+ *
+ * @param session The {@link InputMethodSession} previously provided through
+ * SessionCallback.sessionCreated() that is to be changed.
+ */
+ @MainThread
+ public void setSessionEnabled(InputMethodSession session, boolean enabled);
+
+ /**
+ * Disable and destroy a session that was previously created with
+ * {@link #createSession(android.view.inputmethod.InputMethod.SessionCallback)}.
+ * After this call, the given session interface is no longer active and
+ * calls on it will fail.
+ *
+ * @param session The {@link InputMethodSession} previously provided through
+ * SessionCallback.sessionCreated() that is to be revoked.
+ */
+ @MainThread
+ public void revokeSession(InputMethodSession session);
+
+ /**
+ * Flag for {@link #showSoftInput}: this show has been explicitly
+ * requested by the user. If not set, the system has decided it may be
+ * a good idea to show the input method based on a navigation operation
+ * in the UI.
+ */
+ public static final int SHOW_EXPLICIT = 0x00001;
+
+ /**
+ * Flag for {@link #showSoftInput}: this show has been forced to
+ * happen by the user. If set, the input method should remain visible
+ * until deliberated dismissed by the user in its UI.
+ */
+ public static final int SHOW_FORCED = 0x00002;
+
+ /**
+ * Request that any soft input part of the input method be shown to the user.
+ *
+ * @param flags Provides additional information about the show request.
+ * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
+ * @param resultReceiver The client requesting the show may wish to
+ * be told the impact of their request, which should be supplied here.
+ * The result code should be
+ * {@link InputMethodManager#RESULT_UNCHANGED_SHOWN InputMethodManager.RESULT_UNCHANGED_SHOWN},
+ * {@link InputMethodManager#RESULT_UNCHANGED_HIDDEN InputMethodManager.RESULT_UNCHANGED_HIDDEN},
+ * {@link InputMethodManager#RESULT_SHOWN InputMethodManager.RESULT_SHOWN}, or
+ * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
+ * @param showInputToken an opaque {@link android.os.Binder} token to identify which API call
+ * of {@link InputMethodManager#showSoftInput(View, int)} is associated with
+ * this callback.
+ * @hide
+ */
+ @MainThread
+ default public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ IBinder showInputToken) {
+ showSoftInput(flags, resultReceiver);
+ }
+
+ /**
+ * Request that any soft input part of the input method be shown to the user.
+ *
+ * @param flags Provides additional information about the show request.
+ * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
+ * @param resultReceiver The client requesting the show may wish to
+ * be told the impact of their request, which should be supplied here.
+ * The result code should be
+ * {@link InputMethodManager#RESULT_UNCHANGED_SHOWN InputMethodManager.RESULT_UNCHANGED_SHOWN},
+ * {@link InputMethodManager#RESULT_UNCHANGED_HIDDEN InputMethodManager.RESULT_UNCHANGED_HIDDEN},
+ * {@link InputMethodManager#RESULT_SHOWN InputMethodManager.RESULT_SHOWN}, or
+ * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
+ */
+ @MainThread
+ public void showSoftInput(int flags, ResultReceiver resultReceiver);
+
+ /**
+ * Request that any soft input part of the input method be hidden from the user.
+ * @param flags Provides additional information about the show request.
+ * Currently always 0.
+ * @param resultReceiver The client requesting the show may wish to
+ * be told the impact of their request, which should be supplied here.
+ * The result code should be
+ * {@link InputMethodManager#RESULT_UNCHANGED_SHOWN InputMethodManager.RESULT_UNCHANGED_SHOWN},
+ * {@link InputMethodManager#RESULT_UNCHANGED_HIDDEN InputMethodManager.RESULT_UNCHANGED_HIDDEN},
+ * {@link InputMethodManager#RESULT_SHOWN InputMethodManager.RESULT_SHOWN}, or
+ * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
+ * @param hideInputToken an opaque {@link android.os.Binder} token to identify which API call
+ * of {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}} is associated
+ * with this callback.
+ * @hide
+ */
+ @MainThread
+ public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ IBinder hideInputToken);
+
+ /**
+ * Request that any soft input part of the input method be hidden from the user.
+ * @param flags Provides additional information about the show request.
+ * Currently always 0.
+ * @param resultReceiver The client requesting the show may wish to
+ * be told the impact of their request, which should be supplied here.
+ * The result code should be
+ * {@link InputMethodManager#RESULT_UNCHANGED_SHOWN InputMethodManager.RESULT_UNCHANGED_SHOWN},
+ * {@link InputMethodManager#RESULT_UNCHANGED_HIDDEN
+ * InputMethodManager.RESULT_UNCHANGED_HIDDEN},
+ * {@link InputMethodManager#RESULT_SHOWN InputMethodManager.RESULT_SHOWN}, or
+ * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
+ */
+ @MainThread
+ public void hideSoftInput(int flags, ResultReceiver resultReceiver);
+
+ /**
+ * Notify that the input method subtype is being changed in the same input method.
+ * @param subtype New subtype of the notified input method
+ */
+ @MainThread
+ public void changeInputMethodSubtype(InputMethodSubtype subtype);
+
+ /**
+ * Update token of the client window requesting {@link #showSoftInput(int, ResultReceiver)}
+ * @param showInputToken placeholder app window token for window requesting
+ * {@link InputMethodManager#showSoftInput(View, int)}
+ * @hide
+ */
+ public void setCurrentShowInputToken(IBinder showInputToken);
+
+ /**
+ * Update token of the client window requesting {@link #hideSoftInput(int, ResultReceiver)}
+ * @param hideInputToken placeholder app window token for window requesting
+ * {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}
+ * @hide
+ */
+ public void setCurrentHideInputToken(IBinder hideInputToken);
+
+}
diff --git a/android/view/inputmethod/InputMethodInfo.java b/android/view/inputmethod/InputMethodInfo.java
new file mode 100644
index 0000000..9b463bb
--- /dev/null
+++ b/android/view/inputmethod/InputMethodInfo.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2007-2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.inputmethodservice.InputMethodService;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Printer;
+import android.util.Slog;
+import android.util.Xml;
+import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is used to specify meta information of an input method.
+ *
+ * <p>It should be defined in an XML resource file with an {@code <input-method>} element.
+ * For more information, see the guide to
+ * <a href="{@docRoot}guide/topics/text/creating-input-method.html">
+ * Creating an Input Method</a>.</p>
+ *
+ * @see InputMethodSubtype
+ *
+ * @attr ref android.R.styleable#InputMethod_settingsActivity
+ * @attr ref android.R.styleable#InputMethod_isDefault
+ * @attr ref android.R.styleable#InputMethod_supportsSwitchingToNextInputMethod
+ * @attr ref android.R.styleable#InputMethod_supportsInlineSuggestions
+ * @attr ref android.R.styleable#InputMethod_suppressesSpellChecker
+ * @attr ref android.R.styleable#InputMethod_showInInputMethodPicker
+ * @attr ref android.R.styleable#InputMethod_configChanges
+ */
+public final class InputMethodInfo implements Parcelable {
+ static final String TAG = "InputMethodInfo";
+
+ /**
+ * The Service that implements this input method component.
+ */
+ final ResolveInfo mService;
+
+ /**
+ * IME only supports VR mode.
+ */
+ final boolean mIsVrOnly;
+
+ /**
+ * The unique string Id to identify the input method. This is generated
+ * from the input method component.
+ */
+ final String mId;
+
+ /**
+ * The input method setting activity's name, used by the system settings to
+ * launch the setting activity of this input method.
+ */
+ final String mSettingsActivityName;
+
+ /**
+ * The resource in the input method's .apk that holds a boolean indicating
+ * whether it should be considered the default input method for this
+ * system. This is a resource ID instead of the final value so that it
+ * can change based on the configuration (in particular locale).
+ */
+ final int mIsDefaultResId;
+
+ /**
+ * An array-like container of the subtypes.
+ */
+ @UnsupportedAppUsage
+ private final InputMethodSubtypeArray mSubtypes;
+
+ private final boolean mIsAuxIme;
+
+ /**
+ * Caveat: mForceDefault must be false for production. This flag is only for test.
+ */
+ private final boolean mForceDefault;
+
+ /**
+ * The flag whether this IME supports ways to switch to a next input method (e.g. globe key.)
+ */
+ private final boolean mSupportsSwitchingToNextInputMethod;
+
+ /**
+ * The flag whether this IME supports inline suggestions.
+ */
+ private final boolean mInlineSuggestionsEnabled;
+
+ /**
+ * The flag whether this IME suppresses spell checker.
+ */
+ private final boolean mSuppressesSpellChecker;
+
+ /**
+ * The flag whether this IME should be shown as an option in the IME picker.
+ */
+ private final boolean mShowInInputMethodPicker;
+
+ /**
+ * The flag for configurations IME assumes the responsibility for handling in
+ * {@link InputMethodService#onConfigurationChanged(Configuration)}}.
+ */
+ private final int mHandledConfigChanges;
+
+ /**
+ * @param service the {@link ResolveInfo} corresponds in which the IME is implemented.
+ * @return a unique ID to be returned by {@link #getId()}. We have used
+ * {@link ComponentName#flattenToShortString()} for this purpose (and it is already
+ * unrealistic to switch to a different scheme as it is already implicitly assumed in
+ * many places).
+ * @hide
+ */
+ public static String computeId(@NonNull ResolveInfo service) {
+ final ServiceInfo si = service.serviceInfo;
+ return new ComponentName(si.packageName, si.name).flattenToShortString();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The Context in which we are parsing the input method.
+ * @param service The ResolveInfo returned from the package manager about
+ * this input method's component.
+ */
+ public InputMethodInfo(Context context, ResolveInfo service)
+ throws XmlPullParserException, IOException {
+ this(context, service, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The Context in which we are parsing the input method.
+ * @param service The ResolveInfo returned from the package manager about
+ * this input method's component.
+ * @param additionalSubtypes additional subtypes being added to this InputMethodInfo
+ * @hide
+ */
+ public InputMethodInfo(Context context, ResolveInfo service,
+ List<InputMethodSubtype> additionalSubtypes)
+ throws XmlPullParserException, IOException {
+ mService = service;
+ ServiceInfo si = service.serviceInfo;
+ mId = computeId(service);
+ boolean isAuxIme = true;
+ boolean supportsSwitchingToNextInputMethod = false; // false as default
+ boolean inlineSuggestionsEnabled = false; // false as default
+ boolean suppressesSpellChecker = false; // false as default
+ boolean showInInputMethodPicker = true; // true as default
+ mForceDefault = false;
+
+ PackageManager pm = context.getPackageManager();
+ String settingsActivityComponent = null;
+ boolean isVrOnly;
+ int isDefaultResId = 0;
+
+ XmlResourceParser parser = null;
+ final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+ try {
+ parser = si.loadXmlMetaData(pm, InputMethod.SERVICE_META_DATA);
+ if (parser == null) {
+ throw new XmlPullParserException("No "
+ + InputMethod.SERVICE_META_DATA + " meta-data");
+ }
+
+ Resources res = pm.getResourcesForApplication(si.applicationInfo);
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ String nodeName = parser.getName();
+ if (!"input-method".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data does not start with input-method tag");
+ }
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.InputMethod);
+ settingsActivityComponent = sa.getString(
+ com.android.internal.R.styleable.InputMethod_settingsActivity);
+ isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false);
+ isDefaultResId = sa.getResourceId(
+ com.android.internal.R.styleable.InputMethod_isDefault, 0);
+ supportsSwitchingToNextInputMethod = sa.getBoolean(
+ com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod,
+ false);
+ inlineSuggestionsEnabled = sa.getBoolean(
+ com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions, false);
+ suppressesSpellChecker = sa.getBoolean(
+ com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false);
+ showInInputMethodPicker = sa.getBoolean(
+ com.android.internal.R.styleable.InputMethod_showInInputMethodPicker, true);
+ mHandledConfigChanges = sa.getInt(
+ com.android.internal.R.styleable.InputMethod_configChanges, 0);
+ sa.recycle();
+
+ final int depth = parser.getDepth();
+ // Parse all subtypes
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.START_TAG) {
+ nodeName = parser.getName();
+ if (!"subtype".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data in input-method does not start with subtype tag");
+ }
+ final TypedArray a = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.InputMethod_Subtype);
+ final InputMethodSubtype subtype = new InputMethodSubtypeBuilder()
+ .setSubtypeNameResId(a.getResourceId(com.android.internal.R.styleable
+ .InputMethod_Subtype_label, 0))
+ .setSubtypeIconResId(a.getResourceId(com.android.internal.R.styleable
+ .InputMethod_Subtype_icon, 0))
+ .setLanguageTag(a.getString(com.android.internal.R.styleable
+ .InputMethod_Subtype_languageTag))
+ .setSubtypeLocale(a.getString(com.android.internal.R.styleable
+ .InputMethod_Subtype_imeSubtypeLocale))
+ .setSubtypeMode(a.getString(com.android.internal.R.styleable
+ .InputMethod_Subtype_imeSubtypeMode))
+ .setSubtypeExtraValue(a.getString(com.android.internal.R.styleable
+ .InputMethod_Subtype_imeSubtypeExtraValue))
+ .setIsAuxiliary(a.getBoolean(com.android.internal.R.styleable
+ .InputMethod_Subtype_isAuxiliary, false))
+ .setOverridesImplicitlyEnabledSubtype(a.getBoolean(
+ com.android.internal.R.styleable
+ .InputMethod_Subtype_overridesImplicitlyEnabledSubtype, false))
+ .setSubtypeId(a.getInt(com.android.internal.R.styleable
+ .InputMethod_Subtype_subtypeId, 0 /* use Arrays.hashCode */))
+ .setIsAsciiCapable(a.getBoolean(com.android.internal.R.styleable
+ .InputMethod_Subtype_isAsciiCapable, false)).build();
+ if (!subtype.isAuxiliary()) {
+ isAuxIme = false;
+ }
+ subtypes.add(subtype);
+ }
+ }
+ } catch (NameNotFoundException | IndexOutOfBoundsException | NumberFormatException e) {
+ throw new XmlPullParserException(
+ "Unable to create context for: " + si.packageName);
+ } finally {
+ if (parser != null) parser.close();
+ }
+
+ if (subtypes.size() == 0) {
+ isAuxIme = false;
+ }
+
+ if (additionalSubtypes != null) {
+ final int N = additionalSubtypes.size();
+ for (int i = 0; i < N; ++i) {
+ final InputMethodSubtype subtype = additionalSubtypes.get(i);
+ if (!subtypes.contains(subtype)) {
+ subtypes.add(subtype);
+ } else {
+ Slog.w(TAG, "Duplicated subtype definition found: "
+ + subtype.getLocale() + ", " + subtype.getMode());
+ }
+ }
+ }
+ mSubtypes = new InputMethodSubtypeArray(subtypes);
+ mSettingsActivityName = settingsActivityComponent;
+ mIsDefaultResId = isDefaultResId;
+ mIsAuxIme = isAuxIme;
+ mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
+ mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
+ mSuppressesSpellChecker = suppressesSpellChecker;
+ mShowInInputMethodPicker = showInInputMethodPicker;
+ mIsVrOnly = isVrOnly;
+ }
+
+ InputMethodInfo(Parcel source) {
+ mId = source.readString();
+ mSettingsActivityName = source.readString();
+ mIsDefaultResId = source.readInt();
+ mIsAuxIme = source.readInt() == 1;
+ mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
+ mInlineSuggestionsEnabled = source.readInt() == 1;
+ mSuppressesSpellChecker = source.readBoolean();
+ mShowInInputMethodPicker = source.readBoolean();
+ mIsVrOnly = source.readBoolean();
+ mService = ResolveInfo.CREATOR.createFromParcel(source);
+ mSubtypes = new InputMethodSubtypeArray(source);
+ mHandledConfigChanges = source.readInt();
+ mForceDefault = false;
+ }
+
+ /**
+ * Temporary API for creating a built-in input method for test.
+ */
+ public InputMethodInfo(String packageName, String className,
+ CharSequence label, String settingsActivity) {
+ this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */,
+ settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
+ false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
+ 0 /* handledConfigChanges */);
+ }
+
+ /**
+ * Temporary API for creating a built-in input method for test.
+ * @hide
+ */
+ @TestApi
+ public InputMethodInfo(@NonNull String packageName, @NonNull String className,
+ @NonNull CharSequence label, @NonNull String settingsActivity,
+ int handledConfigChanges) {
+ this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */,
+ settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
+ false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ false /* inlineSuggestionsEnabled */, false /* isVrOnly */, handledConfigChanges);
+ }
+
+ /**
+ * Temporary API for creating a built-in input method for test.
+ * @hide
+ */
+ public InputMethodInfo(ResolveInfo ri, boolean isAuxIme,
+ String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId,
+ boolean forceDefault) {
+ this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault,
+ true /* supportsSwitchingToNextInputMethod */, false /* inlineSuggestionsEnabled */,
+ false /* isVrOnly */, 0 /* handledconfigChanges */);
+ }
+
+ /**
+ * Temporary API for creating a built-in input method for test.
+ * @hide
+ */
+ public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
+ List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault,
+ boolean supportsSwitchingToNextInputMethod, boolean isVrOnly) {
+ this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault,
+ supportsSwitchingToNextInputMethod, false /* inlineSuggestionsEnabled */, isVrOnly,
+ 0 /* handledConfigChanges */);
+ }
+
+ /**
+ * Temporary API for creating a built-in input method for test.
+ * @hide
+ */
+ public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
+ List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault,
+ boolean supportsSwitchingToNextInputMethod, boolean inlineSuggestionsEnabled,
+ boolean isVrOnly, int handledConfigChanges) {
+ final ServiceInfo si = ri.serviceInfo;
+ mService = ri;
+ mId = new ComponentName(si.packageName, si.name).flattenToShortString();
+ mSettingsActivityName = settingsActivity;
+ mIsDefaultResId = isDefaultResId;
+ mIsAuxIme = isAuxIme;
+ mSubtypes = new InputMethodSubtypeArray(subtypes);
+ mForceDefault = forceDefault;
+ mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
+ mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
+ mSuppressesSpellChecker = false;
+ mShowInInputMethodPicker = true;
+ mIsVrOnly = isVrOnly;
+ mHandledConfigChanges = handledConfigChanges;
+ }
+
+ private static ResolveInfo buildFakeResolveInfo(String packageName, String className,
+ CharSequence label) {
+ ResolveInfo ri = new ResolveInfo();
+ ServiceInfo si = new ServiceInfo();
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.packageName = packageName;
+ ai.enabled = true;
+ si.applicationInfo = ai;
+ si.enabled = true;
+ si.packageName = packageName;
+ si.name = className;
+ si.exported = true;
+ si.nonLocalizedLabel = label;
+ ri.serviceInfo = si;
+ return ri;
+ }
+
+ /**
+ * Return a unique ID for this input method. The ID is generated from
+ * the package and class name implementing the method.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Return the .apk package that implements this input method.
+ */
+ public String getPackageName() {
+ return mService.serviceInfo.packageName;
+ }
+
+ /**
+ * Return the class name of the service component that implements
+ * this input method.
+ */
+ public String getServiceName() {
+ return mService.serviceInfo.name;
+ }
+
+ /**
+ * Return the raw information about the Service implementing this
+ * input method. Do not modify the returned object.
+ */
+ public ServiceInfo getServiceInfo() {
+ return mService.serviceInfo;
+ }
+
+ /**
+ * Return the component of the service that implements this input
+ * method.
+ */
+ public ComponentName getComponent() {
+ return new ComponentName(mService.serviceInfo.packageName,
+ mService.serviceInfo.name);
+ }
+
+ /**
+ * Load the user-displayed label for this input method.
+ *
+ * @param pm Supply a PackageManager used to load the input method's
+ * resources.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ return mService.loadLabel(pm);
+ }
+
+ /**
+ * Load the user-displayed icon for this input method.
+ *
+ * @param pm Supply a PackageManager used to load the input method's
+ * resources.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ return mService.loadIcon(pm);
+ }
+
+ /**
+ * Return the class name of an activity that provides a settings UI for
+ * the input method. You can launch this activity be starting it with
+ * an {@link android.content.Intent} whose action is MAIN and with an
+ * explicit {@link android.content.ComponentName}
+ * composed of {@link #getPackageName} and the class name returned here.
+ *
+ * <p>A null will be returned if there is no settings activity associated
+ * with the input method.</p>
+ */
+ public String getSettingsActivity() {
+ return mSettingsActivityName;
+ }
+
+ /**
+ * Returns true if IME supports VR mode only.
+ * @hide
+ */
+ public boolean isVrOnly() {
+ return mIsVrOnly;
+ }
+
+ /**
+ * Return the count of the subtypes of Input Method.
+ */
+ public int getSubtypeCount() {
+ return mSubtypes.getCount();
+ }
+
+ /**
+ * Return the Input Method's subtype at the specified index.
+ *
+ * @param index the index of the subtype to return.
+ */
+ public InputMethodSubtype getSubtypeAt(int index) {
+ return mSubtypes.get(index);
+ }
+
+ /**
+ * Return the resource identifier of a resource inside of this input
+ * method's .apk that determines whether it should be considered a
+ * default input method for the system.
+ */
+ public int getIsDefaultResourceId() {
+ return mIsDefaultResId;
+ }
+
+ /**
+ * Return whether or not this ime is a default ime or not.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean isDefault(Context context) {
+ if (mForceDefault) {
+ return true;
+ }
+ try {
+ if (getIsDefaultResourceId() == 0) {
+ return false;
+ }
+ final Resources res = context.createPackageContext(getPackageName(), 0).getResources();
+ return res.getBoolean(getIsDefaultResourceId());
+ } catch (NameNotFoundException | NotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the bit mask of kinds of configuration changes that this IME
+ * can handle itself (without being restarted by the system).
+ *
+ * @attr ref android.R.styleable#InputMethod_configChanges
+ */
+ @ActivityInfo.Config
+ public int getConfigChanges() {
+ return mHandledConfigChanges;
+ }
+
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + "mId=" + mId
+ + " mSettingsActivityName=" + mSettingsActivityName
+ + " mIsVrOnly=" + mIsVrOnly
+ + " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod
+ + " mInlineSuggestionsEnabled=" + mInlineSuggestionsEnabled
+ + " mSuppressesSpellChecker=" + mSuppressesSpellChecker
+ + " mShowInInputMethodPicker=" + mShowInInputMethodPicker);
+ pw.println(prefix + "mIsDefaultResId=0x"
+ + Integer.toHexString(mIsDefaultResId));
+ pw.println(prefix + "Service:");
+ mService.dump(pw, prefix + " ");
+ }
+
+ @Override
+ public String toString() {
+ return "InputMethodInfo{" + mId
+ + ", settings: "
+ + mSettingsActivityName + "}";
+ }
+
+ /**
+ * Used to test whether the given parameter object is an
+ * {@link InputMethodInfo} and its Id is the same to this one.
+ *
+ * @return true if the given parameter object is an
+ * {@link InputMethodInfo} and its Id is the same to this one.
+ */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) return true;
+ if (o == null) return false;
+
+ if (!(o instanceof InputMethodInfo)) return false;
+
+ InputMethodInfo obj = (InputMethodInfo) o;
+ return mId.equals(obj.mId);
+ }
+
+ @Override
+ public int hashCode() {
+ return mId.hashCode();
+ }
+
+ /**
+ * @hide
+ * @return {@code true} if the IME is a trusted system component (e.g. pre-installed)
+ */
+ public boolean isSystem() {
+ return (mService.serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isAuxiliaryIme() {
+ return mIsAuxIme;
+ }
+
+ /**
+ * @return true if this input method supports ways to switch to a next input method.
+ * @hide
+ */
+ public boolean supportsSwitchingToNextInputMethod() {
+ return mSupportsSwitchingToNextInputMethod;
+ }
+
+ /**
+ * @return true if this input method supports inline suggestions.
+ * @hide
+ */
+ public boolean isInlineSuggestionsEnabled() {
+ return mInlineSuggestionsEnabled;
+ }
+
+ /**
+ * Return {@code true} if this input method suppresses spell checker.
+ */
+ public boolean suppressesSpellChecker() {
+ return mSuppressesSpellChecker;
+ }
+
+ /**
+ * Returns {@code true} if this input method should be shown in menus for selecting an Input
+ * Method, such as the system Input Method Picker. This is {@code false} if the IME is intended
+ * to be accessed programmatically.
+ */
+ public boolean shouldShowInInputMethodPicker() {
+ return mShowInInputMethodPicker;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeString(mSettingsActivityName);
+ dest.writeInt(mIsDefaultResId);
+ dest.writeInt(mIsAuxIme ? 1 : 0);
+ dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
+ dest.writeInt(mInlineSuggestionsEnabled ? 1 : 0);
+ dest.writeBoolean(mSuppressesSpellChecker);
+ dest.writeBoolean(mShowInInputMethodPicker);
+ dest.writeBoolean(mIsVrOnly);
+ mService.writeToParcel(dest, flags);
+ mSubtypes.writeToParcel(dest);
+ dest.writeInt(mHandledConfigChanges);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<InputMethodInfo> CREATOR
+ = new Parcelable.Creator<InputMethodInfo>() {
+ @Override
+ public InputMethodInfo createFromParcel(Parcel source) {
+ return new InputMethodInfo(source);
+ }
+
+ @Override
+ public InputMethodInfo[] newArray(int size) {
+ return new InputMethodInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/inputmethod/InputMethodManager.java b/android/view/inputmethod/InputMethodManager.java
new file mode 100644
index 0000000..42d77cd
--- /dev/null
+++ b/android/view/inputmethod/InputMethodManager.java
@@ -0,0 +1,3244 @@
+/*
+ * Copyright (C) 2007-2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
+import static android.util.imetracing.ImeTracing.PROTO_ARG;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.EDITOR_INFO;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_INSETS_SOURCE_CONSUMER;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INPUT_CONNECTION;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INPUT_CONNECTION_CALL;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INPUT_METHOD_MANAGER;
+import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.VIEW_ROOT_IMPL;
+import static android.view.inputmethod.InputMethodManagerProto.ACTIVE;
+import static android.view.inputmethod.InputMethodManagerProto.CUR_ID;
+import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE;
+import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING;
+
+import static com.android.internal.inputmethod.StartInputReason.WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION;
+import static com.android.internal.inputmethod.StartInputReason.WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION;
+
+import android.annotation.DisplayContext;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.annotation.UserIdInt;
+import android.app.ActivityThread;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+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.ResultReceiver;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.style.SuggestionSpan;
+import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
+import android.util.PrintWriterPrinter;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.imetracing.ImeTracing;
+import android.util.proto.ProtoOutputStream;
+import android.view.Display;
+import android.view.ImeFocusController;
+import android.view.ImeInsetsSourceConsumer;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventSender;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.autofill.AutofillManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.inputmethod.StartInputFlags;
+import com.android.internal.inputmethod.StartInputReason;
+import com.android.internal.inputmethod.UnbindReason;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.view.IInputConnectionWrapper;
+import com.android.internal.view.IInputContext;
+import com.android.internal.view.IInputMethodClient;
+import com.android.internal.view.IInputMethodManager;
+import com.android.internal.view.IInputMethodSession;
+import com.android.internal.view.InputBindResult;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Central system API to the overall input method framework (IMF) architecture,
+ * which arbitrates interaction between applications and the current input method.
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#ArchitectureOverview">Architecture Overview</a>
+ * <li><a href="#Applications">Applications</a>
+ * <li><a href="#InputMethods">Input Methods</a>
+ * <li><a href="#Security">Security</a>
+ * </ol>
+ *
+ * <a name="ArchitectureOverview"></a>
+ * <h3>Architecture Overview</h3>
+ *
+ * <p>There are three primary parties involved in the input method
+ * framework (IMF) architecture:</p>
+ *
+ * <ul>
+ * <li> The <strong>input method 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> An <strong>input method (IME)</strong> implements a particular
+ * interaction model allowing the user to generate text. The system binds
+ * to the current input method that is in use, causing it to be created and run,
+ * and tells it when to hide and show its UI. Only one IME is running at a time.
+ * <li> Multiple <strong>client applications</strong> arbitrate with the input
+ * method manager for input focus and control over the state of the IME. Only
+ * one such client is ever active (working with the IME) at a time.
+ * </ul>
+ *
+ *
+ * <a name="Applications"></a>
+ * <h3>Applications</h3>
+ *
+ * <p>In most cases, applications that are using the standard
+ * {@link android.widget.TextView} or its subclasses will have little they need
+ * to do to work well with soft input methods. The main things you need to
+ * be aware of are:</p>
+ *
+ * <ul>
+ * <li> Properly set the {@link android.R.attr#inputType} in your editable
+ * text views, so that the input method will have enough context to help the
+ * user in entering text into them.
+ * <li> Deal well with losing screen space when the input method is
+ * displayed. Ideally an application should handle its window being resized
+ * smaller, but it can rely on the system performing panning of the window
+ * if needed. You should set the {@link android.R.attr#windowSoftInputMode}
+ * attribute on your activity or the corresponding values on windows you
+ * create to help the system determine whether to pan or resize (it will
+ * try to determine this automatically but may get it wrong).
+ * <li> You can also control the preferred soft input state (open, closed, etc)
+ * for your window using the same {@link android.R.attr#windowSoftInputMode}
+ * attribute.
+ * </ul>
+ *
+ * <p>More finer-grained control is available through the APIs here to directly
+ * interact with the IMF and its IME -- either showing or hiding the input
+ * area, letting the user pick an input method, etc.</p>
+ *
+ * <p>For the rare people amongst us writing their own text editors, you
+ * will need to implement {@link android.view.View#onCreateInputConnection}
+ * to return a new instance of your own {@link InputConnection} interface
+ * allowing the IME to interact with your editor.</p>
+ *
+ *
+ * <a name="InputMethods"></a>
+ * <h3>Input Methods</h3>
+ *
+ * <p>An input method (IME) is implemented
+ * as a {@link android.app.Service}, typically deriving from
+ * {@link android.inputmethodservice.InputMethodService}. It must provide
+ * the core {@link InputMethod} interface, though this is normally handled by
+ * {@link android.inputmethodservice.InputMethodService} and implementors will
+ * only need to deal with the higher-level API there.</p>
+ *
+ * See the {@link android.inputmethodservice.InputMethodService} class for
+ * more information on implementing IMEs.
+ *
+ *
+ * <a name="Security"></a>
+ * <h3>Security</h3>
+ *
+ * <p>There are a lot of security issues associated with input methods,
+ * since they essentially have freedom to completely drive the UI and monitor
+ * everything the user enters. The Android input method framework also allows
+ * arbitrary third party IMEs, so care must be taken to restrict their
+ * selection and interactions.</p>
+ *
+ * <p>Here are some key points about the security architecture behind the
+ * IMF:</p>
+ *
+ * <ul>
+ * <li> <p>Only the system is allowed to directly access an IME's
+ * {@link InputMethod} interface, via the
+ * {@link android.Manifest.permission#BIND_INPUT_METHOD} permission. This is
+ * enforced in the system by not binding to an input method service that does
+ * not require this permission, so the system can guarantee no other untrusted
+ * clients are accessing the current input method outside of its control.</p>
+ *
+ * <li> <p>There may be many client processes of the IMF, but only one may
+ * be active at a time. The inactive clients can not interact with key
+ * parts of the IMF through the mechanisms described below.</p>
+ *
+ * <li> <p>Clients of an input method are only given access to its
+ * {@link InputMethodSession} interface. One instance of this interface is
+ * created for each client, and only calls from the session associated with
+ * the active client will be processed by the current IME. This is enforced
+ * by {@link android.inputmethodservice.AbstractInputMethodService} for normal
+ * IMEs, but must be explicitly handled by an IME that is customizing the
+ * raw {@link InputMethodSession} implementation.</p>
+ *
+ * <li> <p>Only the active client's {@link InputConnection} will accept
+ * operations. The IMF tells each client process whether it is active, and
+ * the framework enforces that in inactive processes calls on to the current
+ * InputConnection will be ignored. This ensures that the current IME can
+ * only deliver events and text edits to the UI that the user sees as
+ * being in focus.</p>
+ *
+ * <li> <p>An IME can never interact with an {@link InputConnection} while
+ * the screen is off. This is enforced by making all clients inactive while
+ * the screen is off, and prevents bad IMEs from driving the UI when the user
+ * can not be aware of its behavior.</p>
+ *
+ * <li> <p>A client application can ask that the system let the user pick a
+ * new IME, but can not programmatically switch to one itself. This avoids
+ * malicious applications from switching the user to their own IME, which
+ * remains running when the user navigates away to another application. An
+ * IME, on the other hand, <em>is</em> allowed to programmatically switch
+ * the system to another IME, since it already has full control of user
+ * input.</p>
+ *
+ * <li> <p>The user must explicitly enable a new IME in settings before
+ * they can switch to it, to confirm with the system that they know about it
+ * and want to make it available for use.</p>
+ * </ul>
+ */
+@SystemService(Context.INPUT_METHOD_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_INPUT_METHODS)
+public final class InputMethodManager {
+ static final boolean DEBUG = false;
+ static final String TAG = "InputMethodManager";
+
+ static final String PENDING_EVENT_COUNTER = "aq:imm";
+
+ private static final int NOT_A_SUBTYPE_ID = -1;
+
+ /**
+ * A constant that represents Voice IME.
+ *
+ * @see InputMethodSubtype#getMode()
+ */
+ private static final String SUBTYPE_MODE_VOICE = "voice";
+
+ /**
+ * Ensures that {@link #sInstance} becomes non-{@code null} for application that have directly
+ * or indirectly relied on {@link #sInstance} via reflection or something like that.
+ *
+ * <p>Here are scenarios we know and there could be more scenarios we are not
+ * aware of right know.</p>
+ *
+ * <ul>
+ * <li>Apps that directly access {@link #sInstance} via reflection, which is currently
+ * allowed because of {@link UnsupportedAppUsage} annotation. Currently
+ * {@link android.view.WindowManagerGlobal#getWindowSession()} is likely to guarantee that
+ * {@link #sInstance} is not {@code null} when such an app is accessing it, but removing
+ * that code from {@link android.view.WindowManagerGlobal#getWindowSession()} can reveal
+ * untested code paths in their apps, which probably happen in an early startup time of that
+ * app.</li>
+ * <li>Apps that directly access {@link #peekInstance()} via reflection, which is currently
+ * allowed because of {@link UnsupportedAppUsage} annotation. Currently
+ * {@link android.view.WindowManagerGlobal#getWindowSession()} is likely to guarantee that
+ * {@link #peekInstance()} returns non-{@code null} object when such an app is calling
+ * {@link #peekInstance()}, but removing that code from
+ * {@link android.view.WindowManagerGlobal#getWindowSession()} can reveal untested code
+ * paths in their apps, which probably happen in an early startup time of that app. The good
+ * news is that unlike {@link #sInstance}'s case we can at least work around this scenario
+ * by changing the semantics of {@link #peekInstance()}, which is currently defined as
+ * "retrieve the global {@link InputMethodManager} instance, if it exists" to something that
+ * always returns non-{@code null} {@link InputMethodManager}. However, introducing such an
+ * workaround can also trigger different compatibility issues if {@link #peekInstance()} was
+ * called before {@link android.view.WindowManagerGlobal#getWindowSession()} and it expected
+ * {@link #peekInstance()} to return {@code null} as written in the JavaDoc.</li>
+ * </ul>
+ *
+ * <p>Since this is purely a compatibility hack, this method must be used only from
+ * {@link android.view.WindowManagerGlobal#getWindowSession()} and {@link #getInstance()}.</p>
+ *
+ * <p>TODO(Bug 116157766): Remove this method once we clean up {@link UnsupportedAppUsage}.</p>
+ * @hide
+ */
+ public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() {
+ forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
+ }
+
+ private static final Object sLock = new Object();
+
+ /**
+ * @deprecated This cannot be compatible with multi-display. Please do not use this.
+ */
+ @Deprecated
+ @GuardedBy("sLock")
+ @UnsupportedAppUsage
+ static InputMethodManager sInstance;
+
+ /**
+ * Global map between display to {@link InputMethodManager}.
+ *
+ * <p>Currently this map works like a so-called leaky singleton. Once an instance is registered
+ * for the associated display ID, that instance will never be garbage collected.</p>
+ *
+ * <p>TODO(Bug 116699479): Implement instance clean up mechanism.</p>
+ */
+ @GuardedBy("sLock")
+ private static final SparseArray<InputMethodManager> sInstanceMap = new SparseArray<>();
+
+ /**
+ * Timeout in milliseconds for delivering a key to an IME.
+ */
+ static final long INPUT_METHOD_NOT_RESPONDING_TIMEOUT = 2500;
+
+ /** @hide */
+ public static final int DISPATCH_IN_PROGRESS = -1;
+
+ /** @hide */
+ public static final int DISPATCH_NOT_HANDLED = 0;
+
+ /** @hide */
+ public static final int DISPATCH_HANDLED = 1;
+
+ /** @hide */
+ public static final int SHOW_IM_PICKER_MODE_AUTO = 0;
+ /** @hide */
+ public static final int SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES = 1;
+ /** @hide */
+ public static final int SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES = 2;
+
+ @UnsupportedAppUsage
+ final IInputMethodManager mService;
+ final Looper mMainLooper;
+
+ // For scheduling work on the main thread. This also serves as our
+ // global lock.
+ // Remark on @UnsupportedAppUsage: there were context leaks on old versions
+ // of android (b/37043700), so developers used this field to perform manual clean up.
+ // Leaks were fixed, hacks were backported to AppCompatActivity,
+ // so an access to the field is closed.
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ final H mH;
+
+ // Our generic input connection if the current target does not have its own.
+ final IInputContext mIInputContext;
+
+ private final int mDisplayId;
+
+ /**
+ * True if this input method client is active, initially false.
+ */
+ boolean mActive = false;
+
+ /**
+ * {@code true} if next {@link ImeFocusController#onPostWindowFocus} needs to
+ * restart input.
+ */
+ private boolean mRestartOnNextWindowFocus = true;
+
+ /**
+ * As reported by IME through InputConnection.
+ */
+ boolean mFullscreenMode;
+
+ // -----------------------------------------------------------
+
+ /**
+ * This is the root view of the overall window that currently has input
+ * method focus.
+ */
+ @GuardedBy("mH")
+ ViewRootImpl mCurRootView;
+ /**
+ * This is set when we are in the process of connecting, to determine
+ * when we have actually finished.
+ */
+ boolean mServedConnecting;
+ /**
+ * This is non-null when we have connected the served view; it holds
+ * the attributes that were last retrieved from the served view and given
+ * to the input connection.
+ */
+ EditorInfo mCurrentTextBoxAttribute;
+ /**
+ * The InputConnection that was last retrieved from the served view.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ IInputConnectionWrapper mServedInputConnectionWrapper;
+ /**
+ * The completions that were last provided by the served view.
+ */
+ CompletionInfo[] mCompletions;
+
+ // Cursor position on the screen.
+ @UnsupportedAppUsage
+ Rect mTmpCursorRect = new Rect();
+ @UnsupportedAppUsage
+ Rect mCursorRect = new Rect();
+ int mCursorSelStart;
+ int mCursorSelEnd;
+ int mCursorCandStart;
+ int mCursorCandEnd;
+
+ /**
+ * The instance that has previously been sent to the input method.
+ */
+ private CursorAnchorInfo mCursorAnchorInfo = null;
+
+ /**
+ * As reported by {@link InputBindResult}. This value is determined by
+ * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecking}.
+ */
+ @GuardedBy("mH")
+ private boolean mIsInputMethodSuppressingSpellChecker = false;
+
+ // -----------------------------------------------------------
+
+ /**
+ * Sequence number of this binding, as returned by the server.
+ */
+ int mBindSequence = -1;
+ /**
+ * ID of the method we are bound to.
+ */
+ @UnsupportedAppUsage
+ String mCurId;
+
+ /**
+ * Kept for {@link UnsupportedAppUsage}. Not officially maintained.
+ *
+ * @deprecated New code should use {@link #mCurrentInputMethodSession}.
+ */
+ @Deprecated
+ @GuardedBy("mH")
+ @Nullable
+ @UnsupportedAppUsage
+ IInputMethodSession mCurMethod;
+
+ /**
+ * Encapsulates IPCs to the currently connected InputMethodService.
+ */
+ @Nullable
+ @GuardedBy("mH")
+ private InputMethodSessionWrapper mCurrentInputMethodSession = null;
+
+ InputChannel mCurChannel;
+ ImeInputEventSender mCurSender;
+
+ private static final int REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE = 0x0;
+
+ /**
+ * The monitor mode for {@link #updateCursorAnchorInfo(View, CursorAnchorInfo)}.
+ */
+ private int mRequestUpdateCursorAnchorInfoMonitorMode = REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE;
+
+ /**
+ * Applies the IME visibility and listens for other state changes.
+ */
+ private ImeInsetsSourceConsumer mImeInsetsConsumer;
+
+ final Pool<PendingEvent> mPendingEventPool = new SimplePool<>(20);
+ final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20);
+
+ final DelegateImpl mDelegate = new DelegateImpl();
+
+ // -----------------------------------------------------------
+
+ static final int MSG_DUMP = 1;
+ static final int MSG_BIND = 2;
+ static final int MSG_UNBIND = 3;
+ static final int MSG_SET_ACTIVE = 4;
+ static final int MSG_SEND_INPUT_EVENT = 5;
+ static final int MSG_TIMEOUT_INPUT_EVENT = 6;
+ static final int MSG_FLUSH_INPUT_EVENT = 7;
+ static final int MSG_REPORT_FULLSCREEN_MODE = 10;
+
+ private static boolean isAutofillUIShowing(View servedView) {
+ AutofillManager afm = servedView.getContext().getSystemService(AutofillManager.class);
+ return afm != null && afm.isAutofillUiShowing();
+ }
+
+ /**
+ * Returns fallback {@link InputMethodManager} if the called one is not likely to be compatible
+ * with the given {@code view}.
+ *
+ * @param view {@link View} to be checked.
+ * @return {@code null} when it is unnecessary (or impossible) to use fallback
+ * {@link InputMethodManager} to which IME API calls need to be re-dispatched.
+ * Non-{@code null} {@link InputMethodManager} if this method believes it'd be safer to
+ * re-dispatch IME APIs calls on it.
+ */
+ @Nullable
+ private InputMethodManager getFallbackInputMethodManagerIfNecessary(@Nullable View view) {
+ if (view == null) {
+ return null;
+ }
+ // As evidenced in Bug 118341760, view.getViewRootImpl().getDisplayId() is supposed to be
+ // more reliable to determine with which display the given view is interacting than
+ // view.getContext().getDisplayId() / view.getContext().getSystemService(), which can be
+ // easily messed up by app developers (or library authors) by creating inconsistent
+ // ContextWrapper objects that re-dispatch those methods to other Context such as
+ // ApplicationContext.
+ final ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ if (viewRootImpl == null) {
+ return null;
+ }
+ final int viewRootDisplayId = viewRootImpl.getDisplayId();
+ if (viewRootDisplayId == mDisplayId) {
+ // Expected case. Good to go.
+ return null;
+ }
+ final InputMethodManager fallbackImm =
+ viewRootImpl.mContext.getSystemService(InputMethodManager.class);
+ if (fallbackImm == null) {
+ Log.v(TAG, "b/117267690: Failed to get non-null fallback IMM. view=" + view);
+ return null;
+ }
+ if (fallbackImm.mDisplayId != viewRootDisplayId) {
+ Log.v(TAG, "b/117267690: Failed to get fallback IMM with expected displayId="
+ + viewRootDisplayId + " actual IMM#displayId=" + fallbackImm.mDisplayId
+ + " view=" + view);
+ return null;
+ }
+ Log.v(TAG, "b/117267690: Display ID mismatch found."
+ + " ViewRootImpl displayId=" + viewRootDisplayId
+ + " InputMethodManager displayId=" + mDisplayId
+ + ". Use the right InputMethodManager instance to avoid performance overhead.",
+ new Throwable());
+ return fallbackImm;
+ }
+
+ private static boolean canStartInput(View servedView) {
+ // We can start input ether the servedView has window focus
+ // or the activity is showing autofill ui.
+ return servedView.hasWindowFocus() || isAutofillUIShowing(servedView);
+ }
+
+ /**
+ * Reports whether the IME is currently perceptible or not, according to the leash applied by
+ * {@link android.view.WindowInsetsController}.
+ * @hide
+ */
+ public void reportPerceptible(IBinder windowToken, boolean perceptible) {
+ try {
+ mService.reportPerceptibleAsync(windowToken, perceptible);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private final class DelegateImpl implements
+ ImeFocusController.InputMethodManagerDelegate {
+ /**
+ * Used by {@link ImeFocusController} to start input connection.
+ */
+ @Override
+ public boolean startInput(@StartInputReason int startInputReason, View focusedView,
+ @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
+ int windowFlags) {
+ final View servedView;
+ ImeTracing.getInstance().triggerClientDump(
+ "InputMethodManager.DelegateImpl#startInput", InputMethodManager.this,
+ null /* icProto */);
+ synchronized (mH) {
+ mCurrentTextBoxAttribute = null;
+ mCompletions = null;
+ mServedConnecting = true;
+ servedView = getServedViewLocked();
+ }
+ return startInputInner(startInputReason,
+ focusedView != null ? focusedView.getWindowToken() : null, startInputFlags,
+ softInputMode, windowFlags);
+ }
+
+ /**
+ * Used by {@link ImeFocusController} to finish input connection.
+ */
+ @Override
+ public void finishInput() {
+ ImeTracing.getInstance().triggerClientDump(
+ "InputMethodManager.DelegateImpl#finishInput", InputMethodManager.this,
+ null /* icProto */);
+ synchronized (mH) {
+ finishInputLocked();
+ }
+ }
+
+ /**
+ * Used by {@link ImeFocusController} to finish input connection and callback
+ * {@link InputMethodService#onFinishInput()}.
+ *
+ * This method is especially for when ImeFocusController received device screen-off event to
+ * ensure the entire finish input connection and the connection lifecycle callback to
+ * IME can be done for security concern.
+ */
+ @Override
+ public void finishInputAndReportToIme() {
+ synchronized (mH) {
+ finishInputLocked();
+ if (mCurrentInputMethodSession != null) {
+ mCurrentInputMethodSession.finishInput();
+ }
+ }
+ }
+
+ /**
+ * Used by {@link ImeFocusController} to hide current input method editor.
+ */
+ @Override
+ public void closeCurrentIme() {
+ closeCurrentInput();
+ }
+
+ /**
+ * For {@link ImeFocusController} to start input asynchronously when focus gain.
+ */
+ @Override
+ public void startInputAsyncOnWindowFocusGain(View focusedView,
+ @SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
+ int startInputFlags = getStartInputFlags(focusedView, 0);
+ startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS;
+
+ ImeTracing.getInstance().triggerClientDump(
+ "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain",
+ InputMethodManager.this, null /* icProto */);
+
+ final ImeFocusController controller = getFocusController();
+ if (controller == null) {
+ return;
+ }
+ if (controller.checkFocus(forceNewFocus, false)) {
+ // We need to restart input on the current focus view. This
+ // should be done in conjunction with telling the system service
+ // about the window gaining focus, to help make the transition
+ // smooth.
+ if (startInput(StartInputReason.WINDOW_FOCUS_GAIN,
+ focusedView, startInputFlags, softInputMode, windowFlags)) {
+ return;
+ }
+ }
+
+ synchronized (mH) {
+ // For some reason we didn't do a startInput + windowFocusGain, so
+ // we'll just do a window focus gain and call it a day.
+ try {
+ View servedView = controller.getServedView();
+ boolean nextFocusHasConnection = servedView != null && servedView == focusedView
+ && hasActiveConnection(focusedView);
+ if (DEBUG) {
+ Log.v(TAG, "Reporting focus gain, without startInput"
+ + ", nextFocusIsServedView=" + nextFocusHasConnection);
+ }
+
+ final int startInputReason = nextFocusHasConnection
+ ? WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION
+ : WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION;
+ // ignore the result
+ mService.startInputOrWindowGainedFocus(
+ startInputReason, mClient,
+ focusedView.getWindowToken(), startInputFlags, softInputMode,
+ windowFlags,
+ null,
+ null,
+ 0 /* missingMethodFlags */,
+ mCurRootView.mContext.getApplicationInfo().targetSdkVersion);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Used by {@link ImeFocusController} to finish current composing text.
+ */
+ @Override
+ public void finishComposingText() {
+ if (mServedInputConnectionWrapper != null) {
+ mServedInputConnectionWrapper.finishComposingText();
+ }
+ }
+
+ /**
+ * Used for {@link ImeFocusController} to set the current focused root view.
+ */
+ @Override
+ public void setCurrentRootView(ViewRootImpl rootView) {
+ synchronized (mH) {
+ mCurRootView = rootView;
+ }
+ }
+
+ /**
+ * Used for {@link ImeFocusController} to return if the root view from the
+ * controller is this {@link InputMethodManager} currently focused.
+ * TODO: Address event-order problem when get current root view in multi-threads.
+ */
+ @Override
+ public boolean isCurrentRootView(ViewRootImpl rootView) {
+ synchronized (mH) {
+ return mCurRootView == rootView;
+ }
+ }
+
+ /**
+ * For {@link ImeFocusController#checkFocus} if needed to force check new focus.
+ */
+ @Override
+ public boolean isRestartOnNextWindowFocus(boolean reset) {
+ final boolean result = mRestartOnNextWindowFocus;
+ if (reset) {
+ mRestartOnNextWindowFocus = false;
+ }
+ return result;
+ }
+
+ /**
+ * Checks whether the active input connection (if any) is for the given view.
+ *
+ * TODO(b/182259171): Clean-up hasActiveConnection to simplify the logic.
+ *
+ * Note that this method is only intended for restarting input after focus gain
+ * (e.g. b/160391516), DO NOT leverage this method to do another check.
+ */
+ @Override
+ public boolean hasActiveConnection(View view) {
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view)
+ || mCurrentInputMethodSession == null) {
+ return false;
+ }
+
+ return mServedInputConnectionWrapper != null
+ && mServedInputConnectionWrapper.isActive()
+ && mServedInputConnectionWrapper.getServedView() == view;
+ }
+ }
+ }
+
+ /** @hide */
+ public DelegateImpl getDelegate() {
+ return mDelegate;
+ }
+
+ /**
+ * Checks whether the active input connection (if any) is for the given view.
+ *
+ * @hide
+ * @see ImeFocusController#getImmDelegate()#hasActiveInputConnection(View)
+ */
+ @TestApi
+ public boolean hasActiveInputConnection(@Nullable View view) {
+ return mDelegate.hasActiveConnection(view);
+ }
+
+ private View getServedViewLocked() {
+ return mCurRootView != null ? mCurRootView.getImeFocusController().getServedView() : null;
+ }
+
+ private View getNextServedViewLocked() {
+ return mCurRootView != null ? mCurRootView.getImeFocusController().getNextServedView()
+ : null;
+ }
+
+ private void setServedViewLocked(View view) {
+ if (mCurRootView != null) {
+ mCurRootView.getImeFocusController().setServedView(view);
+ }
+ }
+
+ private void setNextServedViewLocked(View view) {
+ if (mCurRootView != null) {
+ mCurRootView.getImeFocusController().setNextServedView(view);
+ }
+ }
+
+ private ImeFocusController getFocusController() {
+ synchronized (mH) {
+ if (mCurRootView != null) {
+ return mCurRootView.getImeFocusController();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Returns {@code true} when the given view has been served by Input Method.
+ */
+ private boolean hasServedByInputMethodLocked(View view) {
+ final View servedView = getServedViewLocked();
+ return (servedView == view
+ || (servedView != null && servedView.checkInputConnectionProxy(view)));
+ }
+
+ class H extends Handler {
+ H(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DUMP: {
+ SomeArgs args = (SomeArgs)msg.obj;
+ try {
+ doDump((FileDescriptor)args.arg1,
+ (PrintWriter)args.arg2, (String[])args.arg3);
+ } catch (RuntimeException e) {
+ ((PrintWriter)args.arg2).println("Exception: " + e);
+ }
+ synchronized (args.arg4) {
+ ((CountDownLatch)args.arg4).countDown();
+ }
+ args.recycle();
+ return;
+ }
+ case MSG_BIND: {
+ final InputBindResult res = (InputBindResult)msg.obj;
+ if (DEBUG) {
+ Log.i(TAG, "handleMessage: MSG_BIND " + res.sequence + "," + res.id);
+ }
+ synchronized (mH) {
+ if (mBindSequence < 0 || mBindSequence != res.sequence) {
+ Log.w(TAG, "Ignoring onBind: cur seq=" + mBindSequence
+ + ", given seq=" + res.sequence);
+ if (res.channel != null && res.channel != mCurChannel) {
+ res.channel.dispose();
+ }
+ return;
+ }
+
+ mRequestUpdateCursorAnchorInfoMonitorMode =
+ REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE;
+
+ setInputChannelLocked(res.channel);
+ mCurMethod = res.method; // for @UnsupportedAppUsage
+ mCurrentInputMethodSession =
+ InputMethodSessionWrapper.createOrNull(res.method);
+ mCurId = res.id;
+ mBindSequence = res.sequence;
+ mIsInputMethodSuppressingSpellChecker =
+ res.isInputMethodSuppressingSpellChecker;
+ }
+ startInputInner(StartInputReason.BOUND_TO_IMMS, null, 0, 0, 0);
+ return;
+ }
+ case MSG_UNBIND: {
+ final int sequence = msg.arg1;
+ @UnbindReason
+ final int reason = msg.arg2;
+ if (DEBUG) {
+ Log.i(TAG, "handleMessage: MSG_UNBIND " + sequence +
+ " reason=" + InputMethodDebug.unbindReasonToString(reason));
+ }
+ final boolean startInput;
+ synchronized (mH) {
+ if (mBindSequence != sequence) {
+ return;
+ }
+ clearBindingLocked();
+ // If we were actively using the last input method, then
+ // we would like to re-connect to the next input method.
+ final View servedView = getServedViewLocked();
+ if (servedView != null && servedView.isFocused()) {
+ mServedConnecting = true;
+ }
+ startInput = mActive;
+ }
+ if (startInput) {
+ startInputInner(
+ StartInputReason.UNBOUND_FROM_IMMS, null, 0, 0, 0);
+ }
+ return;
+ }
+ case MSG_SET_ACTIVE: {
+ final boolean active = msg.arg1 != 0;
+ final boolean fullscreen = msg.arg2 != 0;
+ final boolean reportToImeController = msg.obj != null && (boolean) msg.obj;
+ if (DEBUG) {
+ Log.i(TAG, "handleMessage: MSG_SET_ACTIVE " + active + ", was " + mActive);
+ }
+ synchronized (mH) {
+ mActive = active;
+ mFullscreenMode = fullscreen;
+
+ // Report active state to ImeFocusController to handle IME input
+ // connection lifecycle callback when it allowed.
+ final ImeFocusController controller = getFocusController();
+ final View rootView = mCurRootView != null ? mCurRootView.getView() : null;
+ if (controller != null && rootView != null && reportToImeController) {
+ rootView.post(() -> controller.onInteractiveChanged(active));
+ return;
+ }
+
+ if (!active) {
+ // Some other client has starting using the IME, so note
+ // that this happened and make sure our own editor's
+ // state is reset.
+ mRestartOnNextWindowFocus = true;
+ try {
+ // Note that finishComposingText() is allowed to run
+ // even when we are not active.
+ mIInputContext.finishComposingText();
+ } catch (RemoteException e) {
+ }
+ }
+ // Check focus again in case that "onWindowFocus" is called before
+ // handling this message.
+ final View servedView = getServedViewLocked();
+ if (servedView != null && canStartInput(servedView)) {
+ if (mCurRootView != null && mCurRootView.getImeFocusController()
+ .checkFocus(mRestartOnNextWindowFocus, false)) {
+ final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
+ : StartInputReason.DEACTIVATED_BY_IMMS;
+ mDelegate.startInput(reason, null, 0, 0, 0);
+ }
+ }
+ }
+ return;
+ }
+ case MSG_SEND_INPUT_EVENT: {
+ sendInputEventAndReportResultOnMainLooper((PendingEvent)msg.obj);
+ return;
+ }
+ case MSG_TIMEOUT_INPUT_EVENT: {
+ finishedInputEvent(msg.arg1, false, true);
+ return;
+ }
+ case MSG_FLUSH_INPUT_EVENT: {
+ finishedInputEvent(msg.arg1, false, false);
+ return;
+ }
+ case MSG_REPORT_FULLSCREEN_MODE: {
+ final boolean fullscreen = msg.arg1 != 0;
+ InputConnection ic = null;
+ synchronized (mH) {
+ mFullscreenMode = fullscreen;
+ if (mServedInputConnectionWrapper != null) {
+ ic = mServedInputConnectionWrapper.getInputConnection();
+ }
+ }
+ if (ic != null) {
+ ic.reportFullscreenMode(fullscreen);
+ }
+ return;
+ }
+ }
+ }
+ }
+
+ final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() {
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+ // No need to check for dump permission, since we only give this
+ // interface to the system.
+ CountDownLatch latch = new CountDownLatch(1);
+ SomeArgs sargs = SomeArgs.obtain();
+ sargs.arg1 = fd;
+ sargs.arg2 = fout;
+ sargs.arg3 = args;
+ sargs.arg4 = latch;
+ mH.sendMessage(mH.obtainMessage(MSG_DUMP, sargs));
+ try {
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fout.println("Timeout waiting for dump");
+ }
+ } catch (InterruptedException e) {
+ fout.println("Interrupted waiting for dump");
+ }
+ }
+
+ @Override
+ public void onBindMethod(InputBindResult res) {
+ mH.obtainMessage(MSG_BIND, res).sendToTarget();
+ }
+
+ @Override
+ public void onUnbindMethod(int sequence, @UnbindReason int unbindReason) {
+ mH.obtainMessage(MSG_UNBIND, sequence, unbindReason).sendToTarget();
+ }
+
+ @Override
+ public void setActive(boolean active, boolean fullscreen, boolean reportToImeController) {
+ mH.obtainMessage(MSG_SET_ACTIVE, active ? 1 : 0, fullscreen ? 1 : 0,
+ reportToImeController).sendToTarget();
+ }
+
+ @Override
+ public void scheduleStartInputIfNecessary(boolean fullscreen) {
+ // TODO(b/149859205): See if we can optimize this by having a fused dedicated operation.
+ mH.obtainMessage(MSG_SET_ACTIVE, 0 /* active */, fullscreen ? 1 : 0).sendToTarget();
+ mH.obtainMessage(MSG_SET_ACTIVE, 1 /* active */, fullscreen ? 1 : 0).sendToTarget();
+ }
+
+ @Override
+ public void reportFullscreenMode(boolean fullscreen) {
+ mH.obtainMessage(MSG_REPORT_FULLSCREEN_MODE, fullscreen ? 1 : 0, 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void setImeTraceEnabled(boolean enabled) {
+ ImeTracing.getInstance().setEnabled(enabled);
+ }
+
+ @Override
+ public void throwExceptionFromSystem(String message) {
+ throw new RuntimeException(message);
+ }
+ };
+
+ final InputConnection mDummyInputConnection = new BaseInputConnection(this, false);
+
+ /**
+ * For layoutlib to clean up static objects inside {@link InputMethodManager}.
+ */
+ static void tearDownEditMode() {
+ if (!isInEditMode()) {
+ throw new UnsupportedOperationException(
+ "This method must be called only from layoutlib");
+ }
+ synchronized (sLock) {
+ sInstance = null;
+ }
+ }
+
+ /**
+ * For layoutlib to override this method to return {@code true}.
+ *
+ * @return {@code true} if the process is running for developer tools
+ * @see View#isInEditMode()
+ */
+ private static boolean isInEditMode() {
+ return false;
+ }
+
+ @NonNull
+ private static InputMethodManager createInstance(int displayId, Looper looper) {
+ return isInEditMode() ? createStubInstance(displayId, looper)
+ : createRealInstance(displayId, looper);
+ }
+
+ @NonNull
+ private static InputMethodManager createRealInstance(int displayId, Looper looper) {
+ final IInputMethodManager service;
+ try {
+ service = IInputMethodManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
+ } catch (ServiceNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
+ // InputMethodManagerService#addClient() relies on Binder.getCalling{Pid, Uid}() to
+ // associate PID/UID with each IME client. This means:
+ // A. if this method call will be handled as an IPC, there is no problem.
+ // B. if this method call will be handled as an in-proc method call, we need to
+ // ensure that Binder.getCalling{Pid, Uid}() return Process.my{Pid, Uid}()
+ // Either ways we can always call Binder.{clear, restore}CallingIdentity() because
+ // 1) doing so has no effect for A and 2) doing so is sufficient for B.
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ service.addClient(imm.mClient, imm.mIInputContext, displayId);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return imm;
+ }
+
+ @NonNull
+ private static InputMethodManager createStubInstance(int displayId, Looper looper) {
+ // If InputMethodManager is running for layoutlib, stub out IPCs into IMMS.
+ final Class<IInputMethodManager> c = IInputMethodManager.class;
+ final IInputMethodManager stubInterface =
+ (IInputMethodManager) Proxy.newProxyInstance(c.getClassLoader(),
+ new Class[]{c}, (proxy, method, args) -> {
+ final Class<?> returnType = method.getReturnType();
+ if (returnType == boolean.class) {
+ return false;
+ } else if (returnType == int.class) {
+ return 0;
+ } else if (returnType == long.class) {
+ return 0L;
+ } else if (returnType == short.class) {
+ return 0;
+ } else if (returnType == char.class) {
+ return 0;
+ } else if (returnType == byte.class) {
+ return 0;
+ } else if (returnType == float.class) {
+ return 0f;
+ } else if (returnType == double.class) {
+ return 0.0;
+ } else {
+ return null;
+ }
+ });
+ return new InputMethodManager(stubInterface, displayId, looper);
+ }
+
+ private InputMethodManager(IInputMethodManager service, int displayId, Looper looper) {
+ mService = service;
+ mMainLooper = looper;
+ mH = new H(looper);
+ mDisplayId = displayId;
+ mIInputContext = new IInputConnectionWrapper(looper, mDummyInputConnection, this, null);
+ }
+
+ /**
+ * Retrieve an instance for the given {@link Context}, creating it if it doesn't already exist.
+ *
+ * @param context {@link Context} for which IME APIs need to work
+ * @return {@link InputMethodManager} instance
+ * @hide
+ */
+ @NonNull
+ public static InputMethodManager forContext(@DisplayContext Context context) {
+ final int displayId = context.getDisplayId();
+ // For better backward compatibility, we always use Looper.getMainLooper() for the default
+ // display case.
+ final Looper looper = displayId == Display.DEFAULT_DISPLAY
+ ? Looper.getMainLooper() : context.getMainLooper();
+ return forContextInternal(displayId, looper);
+ }
+
+ @NonNull
+ private static InputMethodManager forContextInternal(int displayId, Looper looper) {
+ final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY;
+ synchronized (sLock) {
+ InputMethodManager instance = sInstanceMap.get(displayId);
+ if (instance != null) {
+ return instance;
+ }
+ instance = createInstance(displayId, looper);
+ // For backward compatibility, store the instance also to sInstance for default display.
+ if (sInstance == null && isDefaultDisplay) {
+ sInstance = instance;
+ }
+ sInstanceMap.put(displayId, instance);
+ return instance;
+ }
+ }
+
+ /**
+ * Deprecated. Do not use.
+ *
+ * @return global {@link InputMethodManager} instance
+ * @deprecated Use {@link Context#getSystemService(Class)} instead. This method cannot fully
+ * support multi-display scenario.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public static InputMethodManager getInstance() {
+ Log.w(TAG, "InputMethodManager.getInstance() is deprecated because it cannot be"
+ + " compatible with multi-display."
+ + " Use context.getSystemService(InputMethodManager.class) instead.",
+ new Throwable());
+ ensureDefaultInstanceForDefaultDisplayIfNecessary();
+ return peekInstance();
+ }
+
+ /**
+ * Deprecated. Do not use.
+ *
+ * @return {@link #sInstance}
+ * @deprecated Use {@link Context#getSystemService(Class)} instead. This method cannot fully
+ * support multi-display scenario.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public static InputMethodManager peekInstance() {
+ Log.w(TAG, "InputMethodManager.peekInstance() is deprecated because it cannot be"
+ + " compatible with multi-display."
+ + " Use context.getSystemService(InputMethodManager.class) instead.",
+ new Throwable());
+ synchronized (sLock) {
+ return sInstance;
+ }
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public IInputMethodClient getClient() {
+ return mClient;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public IInputContext getInputContext() {
+ return mIInputContext;
+ }
+
+ /**
+ * Returns the list of installed input methods.
+ *
+ * <p>On multi user environment, this API returns a result for the calling process user.</p>
+ *
+ * @return {@link List} of {@link InputMethodInfo}.
+ */
+ public List<InputMethodInfo> getInputMethodList() {
+ try {
+ // We intentionally do not use UserHandle.getCallingUserId() here because for system
+ // services InputMethodManagerInternal.getInputMethodListAsUser() should be used
+ // instead.
+ return mService.getInputMethodList(UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the list of installed input methods for the specified user.
+ *
+ * @param userId user ID to query
+ * @return {@link List} of {@link InputMethodInfo}.
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
+ @NonNull
+ public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
+ try {
+ return mService.getInputMethodList(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the list of enabled input methods.
+ *
+ * <p>On multi user environment, this API returns a result for the calling process user.</p>
+ *
+ * @return {@link List} of {@link InputMethodInfo}.
+ */
+ public List<InputMethodInfo> getEnabledInputMethodList() {
+ try {
+ // We intentionally do not use UserHandle.getCallingUserId() here because for system
+ // services InputMethodManagerInternal.getEnabledInputMethodListAsUser() should be used
+ // instead.
+ return mService.getEnabledInputMethodList(UserHandle.myUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the list of enabled input methods for the specified user.
+ *
+ * @param userId user ID to query
+ * @return {@link List} of {@link InputMethodInfo}.
+ * @hide
+ */
+ @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
+ public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) {
+ try {
+ return mService.getEnabledInputMethodList(userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns a list of enabled input method subtypes for the specified input method info.
+ *
+ * <p>On multi user environment, this API returns a result for the calling process user.</p>
+ *
+ * @param imi An input method info whose subtypes list will be returned.
+ * @param allowsImplicitlySelectedSubtypes A boolean flag to allow to return the implicitly
+ * selected subtypes. If an input method info doesn't have enabled subtypes, the framework
+ * will implicitly enable subtypes according to the current system language.
+ */
+ public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi,
+ boolean allowsImplicitlySelectedSubtypes) {
+ try {
+ return mService.getEnabledInputMethodSubtypeList(
+ imi == null ? null : imi.getId(),
+ allowsImplicitlySelectedSubtypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @deprecated Use {@link InputMethodService#showStatusIcon(int)} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in this
+ * class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public void showStatusIcon(IBinder imeToken, String packageName, @DrawableRes int iconId) {
+ InputMethodPrivilegedOperationsRegistry.get(
+ imeToken).updateStatusIconAsync(packageName, iconId);
+ }
+
+ /**
+ * @deprecated Use {@link InputMethodService#hideStatusIcon()} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in
+ * this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public void hideStatusIcon(IBinder imeToken) {
+ InputMethodPrivilegedOperationsRegistry.get(imeToken).updateStatusIconAsync(null, 0);
+ }
+
+ /**
+ * This hidden API is deprecated in {@link android.os.Build.VERSION_CODES#Q}. Does nothing.
+ *
+ * @param spans will be ignored.
+ *
+ * @deprecated Do not use.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) {
+ Log.w(TAG, "registerSuggestionSpansForNotification() is deprecated. Does nothing.");
+ }
+
+ /**
+ * This hidden API is deprecated in {@link android.os.Build.VERSION_CODES#Q}. Does nothing.
+ *
+ * @deprecated Do not use.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public void notifySuggestionPicked(SuggestionSpan span, String originalString, int index) {
+ Log.w(TAG, "notifySuggestionPicked() is deprecated. Does nothing.");
+ }
+
+ /**
+ * Allows you to discover whether the attached input method is running
+ * in fullscreen mode. Return true if it is fullscreen, entirely covering
+ * your UI, else returns false.
+ */
+ public boolean isFullscreenMode() {
+ synchronized (mH) {
+ return mFullscreenMode;
+ }
+ }
+
+ /**
+ * Return true if the given view is the currently active view for the
+ * input method.
+ */
+ public boolean isActive(View view) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ return fallbackImm.isActive(view);
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ return hasServedByInputMethodLocked(view) && mCurrentTextBoxAttribute != null;
+ }
+ }
+
+ /**
+ * Return true if any view is currently active in the input method.
+ */
+ public boolean isActive() {
+ checkFocus();
+ synchronized (mH) {
+ return getServedViewLocked() != null && mCurrentTextBoxAttribute != null;
+ }
+ }
+
+ /**
+ * Return {@code true} if the currently served view is accepting full text edits.
+ * If {@code false}, it has no input connection, so it can only handle raw key events.
+ */
+ public boolean isAcceptingText() {
+ checkFocus();
+ synchronized (mH) {
+ return mServedInputConnectionWrapper != null
+ && mServedInputConnectionWrapper.getInputConnection() != null;
+ }
+ }
+
+ /**
+ * Return {@code true} if the input method is suppressing system spell checker.
+ */
+ public boolean isInputMethodSuppressingSpellChecker() {
+ synchronized (mH) {
+ return mIsInputMethodSuppressingSpellChecker;
+ }
+ }
+
+ /**
+ * Reset all of the state associated with being bound to an input method.
+ */
+ void clearBindingLocked() {
+ if (DEBUG) Log.v(TAG, "Clearing binding!");
+ clearConnectionLocked();
+ setInputChannelLocked(null);
+ mBindSequence = -1;
+ mCurId = null;
+ mCurMethod = null; // for @UnsupportedAppUsage
+ mCurrentInputMethodSession = null;
+ }
+
+ void setInputChannelLocked(InputChannel channel) {
+ if (mCurChannel == channel) {
+ return;
+ }
+ if (mCurChannel != null && channel != null
+ && mCurChannel.getToken() == channel.getToken()) {
+ // channel is a dupe of 'mCurChannel', because they have the same token, and represent
+ // the same connection. Ignore the incoming channel and keep using 'mCurChannel' to
+ // avoid confusing the InputEventReceiver.
+ return;
+ }
+ if (mCurSender != null) {
+ flushPendingEventsLocked();
+ mCurSender.dispose();
+ mCurSender = null;
+ }
+ if (mCurChannel != null) {
+ mCurChannel.dispose();
+ }
+ mCurChannel = channel;
+ }
+
+ /**
+ * Reset all of the state associated with a served view being connected
+ * to an input method
+ */
+ void clearConnectionLocked() {
+ mCurrentTextBoxAttribute = null;
+ if (mServedInputConnectionWrapper != null) {
+ mServedInputConnectionWrapper.deactivate();
+ mServedInputConnectionWrapper = null;
+ }
+ }
+
+ /**
+ * Disconnect any existing input connection, clearing the served view.
+ */
+ @UnsupportedAppUsage
+ void finishInputLocked() {
+ mIsInputMethodSuppressingSpellChecker = false;
+ setNextServedViewLocked(null);
+ if (getServedViewLocked() != null) {
+ if (DEBUG) {
+ Log.v(TAG, "FINISH INPUT: mServedView="
+ + dumpViewInfo(getServedViewLocked()));
+ }
+ setServedViewLocked(null);
+ mCompletions = null;
+ mServedConnecting = false;
+ clearConnectionLocked();
+ }
+ }
+
+ public void displayCompletions(View view, CompletionInfo[] completions) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.displayCompletions(view, completions);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view)) {
+ return;
+ }
+
+ mCompletions = completions;
+ if (mCurrentInputMethodSession != null) {
+ mCurrentInputMethodSession.displayCompletions(mCompletions);
+ }
+ }
+ }
+
+ public void updateExtractedText(View view, int token, ExtractedText text) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.updateExtractedText(view, token, text);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view)) {
+ return;
+ }
+
+ if (mCurrentInputMethodSession != null) {
+ mCurrentInputMethodSession.updateExtractedText(token, text);
+ }
+ }
+ }
+
+ /**
+ * Flag for {@link #showSoftInput} to indicate that this is an implicit
+ * request to show the input window, not as the result of a direct request
+ * by the user. The window may not be shown in this case.
+ */
+ public static final int SHOW_IMPLICIT = 0x0001;
+
+ /**
+ * Flag for {@link #showSoftInput} to indicate that the user has forced
+ * the input method open (such as by long-pressing menu) so it should
+ * not be closed until they explicitly do so.
+ */
+ public static final int SHOW_FORCED = 0x0002;
+
+ /**
+ * Synonym for {@link #showSoftInput(View, int, ResultReceiver)} without
+ * a result receiver: explicitly request that the current input method's
+ * soft input area be shown to the user, if needed.
+ *
+ * @param view The currently focused view, which would like to receive
+ * soft keyboard input.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #SHOW_IMPLICIT} bit set.
+ */
+ public boolean showSoftInput(View view, int flags) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ return fallbackImm.showSoftInput(view, flags);
+ }
+
+ return showSoftInput(view, flags, null);
+ }
+
+ /**
+ * Flag for the {@link ResultReceiver} result code from
+ * {@link #showSoftInput(View, int, ResultReceiver)} and
+ * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the
+ * state of the soft input window was unchanged and remains shown.
+ */
+ public static final int RESULT_UNCHANGED_SHOWN = 0;
+
+ /**
+ * Flag for the {@link ResultReceiver} result code from
+ * {@link #showSoftInput(View, int, ResultReceiver)} and
+ * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the
+ * state of the soft input window was unchanged and remains hidden.
+ */
+ public static final int RESULT_UNCHANGED_HIDDEN = 1;
+
+ /**
+ * Flag for the {@link ResultReceiver} result code from
+ * {@link #showSoftInput(View, int, ResultReceiver)} and
+ * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the
+ * state of the soft input window changed from hidden to shown.
+ */
+ public static final int RESULT_SHOWN = 2;
+
+ /**
+ * Flag for the {@link ResultReceiver} result code from
+ * {@link #showSoftInput(View, int, ResultReceiver)} and
+ * {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}: the
+ * state of the soft input window changed from shown to hidden.
+ */
+ public static final int RESULT_HIDDEN = 3;
+
+ /**
+ * Explicitly request that the current input method's soft input area be
+ * shown to the user, if needed. Call this if the user interacts with
+ * your view in such a way that they have expressed they would like to
+ * start performing input into it.
+ *
+ * <p><strong>Caveat:</strong> {@link ResultReceiver} instance passed to
+ * this method can be a long-lived object, because it may not be
+ * garbage-collected until all the corresponding {@link ResultReceiver}
+ * objects transferred to different processes get garbage-collected.
+ * Follow the general patterns to avoid memory leaks in Android.
+ * Consider to use {@link java.lang.ref.WeakReference} so that application
+ * logic objects such as {@link android.app.Activity} and {@link Context}
+ * can be garbage collected regardless of the lifetime of
+ * {@link ResultReceiver}.
+ *
+ * @param view The currently focused view, which would like to receive
+ * soft keyboard input.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #SHOW_IMPLICIT} bit set.
+ * @param resultReceiver If non-null, this will be called by the IME when
+ * it has processed your request to tell you what it has done. The result
+ * code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
+ * {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
+ * {@link #RESULT_HIDDEN}.
+ */
+ public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
+ return showSoftInput(view, flags, resultReceiver, SoftInputShowHideReason.SHOW_SOFT_INPUT);
+ }
+
+ private boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
+ ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this,
+ null /* icProto */);
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ return fallbackImm.showSoftInput(view, flags, resultReceiver);
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view)) {
+ Log.w(TAG, "Ignoring showSoftInput() as view=" + view + " is not served.");
+ return false;
+ }
+
+ try {
+ Log.d(TAG, "showSoftInput() view=" + view + " flags=" + flags + " reason="
+ + InputMethodDebug.softInputDisplayReasonToString(reason));
+ return mService.showSoftInput(
+ mClient,
+ view.getWindowToken(),
+ flags,
+ resultReceiver,
+ reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * This method is still kept for a while until android.support.v7.widget.SearchView ver. 26.0
+ * is publicly released because previous implementations of that class had relied on this method
+ * via reflection.
+ *
+ * @deprecated This is a hidden API. You should never use this.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
+ public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
+ synchronized (mH) {
+ try {
+ Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be"
+ + " removed soon. If you are using android.support.v7.widget.SearchView,"
+ + " please update to version 26.0 or newer version.");
+ if (mCurRootView == null || mCurRootView.getView() == null) {
+ Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()");
+ return;
+ }
+ mService.showSoftInput(
+ mClient,
+ mCurRootView.getView().getWindowToken(),
+ flags,
+ resultReceiver,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)}
+ * to indicate that the soft input window should only be hidden if it was not explicitly shown
+ * by the user.
+ */
+ public static final int HIDE_IMPLICIT_ONLY = 0x0001;
+
+ /**
+ * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestShowSelf(int)}
+ * to indicate that the soft input window should normally be hidden, unless it was originally
+ * shown with {@link #SHOW_FORCED}.
+ */
+ public static final int HIDE_NOT_ALWAYS = 0x0002;
+
+ /**
+ * Synonym for {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}
+ * without a result: request to hide the soft input window from the
+ * context of the window that is currently accepting input.
+ *
+ * @param windowToken The token of the window that is making the request,
+ * as returned by {@link View#getWindowToken() View.getWindowToken()}.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
+ */
+ public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
+ return hideSoftInputFromWindow(windowToken, flags, null);
+ }
+
+ /**
+ * Request to hide the soft input window from the context of the window
+ * that is currently accepting input. This should be called as a result
+ * of the user doing some actually than fairly explicitly requests to
+ * have the input window hidden.
+ *
+ * <p><strong>Caveat:</strong> {@link ResultReceiver} instance passed to
+ * this method can be a long-lived object, because it may not be
+ * garbage-collected until all the corresponding {@link ResultReceiver}
+ * objects transferred to different processes get garbage-collected.
+ * Follow the general patterns to avoid memory leaks in Android.
+ * Consider to use {@link java.lang.ref.WeakReference} so that application
+ * logic objects such as {@link android.app.Activity} and {@link Context}
+ * can be garbage collected regardless of the lifetime of
+ * {@link ResultReceiver}.
+ *
+ * @param windowToken The token of the window that is making the request,
+ * as returned by {@link View#getWindowToken() View.getWindowToken()}.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
+ * @param resultReceiver If non-null, this will be called by the IME when
+ * it has processed your request to tell you what it has done. The result
+ * code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
+ * {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
+ * {@link #RESULT_HIDDEN}.
+ */
+ public boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+ ResultReceiver resultReceiver) {
+ return hideSoftInputFromWindow(windowToken, flags, resultReceiver,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT);
+ }
+
+ private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
+ this, null /* icProto */);
+ checkFocus();
+ synchronized (mH) {
+ final View servedView = getServedViewLocked();
+ if (servedView == null || servedView.getWindowToken() != windowToken) {
+ return false;
+ }
+
+ try {
+ return mService.hideSoftInput(mClient, windowToken, flags, resultReceiver, reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * This method toggles the input method window display.
+ * If the input window is already displayed, it gets hidden.
+ * If not the input window will be displayed.
+ * @param windowToken The token of the window that is making the request,
+ * as returned by {@link View#getWindowToken() View.getWindowToken()}.
+ * @param showFlags Provides additional operating flags. May be
+ * 0 or have the {@link #SHOW_IMPLICIT},
+ * {@link #SHOW_FORCED} bit set.
+ * @param hideFlags Provides additional operating flags. May be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
+ * {@link #HIDE_NOT_ALWAYS} bit set.
+ *
+ * @deprecated Use {@link #showSoftInput(View, int)} or
+ * {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
+ * In particular during focus changes, the current visibility of the IME is not
+ * well defined. Starting in {@link Build.VERSION_CODES#S Android S}, this only
+ * has an effect if the calling app is the current IME focus.
+ */
+ @Deprecated
+ public void toggleSoftInputFromWindow(IBinder windowToken, int showFlags, int hideFlags) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InputMethodManager#toggleSoftInputFromWindow", InputMethodManager.this,
+ null /* icProto */);
+ synchronized (mH) {
+ final View servedView = getServedViewLocked();
+ if (servedView == null || servedView.getWindowToken() != windowToken) {
+ return;
+ }
+ toggleSoftInput(showFlags, hideFlags);
+ }
+ }
+
+ /**
+ * This method toggles the input method window display.
+ *
+ * If the input window is already displayed, it gets hidden.
+ * If not the input window will be displayed.
+ * @param showFlags Provides additional operating flags. May be
+ * 0 or have the {@link #SHOW_IMPLICIT},
+ * {@link #SHOW_FORCED} bit set.
+ * @param hideFlags Provides additional operating flags. May be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
+ * {@link #HIDE_NOT_ALWAYS} bit set.
+ *
+ * @deprecated Use {@link #showSoftInput(View, int)} or
+ * {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
+ * In particular during focus changes, the current visibility of the IME is not
+ * well defined. Starting in {@link Build.VERSION_CODES#S Android S}, this only
+ * has an effect if the calling app is the current IME focus.
+ */
+ @Deprecated
+ public void toggleSoftInput(int showFlags, int hideFlags) {
+ ImeTracing.getInstance().triggerClientDump(
+ "InputMethodManager#toggleSoftInput", InputMethodManager.this,
+ null /* icProto */);
+ synchronized (mH) {
+ final View view = getServedViewLocked();
+ if (mImeInsetsConsumer != null && view != null) {
+ if (mImeInsetsConsumer.isRequestedVisible()) {
+ hideSoftInputFromWindow(view.getWindowToken(), hideFlags, null,
+ SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT);
+ } else {
+ showSoftInput(view, showFlags, null,
+ SoftInputShowHideReason.SHOW_TOGGLE_SOFT_INPUT);
+ }
+ }
+ }
+ }
+
+ /**
+ * If the input method is currently connected to the given view,
+ * restart it with its new contents. You should call this when the text
+ * within your view changes outside of the normal input method or key
+ * input flow, such as when an application calls TextView.setText().
+ *
+ * @param view The view whose text has changed.
+ */
+ public void restartInput(View view) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.restartInput(view);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view)) {
+ return;
+ }
+
+ mServedConnecting = true;
+ }
+
+ startInputInner(StartInputReason.APP_CALLED_RESTART_INPUT_API, null, 0, 0, 0);
+ }
+
+ /**
+ * Called when {@link DelegateImpl#startInput}, {@link #restartInput(View)},
+ * {@link #MSG_BIND} or {@link #MSG_UNBIND}.
+ * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
+ * background thread may blocked by other methods which already inside {@code mH} lock.
+ */
+ boolean startInputInner(@StartInputReason int startInputReason,
+ @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
+ @SoftInputModeFlags int softInputMode, int windowFlags) {
+ final View view;
+ synchronized (mH) {
+ view = getServedViewLocked();
+
+ // Make sure we have a window token for the served view.
+ if (DEBUG) {
+ Log.v(TAG, "Starting input: view=" + dumpViewInfo(view) +
+ " reason=" + InputMethodDebug.startInputReasonToString(startInputReason));
+ }
+ if (view == null) {
+ if (DEBUG) Log.v(TAG, "ABORT input: no served view!");
+ return false;
+ }
+ }
+
+ if (windowGainingFocus == null) {
+ windowGainingFocus = view.getWindowToken();
+ if (windowGainingFocus == null) {
+ Log.e(TAG, "ABORT input: ServedView must be attached to a Window");
+ return false;
+ }
+ startInputFlags = getStartInputFlags(view, startInputFlags);
+ softInputMode = view.getViewRootImpl().mWindowAttributes.softInputMode;
+ windowFlags = view.getViewRootImpl().mWindowAttributes.flags;
+ }
+
+ // Now we need to get an input connection from the served view.
+ // This is complicated in a couple ways: we can't be holding our lock
+ // when calling out to the view, and we need to make sure we call into
+ // the view on the same thread that is driving its view hierarchy.
+ Handler vh = view.getHandler();
+ if (vh == null) {
+ // If the view doesn't have a handler, something has changed out
+ // from under us, so just close the current input.
+ // If we don't close the current input, the current input method can remain on the
+ // screen without a connection.
+ if (DEBUG) Log.v(TAG, "ABORT input: no handler for view! Close current input.");
+ closeCurrentInput();
+ return false;
+ }
+ if (vh.getLooper() != Looper.myLooper()) {
+ // The view is running on a different thread than our own, so
+ // we need to reschedule our work for over there.
+ if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");
+ vh.post(() -> mDelegate.startInput(startInputReason, null, 0, 0, 0));
+ return false;
+ }
+
+ // Okay we are now ready to call into the served view and have it
+ // do its stuff.
+ // Life is good: let's hook everything up!
+ EditorInfo tba = new EditorInfo();
+ // Note: Use Context#getOpPackageName() rather than Context#getPackageName() so that the
+ // system can verify the consistency between the uid of this process and package name passed
+ // from here. See comment of Context#getOpPackageName() for details.
+ tba.packageName = view.getContext().getOpPackageName();
+ tba.autofillId = view.getAutofillId();
+ tba.fieldId = view.getId();
+ InputConnection ic = view.onCreateInputConnection(tba);
+ if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
+
+ final Handler icHandler;
+ InputBindResult res = null;
+ synchronized (mH) {
+ // Now that we are locked again, validate that our state hasn't
+ // changed.
+ final View servedView = getServedViewLocked();
+ if (servedView != view || !mServedConnecting) {
+ // Something else happened, so abort.
+ if (DEBUG) Log.v(TAG,
+ "Starting input: finished by someone else. view=" + dumpViewInfo(view)
+ + " servedView=" + dumpViewInfo(servedView)
+ + " mServedConnecting=" + mServedConnecting);
+ return false;
+ }
+
+ // If we already have a text box, then this view is already
+ // connected so we want to restart it.
+ if (mCurrentTextBoxAttribute == null) {
+ startInputFlags |= StartInputFlags.INITIAL_CONNECTION;
+ }
+
+ // Hook 'em up and let 'er rip.
+ mCurrentTextBoxAttribute = tba;
+
+ mServedConnecting = false;
+ if (mServedInputConnectionWrapper != null) {
+ mServedInputConnectionWrapper.deactivate();
+ mServedInputConnectionWrapper = null;
+ }
+ IInputConnectionWrapper servedContext;
+ final int missingMethodFlags;
+ if (ic != null) {
+ mCursorSelStart = tba.initialSelStart;
+ mCursorSelEnd = tba.initialSelEnd;
+ mCursorCandStart = -1;
+ mCursorCandEnd = -1;
+ mCursorRect.setEmpty();
+ mCursorAnchorInfo = null;
+ missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic);
+ if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER)
+ != 0) {
+ // InputConnection#getHandler() is not implemented.
+ icHandler = null;
+ } else {
+ icHandler = ic.getHandler();
+ }
+ servedContext = new IInputConnectionWrapper(
+ icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
+ } else {
+ servedContext = null;
+ missingMethodFlags = 0;
+ icHandler = null;
+ }
+ mServedInputConnectionWrapper = servedContext;
+
+ if (DEBUG) {
+ Log.v(TAG, "START INPUT: view=" + dumpViewInfo(view) + " ic="
+ + ic + " tba=" + tba + " startInputFlags="
+ + InputMethodDebug.startInputFlagsToString(startInputFlags));
+ }
+ try {
+ res = mService.startInputOrWindowGainedFocus(
+ startInputReason, mClient, windowGainingFocus, startInputFlags,
+ softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
+ view.getContext().getApplicationInfo().targetSdkVersion);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
+ if (res == null) {
+ Log.wtf(TAG, "startInputOrWindowGainedFocus must not return"
+ + " null. startInputReason="
+ + InputMethodDebug.startInputReasonToString(startInputReason)
+ + " editorInfo=" + tba
+ + " startInputFlags="
+ + InputMethodDebug.startInputFlagsToString(startInputFlags));
+ return false;
+ }
+ mIsInputMethodSuppressingSpellChecker = res.isInputMethodSuppressingSpellChecker;
+ if (res.id != null) {
+ setInputChannelLocked(res.channel);
+ mBindSequence = res.sequence;
+ mCurMethod = res.method; // for @UnsupportedAppUsage
+ mCurrentInputMethodSession = InputMethodSessionWrapper.createOrNull(res.method);
+ mCurId = res.id;
+ } else if (res.channel != null && res.channel != mCurChannel) {
+ res.channel.dispose();
+ }
+ switch (res.result) {
+ case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
+ mRestartOnNextWindowFocus = true;
+ break;
+ }
+ if (mCurrentInputMethodSession != null && mCompletions != null) {
+ mCurrentInputMethodSession.displayCompletions(mCompletions);
+ }
+ }
+
+ // Notify the app that the InputConnection is initialized and ready for use.
+ if (ic != null && res != null && res.method != null) {
+ if (DEBUG) {
+ Log.v(TAG, "Calling View.onInputConnectionOpened: view= " + view
+ + ", ic=" + ic + ", tba=" + tba + ", handler=" + icHandler);
+ }
+ view.onInputConnectionOpenedInternal(ic, tba, icHandler);
+ }
+
+ return true;
+ }
+
+ /**
+ * An empty method only to avoid crashes of apps that call this method via reflection and do not
+ * handle {@link NoSuchMethodException} in a graceful manner.
+ *
+ * @deprecated This is an empty method. No framework method must call this method.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage(trackingBug = 37122102, maxTargetSdk = Build.VERSION_CODES.Q,
+ publicAlternatives = "{@code androidx.activity.ComponentActivity}")
+ public void windowDismissed(IBinder appWindowToken) {
+ // Intentionally empty.
+ //
+ // It seems that some applications call this method via reflection to null clear the
+ // following fields that used to exist in InputMethodManager:
+ // * InputMethodManager#mCurRootView
+ // * InputMethodManager#mServedView
+ // * InputMethodManager#mNextServedView
+ // so that these objects can be garbage-collected when an Activity gets dismissed.
+ //
+ // It is indeed true that older versions of InputMethodManager had issues that prevented
+ // these fields from being null-cleared when it should have been, but the understanding of
+ // the engineering team is that all known issues have already been fixed as of Android 10.
+ //
+ // For older devices, developers can work around the object leaks by using
+ // androidx.activity.ComponentActivity.
+ // See https://issuetracker.google.com/u/1/issues/37122102 for details.
+ //
+ // If you believe InputMethodManager is leaking objects in API 24 or any later version,
+ // please file a bug at https://issuetracker.google.com/issues/new?component=192705.
+ }
+
+ private int getStartInputFlags(View focusedView, int startInputFlags) {
+ startInputFlags |= StartInputFlags.VIEW_HAS_FOCUS;
+ if (focusedView.onCheckIsTextEditor()) {
+ startInputFlags |= StartInputFlags.IS_TEXT_EDITOR;
+ }
+ return startInputFlags;
+ }
+
+ /**
+ * Check the next served view from {@link ImeFocusController} if needs to start input.
+ * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
+ * background thread may blocked by other methods which already inside {@code mH} lock.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void checkFocus() {
+ final ImeFocusController controller = getFocusController();
+ if (controller != null) {
+ controller.checkFocus(false /* forceNewFocus */, true /* startInput */);
+ }
+ }
+
+ @UnsupportedAppUsage
+ void closeCurrentInput() {
+ synchronized (mH) {
+ if (mCurRootView == null || mCurRootView.getView() == null) {
+ Log.w(TAG, "No current root view, ignoring closeCurrentInput()");
+ return;
+ }
+ try {
+ mService.hideSoftInput(
+ mClient,
+ mCurRootView.getView().getWindowToken(),
+ HIDE_NOT_ALWAYS,
+ null,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Register for IME state callbacks and applying visibility in
+ * {@link android.view.ImeInsetsSourceConsumer}.
+ * @hide
+ */
+ public void registerImeConsumer(@NonNull ImeInsetsSourceConsumer imeInsetsConsumer) {
+ if (imeInsetsConsumer == null) {
+ throw new IllegalStateException("ImeInsetsSourceConsumer cannot be null.");
+ }
+
+ synchronized (mH) {
+ mImeInsetsConsumer = imeInsetsConsumer;
+ }
+ }
+
+ /**
+ * Unregister for IME state callbacks and applying visibility in
+ * {@link android.view.ImeInsetsSourceConsumer}.
+ * @hide
+ */
+ public void unregisterImeConsumer(@NonNull ImeInsetsSourceConsumer imeInsetsConsumer) {
+ if (imeInsetsConsumer == null) {
+ throw new IllegalStateException("ImeInsetsSourceConsumer cannot be null.");
+ }
+
+ synchronized (mH) {
+ if (mImeInsetsConsumer == imeInsetsConsumer) {
+ mImeInsetsConsumer = null;
+ }
+ }
+ }
+
+ /**
+ * Call showSoftInput with currently focused view.
+ *
+ * @param windowToken the window from which this request originates. If this doesn't match the
+ * currently served view, the request is ignored and returns {@code false}.
+ *
+ * @return {@code true} if IME can (eventually) be shown, {@code false} otherwise.
+ * @hide
+ */
+ public boolean requestImeShow(IBinder windowToken) {
+ synchronized (mH) {
+ final View servedView = getServedViewLocked();
+ if (servedView == null || servedView.getWindowToken() != windowToken) {
+ return false;
+ }
+ showSoftInput(servedView, 0 /* flags */, null /* resultReceiver */,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API);
+ return true;
+ }
+ }
+
+ /**
+ * Notify IME directly that it is no longer visible.
+ *
+ * @param windowToken the window from which this request originates. If this doesn't match the
+ * currently served view, the request is ignored.
+ * @hide
+ */
+ public void notifyImeHidden(IBinder windowToken) {
+ ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this,
+ null /* icProto */);
+ synchronized (mH) {
+ if (mCurrentInputMethodSession != null && mCurRootView != null
+ && mCurRootView.getWindowToken() == windowToken) {
+ mCurrentInputMethodSession.notifyImeHidden();
+ }
+ }
+ }
+
+ /**
+ * Notify IME directly to remove surface as it is no longer visible.
+ * @param windowToken The client window token that requests the IME to remove its surface.
+ * @hide
+ */
+ public void removeImeSurface(IBinder windowToken) {
+ synchronized (mH) {
+ try {
+ mService.removeImeSurfaceFromWindowAsync(windowToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Report the current selection range.
+ *
+ * <p><strong>Editor authors</strong>, you need to call this method whenever
+ * the cursor moves in your editor. Remember that in addition to doing this, your
+ * editor needs to always supply current cursor values in
+ * {@link EditorInfo#initialSelStart} and {@link EditorInfo#initialSelEnd} every
+ * time {@link android.view.View#onCreateInputConnection(EditorInfo)} is
+ * called, which happens whenever the keyboard shows up or the focus changes
+ * to a text field, among other cases.</p>
+ */
+ public void updateSelection(View view, int selStart, int selEnd,
+ int candidatesStart, int candidatesEnd) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.updateSelection(view, selStart, selEnd, candidatesStart, candidatesEnd);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null
+ || mCurrentInputMethodSession == null) {
+ return;
+ }
+
+ if (mCursorSelStart != selStart || mCursorSelEnd != selEnd
+ || mCursorCandStart != candidatesStart
+ || mCursorCandEnd != candidatesEnd) {
+ if (DEBUG) Log.d(TAG, "updateSelection");
+
+ if (DEBUG) {
+ Log.v(TAG, "SELECTION CHANGE: " + mCurrentInputMethodSession);
+ }
+ final int oldSelStart = mCursorSelStart;
+ final int oldSelEnd = mCursorSelEnd;
+ // Update internal values before sending updateSelection to the IME, because
+ // if it changes the text within its onUpdateSelection handler in a way that
+ // does not move the cursor we don't want to call it again with the same values.
+ mCursorSelStart = selStart;
+ mCursorSelEnd = selEnd;
+ mCursorCandStart = candidatesStart;
+ mCursorCandEnd = candidatesEnd;
+ mCurrentInputMethodSession.updateSelection(
+ oldSelStart, oldSelEnd, selStart, selEnd, candidatesStart, candidatesEnd);
+ }
+ }
+ }
+
+ /**
+ * Notify the event when the user tapped or clicked the text view.
+ *
+ * @param view {@link View} which is being clicked.
+ * @see InputMethodService#onViewClicked(boolean)
+ * @deprecated The semantics of this method can never be defined well for composite {@link View}
+ * that works as a giant "Canvas", which can host its own UI hierarchy and sub focus
+ * state. {@link android.webkit.WebView} is a good example. Application / IME
+ * developers should not rely on this method.
+ */
+ @Deprecated
+ public void viewClicked(View view) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.viewClicked(view);
+ return;
+ }
+
+ final View servedView;
+ final View nextServedView;
+ synchronized (mH) {
+ servedView = getServedViewLocked();
+ nextServedView = getNextServedViewLocked();
+ }
+ final boolean focusChanged = servedView != nextServedView;
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null
+ || mCurrentInputMethodSession == null) {
+ return;
+ }
+ if (DEBUG) Log.v(TAG, "onViewClicked: " + focusChanged);
+ mCurrentInputMethodSession.viewClicked(focusChanged);
+ }
+ }
+
+ /**
+ * Return true if the current input method wants to watch the location
+ * of the input editor's cursor in its window.
+ *
+ * @deprecated Use {@link InputConnection#requestCursorUpdates(int)} instead.
+ */
+ @Deprecated
+ public boolean isWatchingCursor(View view) {
+ return false;
+ }
+
+ /**
+ * Return true if the current input method wants to be notified when cursor/anchor location
+ * is changed.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean isCursorAnchorInfoEnabled() {
+ synchronized (mH) {
+ final boolean isImmediate = (mRequestUpdateCursorAnchorInfoMonitorMode &
+ InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0;
+ final boolean isMonitoring = (mRequestUpdateCursorAnchorInfoMonitorMode &
+ InputConnection.CURSOR_UPDATE_MONITOR) != 0;
+ return isImmediate || isMonitoring;
+ }
+ }
+
+ /**
+ * Set the requested mode for {@link #updateCursorAnchorInfo(View, CursorAnchorInfo)}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void setUpdateCursorAnchorInfoMode(int flags) {
+ synchronized (mH) {
+ mRequestUpdateCursorAnchorInfoMonitorMode = flags;
+ }
+ }
+
+ /**
+ * Report the current cursor location in its window.
+ *
+ * @deprecated Use {@link #updateCursorAnchorInfo(View, CursorAnchorInfo)} instead.
+ */
+ @Deprecated
+ public void updateCursor(View view, int left, int top, int right, int bottom) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.updateCursor(view, left, top, right, bottom);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null
+ || mCurrentInputMethodSession == null) {
+ return;
+ }
+
+ mTmpCursorRect.set(left, top, right, bottom);
+ if (!mCursorRect.equals(mTmpCursorRect)) {
+ if (DEBUG) Log.d(TAG, "updateCursor: " + mCurrentInputMethodSession);
+
+ mCurrentInputMethodSession.updateCursor(mTmpCursorRect);
+ mCursorRect.set(mTmpCursorRect);
+ }
+ }
+ }
+
+ /**
+ * Report positional change of the text insertion point and/or characters in the composition
+ * string.
+ */
+ public void updateCursorAnchorInfo(View view, final CursorAnchorInfo cursorAnchorInfo) {
+ if (view == null || cursorAnchorInfo == null) {
+ return;
+ }
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.updateCursorAnchorInfo(view, cursorAnchorInfo);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null
+ || mCurrentInputMethodSession == null) {
+ return;
+ }
+ // If immediate bit is set, we will call updateCursorAnchorInfo() even when the data has
+ // not been changed from the previous call.
+ final boolean isImmediate = (mRequestUpdateCursorAnchorInfoMonitorMode &
+ InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0;
+ if (!isImmediate && Objects.equals(mCursorAnchorInfo, cursorAnchorInfo)) {
+ // TODO: Consider always emitting this message once we have addressed redundant
+ // calls of this method from android.widget.Editor.
+ if (DEBUG) {
+ Log.w(TAG, "Ignoring redundant updateCursorAnchorInfo: info="
+ + cursorAnchorInfo);
+ }
+ return;
+ }
+ if (DEBUG) Log.v(TAG, "updateCursorAnchorInfo: " + cursorAnchorInfo);
+ mCurrentInputMethodSession.updateCursorAnchorInfo(cursorAnchorInfo);
+ mCursorAnchorInfo = cursorAnchorInfo;
+ // Clear immediate bit (if any).
+ mRequestUpdateCursorAnchorInfoMonitorMode &= ~InputConnection.CURSOR_UPDATE_IMMEDIATE;
+ }
+ }
+
+ /**
+ * Call {@link InputMethodSession#appPrivateCommand(String, Bundle)
+ * InputMethodSession.appPrivateCommand()} on the current Input Method.
+ * @param view Optional View that is sending the command, or null if
+ * you want to send the command regardless of the view that is attached
+ * to the input method.
+ * @param action Name of the command to be performed. This <em>must</em>
+ * be a scoped name, i.e. prefixed with a package name you own, so that
+ * different developers will not create conflicting commands.
+ * @param data Any data to include with the command.
+ */
+ public void sendAppPrivateCommand(View view, String action, Bundle data) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
+ if (fallbackImm != null) {
+ fallbackImm.sendAppPrivateCommand(view, action, data);
+ return;
+ }
+
+ checkFocus();
+ synchronized (mH) {
+ if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null
+ || mCurrentInputMethodSession == null) {
+ return;
+ }
+ if (DEBUG) Log.v(TAG, "APP PRIVATE COMMAND " + action + ": " + data);
+ mCurrentInputMethodSession.appPrivateCommand(action, data);
+ }
+ }
+
+ /**
+ * Force switch to a new input method component. This can only be called
+ * from an application or a service which has a token of the currently active input method.
+ *
+ * <p>On Android {@link Build.VERSION_CODES#Q} and later devices, the undocumented behavior that
+ * token can be {@code null} when the caller has
+ * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} is deprecated. Instead, update
+ * {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and
+ * {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.</p>
+ *
+ * @param token Supplies the identifying token given to an input method
+ * when it was started, which allows it to perform this operation on
+ * itself.
+ * @param id The unique identifier for the new input method to be switched to.
+ * @deprecated Use {@link InputMethodService#switchInputMethod(String)}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public void setInputMethod(IBinder token, String id) {
+ if (token == null) {
+ // There are still some system components that rely on this undocumented behavior
+ // regarding null IME token with WRITE_SECURE_SETTINGS. Provide a fallback logic as a
+ // temporary remedy.
+ if (id == null) {
+ return;
+ }
+ if (Process.myUid() == Process.SYSTEM_UID) {
+ Log.w(TAG, "System process should not be calling setInputMethod() because almost "
+ + "always it is a bug under multi-user / multi-profile environment. "
+ + "Consider interacting with InputMethodManagerService directly via "
+ + "LocalServices.");
+ return;
+ }
+ final Context fallbackContext = ActivityThread.currentApplication();
+ if (fallbackContext == null) {
+ return;
+ }
+ if (fallbackContext.checkSelfPermission(WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ final List<InputMethodInfo> imis = getEnabledInputMethodList();
+ final int numImis = imis.size();
+ boolean found = false;
+ for (int i = 0; i < numImis; ++i) {
+ final InputMethodInfo imi = imis.get(i);
+ if (id.equals(imi.getId())) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ Log.e(TAG, "Ignoring setInputMethod(null, " + id + ") because the specified "
+ + "id not found in enabled IMEs.");
+ return;
+ }
+ Log.w(TAG, "The undocumented behavior that setInputMethod() accepts null token "
+ + "when the caller has WRITE_SECURE_SETTINGS is deprecated. This behavior may "
+ + "be completely removed in a future version. Update secure settings directly "
+ + "instead.");
+ final ContentResolver resolver = fallbackContext.getContentResolver();
+ Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
+ NOT_A_SUBTYPE_ID);
+ Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, id);
+ return;
+ }
+ InputMethodPrivilegedOperationsRegistry.get(token).setInputMethod(id);
+ }
+
+ /**
+ * Force switch to a new input method and subtype. This can only be called
+ * from an application or a service which has a token of the currently active input method.
+ *
+ * <p>On Android {@link Build.VERSION_CODES#Q} and later devices, {@code token} cannot be
+ * {@code null} even with {@link android.Manifest.permission#WRITE_SECURE_SETTINGS}. Instead,
+ * update {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and
+ * {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.</p>
+ *
+ * @param token Supplies the identifying token given to an input method
+ * when it was started, which allows it to perform this operation on
+ * itself.
+ * @param id The unique identifier for the new input method to be switched to.
+ * @param subtype The new subtype of the new input method to be switched to.
+ * @deprecated Use
+ * {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public void setInputMethodAndSubtype(@NonNull IBinder token, String id,
+ InputMethodSubtype subtype) {
+ if (token == null) {
+ Log.e(TAG, "setInputMethodAndSubtype() does not accept null token on Android Q "
+ + "and later.");
+ return;
+ }
+ InputMethodPrivilegedOperationsRegistry.get(token).setInputMethodAndSubtype(id, subtype);
+ }
+
+ /**
+ * Close/hide the input method's soft input area, so the user no longer
+ * sees it or can interact with it. This can only be called
+ * from the currently active input method, as validated by the given token.
+ *
+ * @param token Supplies the identifying token given to an input method
+ * when it was started, which allows it to perform this operation on
+ * itself.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
+ * {@link #HIDE_NOT_ALWAYS} bit set.
+ * @deprecated Use {@link InputMethodService#requestHideSelf(int)} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in this
+ * class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public void hideSoftInputFromInputMethod(IBinder token, int flags) {
+ InputMethodPrivilegedOperationsRegistry.get(token).hideMySoftInput(flags);
+ }
+
+ /**
+ * Show the input method's soft input area, so the user
+ * sees the input method window and can interact with it.
+ * This can only be called from the currently active input method,
+ * as validated by the given token.
+ *
+ * @param token Supplies the identifying token given to an input method
+ * when it was started, which allows it to perform this operation on
+ * itself.
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link #SHOW_IMPLICIT} or
+ * {@link #SHOW_FORCED} bit set.
+ * @deprecated Use {@link InputMethodService#requestShowSelf(int)} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in this
+ * class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public void showSoftInputFromInputMethod(IBinder token, int flags) {
+ InputMethodPrivilegedOperationsRegistry.get(token).showMySoftInput(flags);
+ }
+
+ /**
+ * Dispatches an input event to the IME.
+ *
+ * Returns {@link #DISPATCH_HANDLED} if the event was handled.
+ * Returns {@link #DISPATCH_NOT_HANDLED} if the event was not handled.
+ * Returns {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the
+ * callback will be invoked later.
+ *
+ * @hide
+ */
+ public int dispatchInputEvent(InputEvent event, Object token,
+ FinishedInputEventCallback callback, Handler handler) {
+ synchronized (mH) {
+ if (mCurrentInputMethodSession != null) {
+ if (event instanceof KeyEvent) {
+ KeyEvent keyEvent = (KeyEvent)event;
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN
+ && keyEvent.getKeyCode() == KeyEvent.KEYCODE_SYM
+ && keyEvent.getRepeatCount() == 0) {
+ showInputMethodPickerLocked();
+ return DISPATCH_HANDLED;
+ }
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "DISPATCH INPUT EVENT: " + mCurrentInputMethodSession);
+ }
+
+ PendingEvent p = obtainPendingEventLocked(
+ event, token, mCurId, callback, handler);
+ if (mMainLooper.isCurrentThread()) {
+ // Already running on the IMM thread so we can send the event immediately.
+ return sendInputEventOnMainLooperLocked(p);
+ }
+
+ // Post the event to the IMM thread.
+ Message msg = mH.obtainMessage(MSG_SEND_INPUT_EVENT, p);
+ msg.setAsynchronous(true);
+ mH.sendMessage(msg);
+ return DISPATCH_IN_PROGRESS;
+ }
+ }
+ return DISPATCH_NOT_HANDLED;
+ }
+
+ /**
+ * Provides the default implementation of {@link InputConnection#sendKeyEvent(KeyEvent)}, which
+ * is expected to dispatch an keyboard event sent from the IME to an appropriate event target
+ * depending on the given {@link View} and the current focus state.
+ *
+ * <p>CAUTION: This method is provided only for the situation where
+ * {@link InputConnection#sendKeyEvent(KeyEvent)} needs to be implemented without relying on
+ * {@link BaseInputConnection}. Do not use this API for anything else.</p>
+ *
+ * @param targetView the default target view. If {@code null} is specified, then this method
+ * tries to find a good event target based on the current focus state.
+ * @param event the key event to be dispatched.
+ */
+ public void dispatchKeyEventFromInputMethod(@Nullable View targetView,
+ @NonNull KeyEvent event) {
+ // Re-dispatch if there is a context mismatch.
+ final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(targetView);
+ if (fallbackImm != null) {
+ fallbackImm.dispatchKeyEventFromInputMethod(targetView, event);
+ return;
+ }
+
+ synchronized (mH) {
+ ViewRootImpl viewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
+ if (viewRootImpl == null) {
+ final View servedView = getServedViewLocked();
+ if (servedView != null) {
+ viewRootImpl = servedView.getViewRootImpl();
+ }
+ }
+ if (viewRootImpl != null) {
+ viewRootImpl.dispatchKeyFromIme(event);
+ }
+ }
+ }
+
+ // Must be called on the main looper
+ void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
+ final boolean handled;
+ synchronized (mH) {
+ int result = sendInputEventOnMainLooperLocked(p);
+ if (result == DISPATCH_IN_PROGRESS) {
+ return;
+ }
+
+ handled = (result == DISPATCH_HANDLED);
+ }
+
+ invokeFinishedInputEventCallback(p, handled);
+ }
+
+ // Must be called on the main looper
+ int sendInputEventOnMainLooperLocked(PendingEvent p) {
+ if (mCurChannel != null) {
+ if (mCurSender == null) {
+ mCurSender = new ImeInputEventSender(mCurChannel, mH.getLooper());
+ }
+
+ final InputEvent event = p.mEvent;
+ final int seq = event.getSequenceNumber();
+ if (mCurSender.sendInputEvent(seq, event)) {
+ mPendingEvents.put(seq, p);
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, PENDING_EVENT_COUNTER,
+ mPendingEvents.size());
+
+ Message msg = mH.obtainMessage(MSG_TIMEOUT_INPUT_EVENT, seq, 0, p);
+ msg.setAsynchronous(true);
+ mH.sendMessageDelayed(msg, INPUT_METHOD_NOT_RESPONDING_TIMEOUT);
+ return DISPATCH_IN_PROGRESS;
+ }
+
+ Log.w(TAG, "Unable to send input event to IME: " + mCurId + " dropping: " + event);
+ }
+ return DISPATCH_NOT_HANDLED;
+ }
+
+ void finishedInputEvent(int seq, boolean handled, boolean timeout) {
+ final PendingEvent p;
+ synchronized (mH) {
+ int index = mPendingEvents.indexOfKey(seq);
+ if (index < 0) {
+ return; // spurious, event already finished or timed out
+ }
+
+ p = mPendingEvents.valueAt(index);
+ mPendingEvents.removeAt(index);
+ Trace.traceCounter(Trace.TRACE_TAG_INPUT, PENDING_EVENT_COUNTER, mPendingEvents.size());
+
+ if (timeout) {
+ Log.w(TAG, "Timeout waiting for IME to handle input event after "
+ + INPUT_METHOD_NOT_RESPONDING_TIMEOUT + " ms: " + p.mInputMethodId);
+ } else {
+ mH.removeMessages(MSG_TIMEOUT_INPUT_EVENT, p);
+ }
+ }
+
+ invokeFinishedInputEventCallback(p, handled);
+ }
+
+ // Assumes the event has already been removed from the queue.
+ void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
+ p.mHandled = handled;
+ if (p.mHandler.getLooper().isCurrentThread()) {
+ // Already running on the callback handler thread so we can send the
+ // callback immediately.
+ p.run();
+ } else {
+ // Post the event to the callback handler thread.
+ // In this case, the callback will be responsible for recycling the event.
+ Message msg = Message.obtain(p.mHandler, p);
+ msg.setAsynchronous(true);
+ msg.sendToTarget();
+ }
+ }
+
+ private void flushPendingEventsLocked() {
+ mH.removeMessages(MSG_FLUSH_INPUT_EVENT);
+
+ final int count = mPendingEvents.size();
+ for (int i = 0; i < count; i++) {
+ int seq = mPendingEvents.keyAt(i);
+ Message msg = mH.obtainMessage(MSG_FLUSH_INPUT_EVENT, seq, 0);
+ msg.setAsynchronous(true);
+ msg.sendToTarget();
+ }
+ }
+
+ private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
+ String inputMethodId, FinishedInputEventCallback callback, Handler handler) {
+ PendingEvent p = mPendingEventPool.acquire();
+ if (p == null) {
+ p = new PendingEvent();
+ }
+ p.mEvent = event;
+ p.mToken = token;
+ p.mInputMethodId = inputMethodId;
+ p.mCallback = callback;
+ p.mHandler = handler;
+ return p;
+ }
+
+ private void recyclePendingEventLocked(PendingEvent p) {
+ p.recycle();
+ mPendingEventPool.release(p);
+ }
+
+ /**
+ * Show IME picker popup window.
+ *
+ * <p>Requires the {@link PackageManager#FEATURE_INPUT_METHODS} feature which can be detected
+ * using {@link PackageManager#hasSystemFeature(String)}.
+ */
+ public void showInputMethodPicker() {
+ synchronized (mH) {
+ showInputMethodPickerLocked();
+ }
+ }
+
+ /**
+ * Shows the input method chooser dialog from system.
+ *
+ * @param showAuxiliarySubtypes Set true to show auxiliary input methods.
+ * @param displayId The ID of the display where the chooser dialog should be shown.
+ * @hide
+ */
+ @RequiresPermission(WRITE_SECURE_SETTINGS)
+ public void showInputMethodPickerFromSystem(boolean showAuxiliarySubtypes, int displayId) {
+ final int mode = showAuxiliarySubtypes
+ ? SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES
+ : SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES;
+ try {
+ mService.showInputMethodPickerFromSystem(mClient, mode, displayId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void showInputMethodPickerLocked() {
+ try {
+ mService.showInputMethodPickerFromClient(mClient, SHOW_IM_PICKER_MODE_AUTO);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * A test API for CTS to make sure that {@link #showInputMethodPicker()} works as expected.
+ *
+ * <p>When customizing the implementation of {@link #showInputMethodPicker()} API, make sure
+ * that this test API returns when and only while and only while
+ * {@link #showInputMethodPicker()} is showing UI. Otherwise your OS implementation may not
+ * pass CTS.</p>
+ *
+ * @return {@code true} while and only while {@link #showInputMethodPicker()} is showing UI.
+ * @hide
+ */
+ @TestApi
+ public boolean isInputMethodPickerShown() {
+ try {
+ return mService.isInputMethodPickerShownForTest();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Show the settings for enabling subtypes of the specified input method.
+ *
+ * @param imiId An input method, whose subtypes settings will be shown. If imiId is null,
+ * subtypes of all input methods will be shown.
+ */
+ public void showInputMethodAndSubtypeEnabler(String imiId) {
+ try {
+ mService.showInputMethodAndSubtypeEnablerFromClient(mClient, imiId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current input method subtype. This subtype is one of the subtypes in
+ * the current input method. This method returns null when the current input method doesn't
+ * have any input method subtype.
+ */
+ public InputMethodSubtype getCurrentInputMethodSubtype() {
+ try {
+ return mService.getCurrentInputMethodSubtype();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Switch to a new input method subtype of the current input method.
+ * @param subtype A new input method subtype to switch.
+ * @return true if the current subtype was successfully switched. When the specified subtype is
+ * null, this method returns false.
+ * @deprecated If the calling process is an IME, use
+ * {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)}, which
+ * does not require any permission as long as the caller is the current IME.
+ * If the calling process is some privileged app that already has
+ * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission, just
+ * directly update {@link Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE}.
+ */
+ @Deprecated
+ @RequiresPermission(WRITE_SECURE_SETTINGS)
+ public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) {
+ if (Process.myUid() == Process.SYSTEM_UID) {
+ Log.w(TAG, "System process should not call setCurrentInputMethodSubtype() because "
+ + "almost always it is a bug under multi-user / multi-profile environment. "
+ + "Consider directly interacting with InputMethodManagerService "
+ + "via LocalServices.");
+ return false;
+ }
+ if (subtype == null) {
+ // See the JavaDoc. This is how this method has worked.
+ return false;
+ }
+ final Context fallbackContext = ActivityThread.currentApplication();
+ if (fallbackContext == null) {
+ return false;
+ }
+ if (fallbackContext.checkSelfPermission(WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ final ContentResolver contentResolver = fallbackContext.getContentResolver();
+ final String imeId = Settings.Secure.getString(contentResolver,
+ Settings.Secure.DEFAULT_INPUT_METHOD);
+ if (ComponentName.unflattenFromString(imeId) == null) {
+ // Null or invalid IME ID format.
+ return false;
+ }
+ final List<InputMethodSubtype> enabledSubtypes;
+ try {
+ enabledSubtypes = mService.getEnabledInputMethodSubtypeList(imeId, true);
+ } catch (RemoteException e) {
+ return false;
+ }
+ final int numSubtypes = enabledSubtypes.size();
+ for (int i = 0; i < numSubtypes; ++i) {
+ final InputMethodSubtype enabledSubtype = enabledSubtypes.get(i);
+ if (enabledSubtype.equals(subtype)) {
+ Settings.Secure.putInt(contentResolver,
+ Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, enabledSubtype.hashCode());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Notify that a user took some action with this input method.
+ *
+ * @deprecated Just kept to avoid possible app compat issue.
+ * @hide
+ */
+ @Deprecated
+ @UnsupportedAppUsage(trackingBug = 114740982, maxTargetSdk = Build.VERSION_CODES.P)
+ public void notifyUserAction() {
+ Log.w(TAG, "notifyUserAction() is a hidden method, which is now just a stub method"
+ + " that does nothing. Leave comments in b.android.com/114740982 if your "
+ + " application still depends on the previous behavior of this method.");
+ }
+
+ /**
+ * Returns a map of all shortcut input method info and their subtypes.
+ */
+ public Map<InputMethodInfo, List<InputMethodSubtype>> getShortcutInputMethodsAndSubtypes() {
+ final List<InputMethodInfo> enabledImes = getEnabledInputMethodList();
+
+ // Ensure we check system IMEs first.
+ enabledImes.sort(Comparator.comparingInt(imi -> imi.isSystem() ? 0 : 1));
+
+ final int numEnabledImes = enabledImes.size();
+ for (int imiIndex = 0; imiIndex < numEnabledImes; ++imiIndex) {
+ final InputMethodInfo imi = enabledImes.get(imiIndex);
+ final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(
+ imi, true);
+ final int subtypeCount = subtypes.size();
+ for (int subtypeIndex = 0; subtypeIndex < subtypeCount; ++subtypeIndex) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(subtypeIndex);
+ if (SUBTYPE_MODE_VOICE.equals(subtype.getMode())) {
+ return Collections.singletonMap(imi, Collections.singletonList(subtype));
+ }
+ }
+ }
+ return Collections.emptyMap();
+ }
+
+ /**
+ * This is kept due to {@link android.compat.annotation.UnsupportedAppUsage}.
+ *
+ * <p>TODO(Bug 113914148): Check if we can remove this. We have accidentally exposed
+ * WindowManagerInternal#getInputMethodWindowVisibleHeight to app developers and some of them
+ * started relying on it.</p>
+ *
+ * @return Something that is not well-defined.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getInputMethodWindowVisibleHeight() {
+ try {
+ return mService.getInputMethodWindowVisibleHeight();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Force switch to the last used input method and subtype. If the last input method didn't have
+ * any subtypes, the framework will simply switch to the last input method with no subtype
+ * specified.
+ * @param imeToken Supplies the identifying token given to an input method when it was started,
+ * which allows it to perform this operation on itself.
+ * @return true if the current input method and subtype was successfully switched to the last
+ * used input method and subtype.
+ * @deprecated Use {@link InputMethodService#switchToPreviousInputMethod()} instead. This method
+ * was intended for IME developers who should be accessing APIs through the service. APIs in
+ * this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public boolean switchToLastInputMethod(IBinder imeToken) {
+ return InputMethodPrivilegedOperationsRegistry.get(imeToken).switchToPreviousInputMethod();
+ }
+
+ /**
+ * Force switch to the next input method and subtype. If there is no IME enabled except
+ * current IME and subtype, do nothing.
+ * @param imeToken Supplies the identifying token given to an input method when it was started,
+ * which allows it to perform this operation on itself.
+ * @param onlyCurrentIme if true, the framework will find the next subtype which
+ * belongs to the current IME
+ * @return true if the current input method and subtype was successfully switched to the next
+ * input method and subtype.
+ * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This
+ * method was intended for IME developers who should be accessing APIs through the service.
+ * APIs in this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) {
+ return InputMethodPrivilegedOperationsRegistry.get(imeToken)
+ .switchToNextInputMethod(onlyCurrentIme);
+ }
+
+ /**
+ * Returns true if the current IME needs to offer the users ways to switch to a next input
+ * method (e.g. a globe key.).
+ * When an IME sets supportsSwitchingToNextInputMethod and this method returns true,
+ * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly.
+ * <p> Note that the system determines the most appropriate next input method
+ * and subtype in order to provide the consistent user experience in switching
+ * between IMEs and subtypes.
+ * @param imeToken Supplies the identifying token given to an input method when it was started,
+ * which allows it to perform this operation on itself.
+ * @deprecated Use {@link InputMethodService#shouldOfferSwitchingToNextInputMethod()}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
+ public boolean shouldOfferSwitchingToNextInputMethod(IBinder imeToken) {
+ return InputMethodPrivilegedOperationsRegistry.get(imeToken)
+ .shouldOfferSwitchingToNextInputMethod();
+ }
+
+ /**
+ * Set additional input method subtypes. Only a process which shares the same uid with the IME
+ * can add additional input method subtypes to the IME.
+ * Please note that a subtype's status is stored in the system.
+ * For example, enabled subtypes are remembered by the framework even after they are removed
+ * by using this method. If you re-add the same subtypes again,
+ * they will just get enabled. If you want to avoid such conflicts, for instance, you may
+ * want to create a "different" new subtype even with the same locale and mode,
+ * by changing its extra value. The different subtype won't get affected by the stored past
+ * status. (You may want to take a look at {@link InputMethodSubtype#hashCode()} to refer
+ * to the current implementation.)
+ *
+ * <p>NOTE: If the same subtype exists in both the manifest XML file and additional subtypes
+ * specified by {@code subtypes}, those multiple instances are automatically merged into one
+ * instance.</p>
+ *
+ * <p>CAVEAT: In API Level 23 and prior, the system may do nothing if an empty
+ * {@link InputMethodSubtype} is specified in {@code subtypes}, which prevents you from removing
+ * the last one entry of additional subtypes. If your IME statically defines one or more
+ * subtypes in the manifest XML file, you may be able to work around this limitation by
+ * specifying one of those statically defined subtypes in {@code subtypes}.</p>
+ *
+ * @param imiId Id of InputMethodInfo which additional input method subtypes will be added to.
+ * @param subtypes subtypes will be added as additional subtypes of the current input method.
+ * @deprecated For IMEs that have already implemented features like customizable/downloadable
+ * keyboard layouts/languages, please start migration to other approaches. One idea
+ * would be exposing only one unified {@link InputMethodSubtype} then implement
+ * IME's own language switching mechanism within that unified subtype. The support
+ * of "Additional Subtype" may be completely dropped in a future version of Android.
+ */
+ @Deprecated
+ public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes) {
+ try {
+ mService.setAdditionalInputMethodSubtypes(imiId, subtypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public InputMethodSubtype getLastInputMethodSubtype() {
+ try {
+ return mService.getLastInputMethodSubtype();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * <p>This is used for CTS test only. Do not use this method outside of CTS package.<p/>
+ * @return the ID of this display which this {@link InputMethodManager} resides
+ * @hide
+ */
+ @TestApi
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ void doDump(FileDescriptor fd, PrintWriter fout, String[] args) {
+ if (processDump(fd, args)) {
+ return;
+ }
+
+ final Printer p = new PrintWriterPrinter(fout);
+ p.println("Input method client state for " + this + ":");
+
+ p.println(" mService=" + mService);
+ p.println(" mMainLooper=" + mMainLooper);
+ p.println(" mIInputContext=" + mIInputContext);
+ p.println(" mActive=" + mActive
+ + " mRestartOnNextWindowFocus=" + mRestartOnNextWindowFocus
+ + " mBindSequence=" + mBindSequence
+ + " mCurId=" + mCurId);
+ p.println(" mFullscreenMode=" + mFullscreenMode);
+ if (mCurrentInputMethodSession != null) {
+ p.println(" mCurMethod=" + mCurrentInputMethodSession);
+ } else {
+ p.println(" mCurMethod= null");
+ }
+ p.println(" mCurRootView=" + mCurRootView);
+ p.println(" mServedView=" + getServedViewLocked());
+ p.println(" mNextServedView=" + getNextServedViewLocked());
+ p.println(" mServedConnecting=" + mServedConnecting);
+ if (mCurrentTextBoxAttribute != null) {
+ p.println(" mCurrentTextBoxAttribute:");
+ mCurrentTextBoxAttribute.dump(p, " ");
+ } else {
+ p.println(" mCurrentTextBoxAttribute: null");
+ }
+ p.println(" mServedInputConnectionWrapper=" + mServedInputConnectionWrapper);
+ p.println(" mCompletions=" + Arrays.toString(mCompletions));
+ p.println(" mCursorRect=" + mCursorRect);
+ p.println(" mCursorSelStart=" + mCursorSelStart
+ + " mCursorSelEnd=" + mCursorSelEnd
+ + " mCursorCandStart=" + mCursorCandStart
+ + " mCursorCandEnd=" + mCursorCandEnd);
+ }
+
+ /**
+ * Callback that is invoked when an input event that was dispatched to
+ * the IME has been finished.
+ * @hide
+ */
+ public interface FinishedInputEventCallback {
+ public void onFinishedInputEvent(Object token, boolean handled);
+ }
+
+ private final class ImeInputEventSender extends InputEventSender {
+ public ImeInputEventSender(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
+
+ @Override
+ public void onInputEventFinished(int seq, boolean handled) {
+ finishedInputEvent(seq, handled, false);
+ }
+ }
+
+ private final class PendingEvent implements Runnable {
+ public InputEvent mEvent;
+ public Object mToken;
+ public String mInputMethodId;
+ public FinishedInputEventCallback mCallback;
+ public Handler mHandler;
+ public boolean mHandled;
+
+ public void recycle() {
+ mEvent = null;
+ mToken = null;
+ mInputMethodId = null;
+ mCallback = null;
+ mHandler = null;
+ mHandled = false;
+ }
+
+ @Override
+ public void run() {
+ mCallback.onFinishedInputEvent(mToken, mHandled);
+
+ synchronized (mH) {
+ recyclePendingEventLocked(this);
+ }
+ }
+ }
+
+ private static String dumpViewInfo(@Nullable final View view) {
+ if (view == null) {
+ return "null";
+ }
+ final StringBuilder sb = new StringBuilder();
+ sb.append(view);
+ sb.append(",focus=" + view.hasFocus());
+ sb.append(",windowFocus=" + view.hasWindowFocus());
+ sb.append(",autofillUiShowing=" + isAutofillUIShowing(view));
+ sb.append(",window=" + view.getWindowToken());
+ sb.append(",displayId=" + view.getContext().getDisplayId());
+ sb.append(",temporaryDetach=" + view.isTemporarilyDetached());
+ sb.append(",hasImeFocus=" + view.hasImeFocus());
+
+ return sb.toString();
+ }
+
+ /**
+ * Checks the args to see if a proto-based ime dump was requested and writes the client side
+ * ime dump to the given {@link FileDescriptor}.
+ *
+ * @return {@code true} if a proto-based ime dump was requested.
+ */
+ private boolean processDump(final FileDescriptor fd, final String[] args) {
+ if (args == null) {
+ return false;
+ }
+
+ for (String arg : args) {
+ if (arg.equals(PROTO_ARG)) {
+ final ProtoOutputStream proto = new ProtoOutputStream(fd);
+ dumpDebug(proto, null /* icProto */);
+ proto.flush();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Write the proto dump of various client side components to the provided
+ * {@link ProtoOutputStream}.
+ *
+ * @param proto The proto stream to which the dumps are written.
+ * @param icProto {@link InputConnection} call data in proto format.
+ * @hide
+ */
+ @GuardedBy("mH")
+ public void dumpDebug(ProtoOutputStream proto, ProtoOutputStream icProto) {
+ if (mCurrentInputMethodSession == null) {
+ return;
+ }
+
+ proto.write(DISPLAY_ID, mDisplayId);
+ final long token = proto.start(INPUT_METHOD_MANAGER);
+ synchronized (mH) {
+ proto.write(CUR_ID, mCurId);
+ proto.write(FULLSCREEN_MODE, mFullscreenMode);
+ proto.write(ACTIVE, mActive);
+ proto.write(SERVED_CONNECTING, mServedConnecting);
+ proto.end(token);
+ if (mCurRootView != null) {
+ mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL);
+ }
+ if (mCurrentTextBoxAttribute != null) {
+ mCurrentTextBoxAttribute.dumpDebug(proto, EDITOR_INFO);
+ }
+ if (mImeInsetsConsumer != null) {
+ mImeInsetsConsumer.dumpDebug(proto, IME_INSETS_SOURCE_CONSUMER);
+ }
+ if (mServedInputConnectionWrapper != null) {
+ mServedInputConnectionWrapper.dumpDebug(proto, INPUT_CONNECTION);
+ }
+ if (icProto != null) {
+ proto.write(INPUT_CONNECTION_CALL, icProto.getBytes());
+ }
+ }
+ }
+}
diff --git a/android/view/inputmethod/InputMethodManager_Accessor.java b/android/view/inputmethod/InputMethodManager_Accessor.java
new file mode 100644
index 0000000..85dc5d3
--- /dev/null
+++ b/android/view/inputmethod/InputMethodManager_Accessor.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+/**
+ * Class allowing access to package-protected methods/fields.
+ */
+public class InputMethodManager_Accessor {
+
+ public static void tearDownEditMode() {
+ InputMethodManager.tearDownEditMode();
+ }
+}
diff --git a/android/view/inputmethod/InputMethodManager_Delegate.java b/android/view/inputmethod/InputMethodManager_Delegate.java
new file mode 100644
index 0000000..5dd25b4
--- /dev/null
+++ b/android/view/inputmethod/InputMethodManager_Delegate.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+
+/**
+ * Delegate used to provide new implementation of a select few methods of {@link InputMethodManager}
+ *
+ * Through the layoutlib_create tool, the original methods of InputMethodManager have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class InputMethodManager_Delegate {
+
+ // ---- Overridden methods ----
+
+ @LayoutlibDelegate
+ /*package*/ static boolean isInEditMode() {
+ return true;
+ }
+}
diff --git a/android/view/inputmethod/InputMethodSession.java b/android/view/inputmethod/InputMethodSession.java
new file mode 100644
index 0000000..52c1cd4
--- /dev/null
+++ b/android/view/inputmethod/InputMethodSession.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2007-2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.inputmethod;
+
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * The InputMethodSession interface provides the per-client functionality
+ * of {@link InputMethod} that is safe to expose to applications.
+ *
+ * <p>Applications will not normally use this interface themselves, instead
+ * relying on the standard interaction provided by
+ * {@link android.widget.TextView} and {@link android.widget.EditText}.
+ */
+public interface InputMethodSession {
+
+ public interface EventCallback {
+ void finishedEvent(int seq, boolean handled);
+ }
+
+ /**
+ * This method is called when the application would like to stop
+ * receiving text input.
+ */
+ public void finishInput();
+
+ /**
+ * This method is called when the selection or cursor in the current
+ * target input field has changed.
+ *
+ * @param oldSelStart The previous text offset of the cursor selection
+ * start position.
+ * @param oldSelEnd The previous text offset of the cursor selection
+ * end position.
+ * @param newSelStart The new text offset of the cursor selection
+ * start position.
+ * @param newSelEnd The new text offset of the cursor selection
+ * end position.
+ * @param candidatesStart The text offset of the current candidate
+ * text start position.
+ * @param candidatesEnd The text offset of the current candidate
+ * text end position.
+ */
+ public void updateSelection(int oldSelStart, int oldSelEnd,
+ int newSelStart, int newSelEnd,
+ int candidatesStart, int candidatesEnd);
+
+ /**
+ * This method is called when the user tapped a text view.
+ * IMEs can't rely on this method being called because this was not part of the original IME
+ * protocol, so applications with custom text editing written before this method appeared will
+ * not call to inform the IME of this interaction.
+ * @param focusChanged true if the user changed the focused view by this click.
+ */
+ public void viewClicked(boolean focusChanged);
+
+ /**
+ * This method is called when cursor location of the target input field
+ * has changed within its window. This is not normally called, but will
+ * only be reported if requested by the input method.
+ *
+ * @param newCursor The rectangle of the cursor currently being shown in
+ * the input field's window coordinates.
+ */
+ public void updateCursor(Rect newCursor);
+
+ /**
+ * Called by a text editor that performs auto completion, to tell the
+ * input method about the completions it has available. This can be used
+ * by the input method to display them to the user to select the text to
+ * be inserted.
+ *
+ * @param completions Array of text completions that are available, starting with
+ * the best. If this array is null, any existing completions will be
+ * removed.
+ */
+ public void displayCompletions(CompletionInfo[] completions);
+
+ /**
+ * Called by a text editor to report its new extracted text when its
+ * contents change. This will only be called if the input method
+ * calls {@link InputConnection#getExtractedText(ExtractedTextRequest, int)
+ * InputConnection.getExtractedText()} with the option to report updates.
+ *
+ * @param token The input method supplied token for identifying its request.
+ * @param text The new extracted text.
+ */
+ public void updateExtractedText(int token, ExtractedText text);
+
+ /**
+ * This method is called when a key is pressed. When done with the event,
+ * the implementation must call back on <var>callback</var> with its
+ * result.
+ *
+ * <p>
+ * If the input method wants to handle this event, return true, otherwise
+ * return false and the caller (i.e. the application) will handle the event.
+ *
+ * @param event The key event.
+ *
+ * @return Whether the input method wants to handle this event.
+ *
+ * @see android.view.KeyEvent
+ */
+ public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback);
+
+ /**
+ * This method is called when there is a track ball event.
+ *
+ * <p>
+ * If the input method wants to handle this event, return true, otherwise
+ * return false and the caller (i.e. the application) will handle the event.
+ *
+ * @param event The motion event.
+ *
+ * @return Whether the input method wants to handle this event.
+ *
+ * @see android.view.MotionEvent
+ */
+ public void dispatchTrackballEvent(int seq, MotionEvent event, EventCallback callback);
+
+ /**
+ * This method is called when there is a generic motion event.
+ *
+ * <p>
+ * If the input method wants to handle this event, return true, otherwise
+ * return false and the caller (i.e. the application) will handle the event.
+ *
+ * @param event The motion event.
+ *
+ * @return Whether the input method wants to handle this event.
+ *
+ * @see android.view.MotionEvent
+ */
+ public void dispatchGenericMotionEvent(int seq, MotionEvent event, EventCallback callback);
+
+ /**
+ * Process a private command sent from the application to the input method.
+ * This can be used to provide domain-specific features that are
+ * only known between certain input methods and their clients.
+ *
+ * @param action Name of the command to be performed. This <em>must</em>
+ * be a scoped name, i.e. prefixed with a package name you own, so that
+ * different developers will not create conflicting commands.
+ * @param data Any data to include with the command.
+ */
+ public void appPrivateCommand(String action, Bundle data);
+
+ /**
+ * Toggle the soft input window.
+ * Applications can toggle the state of the soft input window.
+ * @param showFlags Provides additional operating flags. May be
+ * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT},
+ * {@link InputMethodManager#SHOW_FORCED} bit set.
+ * @param hideFlags Provides additional operating flags. May be
+ * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY},
+ * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set.
+ *
+ * @deprecated Starting in {@link Build.VERSION_CODES#S} the system no longer invokes this
+ * method, instead it explicitly shows or hides the IME. An {@code InputMethodService}
+ * wishing to toggle its own visibility should instead invoke {@link
+ * InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
+ */
+ @Deprecated
+ public void toggleSoftInput(int showFlags, int hideFlags);
+
+ /**
+ * This method is called when the cursor and/or the character position relevant to text input
+ * is changed on the screen. This is not called by default. It will only be reported if
+ * requested by the input method.
+ *
+ * @param cursorAnchorInfo Positional information relevant to text input, such as text
+ * insertion point and composition string.
+ */
+ public void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo);
+
+ /**
+ * Notifies {@link android.inputmethodservice.InputMethodService} that IME has been
+ * hidden from user.
+ * @hide
+ */
+ public void notifyImeHidden();
+
+ /**
+ * Notify IME directly to remove surface as it is no longer visible.
+ * @hide
+ */
+ public void removeImeSurface();
+}
diff --git a/android/view/inputmethod/InputMethodSessionWrapper.java b/android/view/inputmethod/InputMethodSessionWrapper.java
new file mode 100644
index 0000000..ef1814b
--- /dev/null
+++ b/android/view/inputmethod/InputMethodSessionWrapper.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.view.IInputMethodSession;
+
+/**
+ * This class wrap the {@link IInputMethodSession} object from {@link InputMethodManager}.
+ * Using current {@link IInputMethodSession} object to communicate with
+ * {@link android.inputmethodservice.InputMethodService}.
+ */
+final class InputMethodSessionWrapper {
+
+ private static final String TAG = "InputMethodSessionWrapper";
+
+ /**
+ * The actual instance of the method to make calls on it.
+ */
+ @NonNull
+ private final IInputMethodSession mSession;
+
+ private InputMethodSessionWrapper(@NonNull IInputMethodSession inputMethodSession) {
+ mSession = inputMethodSession;
+ }
+
+ /**
+ * Create a {@link InputMethodSessionWrapper} instance if applicability.
+ *
+ * @param inputMethodSession {@link IInputMethodSession} object to be wrapped.
+ * @return an instance of {@link InputMethodSessionWrapper} if {@code inputMethodSession} is not
+ * {@code null}. {@code null} otherwise.
+ */
+ @Nullable
+ public static InputMethodSessionWrapper createOrNull(
+ @NonNull IInputMethodSession inputMethodSession) {
+ return inputMethodSession != null ? new InputMethodSessionWrapper(inputMethodSession)
+ : null;
+ }
+
+ @AnyThread
+ void finishInput() {
+ try {
+ mSession.finishInput();
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
+ try {
+ mSession.updateCursorAnchorInfo(cursorAnchorInfo);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void displayCompletions(CompletionInfo[] completions) {
+ try {
+ mSession.displayCompletions(completions);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void updateExtractedText(int token, ExtractedText text) {
+ try {
+ mSession.updateExtractedText(token, text);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void appPrivateCommand(String action, Bundle data) {
+ try {
+ mSession.appPrivateCommand(action, data);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void notifyImeHidden() {
+ try {
+ mSession.notifyImeHidden();
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void viewClicked(boolean focusChanged) {
+ try {
+ mSession.viewClicked(focusChanged);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void updateCursor(Rect newCursor) {
+ try {
+ mSession.updateCursor(newCursor);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ @AnyThread
+ void updateSelection(int oldSelStart, int oldSelEnd, int selStart, int selEnd,
+ int candidatesStart, int candidatesEnd) {
+ try {
+ mSession.updateSelection(
+ oldSelStart, oldSelEnd, selStart, selEnd, candidatesStart, candidatesEnd);
+ } catch (RemoteException e) {
+ Log.w(TAG, "IME died", e);
+ }
+ }
+
+ /**
+ * @return {@link IInputMethodSession#toString()} as a debug string.
+ */
+ @AnyThread
+ @NonNull
+ @Override
+ public String toString() {
+ return mSession.toString();
+ }
+}
diff --git a/android/view/inputmethod/InputMethodSubtype.java b/android/view/inputmethod/InputMethodSubtype.java
new file mode 100644
index 0000000..9c63d56
--- /dev/null
+++ b/android/view/inputmethod/InputMethodSubtype.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.icu.text.DisplayContext;
+import android.icu.text.LocaleDisplayNames;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.inputmethod.SubtypeLocaleUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IllegalFormatException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * This class is used to specify meta information of a subtype contained in an input method editor
+ * (IME). Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard...),
+ * and is used for IME switch and settings. The input method subtype allows the system to bring up
+ * the specified subtype of the designated IME directly.
+ *
+ * <p>It should be defined in an XML resource file of the input method with the
+ * <code><subtype></code> element, which resides within an {@code <input-method>} element.
+ * For more information, see the guide to
+ * <a href="{@docRoot}guide/topics/text/creating-input-method.html">
+ * Creating an Input Method</a>.</p>
+ *
+ * @see InputMethodInfo
+ *
+ * @attr ref android.R.styleable#InputMethod_Subtype_label
+ * @attr ref android.R.styleable#InputMethod_Subtype_icon
+ * @attr ref android.R.styleable#InputMethod_Subtype_languageTag
+ * @attr ref android.R.styleable#InputMethod_Subtype_imeSubtypeLocale
+ * @attr ref android.R.styleable#InputMethod_Subtype_imeSubtypeMode
+ * @attr ref android.R.styleable#InputMethod_Subtype_imeSubtypeExtraValue
+ * @attr ref android.R.styleable#InputMethod_Subtype_isAuxiliary
+ * @attr ref android.R.styleable#InputMethod_Subtype_overridesImplicitlyEnabledSubtype
+ * @attr ref android.R.styleable#InputMethod_Subtype_subtypeId
+ * @attr ref android.R.styleable#InputMethod_Subtype_isAsciiCapable
+ */
+public final class InputMethodSubtype implements Parcelable {
+ private static final String TAG = InputMethodSubtype.class.getSimpleName();
+ private static final String LANGUAGE_TAG_NONE = "";
+ private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
+ private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
+ // TODO: remove this
+ private static final String EXTRA_KEY_UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME =
+ "UntranslatableReplacementStringInSubtypeName";
+ /** {@hide} */
+ public static final int SUBTYPE_ID_NONE = 0;
+
+ private final boolean mIsAuxiliary;
+ private final boolean mOverridesImplicitlyEnabledSubtype;
+ private final boolean mIsAsciiCapable;
+ private final int mSubtypeHashCode;
+ private final int mSubtypeIconResId;
+ private final int mSubtypeNameResId;
+ private final int mSubtypeId;
+ private final String mSubtypeLocale;
+ private final String mSubtypeLanguageTag;
+ private final String mSubtypeMode;
+ private final String mSubtypeExtraValue;
+ private final Object mLock = new Object();
+ private volatile Locale mCachedLocaleObj;
+ private volatile HashMap<String, String> mExtraValueHashMapCache;
+
+ /**
+ * InputMethodSubtypeBuilder is a builder class of InputMethodSubtype.
+ * This class is designed to be used with
+ * {@link android.view.inputmethod.InputMethodManager#setAdditionalInputMethodSubtypes}.
+ * The developer needs to be aware of what each parameter means.
+ */
+ public static class InputMethodSubtypeBuilder {
+ /**
+ * @param isAuxiliary should true when this subtype is auxiliary, false otherwise.
+ * An auxiliary subtype has the following differences with a regular subtype:
+ * - An auxiliary subtype cannot be chosen as the default IME in Settings.
+ * - The framework will never switch to this subtype through
+ * {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+ * Note that the subtype will still be available in the IME switcher.
+ * The intent is to allow for IMEs to specify they are meant to be invoked temporarily
+ * in a one-shot way, and to return to the previous IME once finished (e.g. voice input).
+ */
+ public InputMethodSubtypeBuilder setIsAuxiliary(boolean isAuxiliary) {
+ mIsAuxiliary = isAuxiliary;
+ return this;
+ }
+ private boolean mIsAuxiliary = false;
+
+ /**
+ * @param overridesImplicitlyEnabledSubtype should be true if this subtype should be
+ * enabled by default if no other subtypes in the IME are enabled explicitly. Note that a
+ * subtype with this parameter set will not be shown in the list of subtypes in each IME's
+ * subtype enabler. A canonical use of this would be for an IME to supply an "automatic"
+ * subtype that adapts to the current system language.
+ */
+ public InputMethodSubtypeBuilder setOverridesImplicitlyEnabledSubtype(
+ boolean overridesImplicitlyEnabledSubtype) {
+ mOverridesImplicitlyEnabledSubtype = overridesImplicitlyEnabledSubtype;
+ return this;
+ }
+ private boolean mOverridesImplicitlyEnabledSubtype = false;
+
+ /**
+ * @param isAsciiCapable should be true if this subtype is ASCII capable. If the subtype
+ * is ASCII capable, it should guarantee that the user can input ASCII characters with
+ * this subtype. This is important because many password fields only allow
+ * ASCII-characters.
+ */
+ public InputMethodSubtypeBuilder setIsAsciiCapable(boolean isAsciiCapable) {
+ mIsAsciiCapable = isAsciiCapable;
+ return this;
+ }
+ private boolean mIsAsciiCapable = false;
+
+ /**
+ * @param subtypeIconResId is a resource ID of the subtype icon drawable.
+ */
+ public InputMethodSubtypeBuilder setSubtypeIconResId(int subtypeIconResId) {
+ mSubtypeIconResId = subtypeIconResId;
+ return this;
+ }
+ private int mSubtypeIconResId = 0;
+
+ /**
+ * @param subtypeNameResId is the resource ID of the subtype name string.
+ * The string resource may have exactly one %s in it. If present,
+ * the %s part will be replaced with the locale's display name by
+ * the formatter. Please refer to {@link #getDisplayName} for details.
+ */
+ public InputMethodSubtypeBuilder setSubtypeNameResId(int subtypeNameResId) {
+ mSubtypeNameResId = subtypeNameResId;
+ return this;
+ }
+ private int mSubtypeNameResId = 0;
+
+ /**
+ * @param subtypeId is the unique ID for this subtype. The input method framework keeps
+ * track of enabled subtypes by ID. When the IME package gets upgraded, enabled IDs will
+ * stay enabled even if other attributes are different. If the ID is unspecified or 0,
+ * Arrays.hashCode(new Object[] {locale, mode, extraValue,
+ * isAuxiliary, overridesImplicitlyEnabledSubtype, isAsciiCapable}) will be used instead.
+ */
+ public InputMethodSubtypeBuilder setSubtypeId(int subtypeId) {
+ mSubtypeId = subtypeId;
+ return this;
+ }
+ private int mSubtypeId = SUBTYPE_ID_NONE;
+
+ /**
+ * @param subtypeLocale is the locale supported by this subtype.
+ */
+ public InputMethodSubtypeBuilder setSubtypeLocale(String subtypeLocale) {
+ mSubtypeLocale = subtypeLocale == null ? "" : subtypeLocale;
+ return this;
+ }
+ private String mSubtypeLocale = "";
+
+ /**
+ * @param languageTag is the BCP-47 Language Tag supported by this subtype.
+ */
+ public InputMethodSubtypeBuilder setLanguageTag(String languageTag) {
+ mSubtypeLanguageTag = languageTag == null ? LANGUAGE_TAG_NONE : languageTag;
+ return this;
+ }
+ private String mSubtypeLanguageTag = LANGUAGE_TAG_NONE;
+
+ /**
+ * @param subtypeMode is the mode supported by this subtype.
+ */
+ public InputMethodSubtypeBuilder setSubtypeMode(String subtypeMode) {
+ mSubtypeMode = subtypeMode == null ? "" : subtypeMode;
+ return this;
+ }
+ private String mSubtypeMode = "";
+ /**
+ * @param subtypeExtraValue is the extra value of the subtype. This string is free-form,
+ * but the API supplies tools to deal with a key-value comma-separated list; see
+ * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
+ */
+ public InputMethodSubtypeBuilder setSubtypeExtraValue(String subtypeExtraValue) {
+ mSubtypeExtraValue = subtypeExtraValue == null ? "" : subtypeExtraValue;
+ return this;
+ }
+ private String mSubtypeExtraValue = "";
+
+ /**
+ * @return InputMethodSubtype using parameters in this InputMethodSubtypeBuilder.
+ */
+ public InputMethodSubtype build() {
+ return new InputMethodSubtype(this);
+ }
+ }
+
+ private static InputMethodSubtypeBuilder getBuilder(int nameId, int iconId, String locale,
+ String mode, String extraValue, boolean isAuxiliary,
+ boolean overridesImplicitlyEnabledSubtype, int id, boolean isAsciiCapable) {
+ final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
+ builder.mSubtypeNameResId = nameId;
+ builder.mSubtypeIconResId = iconId;
+ builder.mSubtypeLocale = locale;
+ builder.mSubtypeMode = mode;
+ builder.mSubtypeExtraValue = extraValue;
+ builder.mIsAuxiliary = isAuxiliary;
+ builder.mOverridesImplicitlyEnabledSubtype = overridesImplicitlyEnabledSubtype;
+ builder.mSubtypeId = id;
+ builder.mIsAsciiCapable = isAsciiCapable;
+ return builder;
+ }
+
+ /**
+ * Constructor with no subtype ID specified.
+ * @deprecated use {@link InputMethodSubtypeBuilder} instead.
+ * Arguments for this constructor have the same meanings as
+ * {@link InputMethodSubtype#InputMethodSubtype(int, int, String, String, String, boolean,
+ * boolean, int)} except "id".
+ */
+ @Deprecated
+ public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
+ boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype) {
+ this(nameId, iconId, locale, mode, extraValue, isAuxiliary,
+ overridesImplicitlyEnabledSubtype, 0);
+ }
+
+ /**
+ * Constructor.
+ * @deprecated use {@link InputMethodSubtypeBuilder} instead.
+ * "isAsciiCapable" is "false" in this constructor.
+ * @param nameId Resource ID of the subtype name string. The string resource may have exactly
+ * one %s in it. If there is, the %s part will be replaced with the locale's display name by
+ * the formatter. Please refer to {@link #getDisplayName} for details.
+ * @param iconId Resource ID of the subtype icon drawable.
+ * @param locale The locale supported by the subtype
+ * @param mode The mode supported by the subtype
+ * @param extraValue The extra value of the subtype. This string is free-form, but the API
+ * supplies tools to deal with a key-value comma-separated list; see
+ * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
+ * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary
+ * subtype will not be shown in the list of enabled IMEs for choosing the current IME in
+ * the Settings even when this subtype is enabled. Please note that this subtype will still
+ * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch
+ * to this subtype while an IME is shown. The framework will never switch the current IME to
+ * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+ * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
+ * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
+ * @param overridesImplicitlyEnabledSubtype true when this subtype should be enabled by default
+ * if no other subtypes in the IME are enabled explicitly. Note that a subtype with this
+ * parameter being true will not be shown in the list of subtypes in each IME's subtype enabler.
+ * Having an "automatic" subtype is an example use of this flag.
+ * @param id The unique ID for the subtype. The input method framework keeps track of enabled
+ * subtypes by ID. When the IME package gets upgraded, enabled IDs will stay enabled even if
+ * other attributes are different. If the ID is unspecified or 0,
+ * Arrays.hashCode(new Object[] {locale, mode, extraValue,
+ * isAuxiliary, overridesImplicitlyEnabledSubtype, isAsciiCapable}) will be used instead.
+ */
+ @Deprecated
+ public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
+ boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, int id) {
+ this(getBuilder(nameId, iconId, locale, mode, extraValue, isAuxiliary,
+ overridesImplicitlyEnabledSubtype, id, false));
+ }
+
+ /**
+ * Constructor.
+ * @param builder Builder for InputMethodSubtype
+ */
+ private InputMethodSubtype(InputMethodSubtypeBuilder builder) {
+ mSubtypeNameResId = builder.mSubtypeNameResId;
+ mSubtypeIconResId = builder.mSubtypeIconResId;
+ mSubtypeLocale = builder.mSubtypeLocale;
+ mSubtypeLanguageTag = builder.mSubtypeLanguageTag;
+ mSubtypeMode = builder.mSubtypeMode;
+ mSubtypeExtraValue = builder.mSubtypeExtraValue;
+ mIsAuxiliary = builder.mIsAuxiliary;
+ mOverridesImplicitlyEnabledSubtype = builder.mOverridesImplicitlyEnabledSubtype;
+ mSubtypeId = builder.mSubtypeId;
+ mIsAsciiCapable = builder.mIsAsciiCapable;
+ // If hashCode() of this subtype is 0 and you want to specify it as an id of this subtype,
+ // just specify 0 as this subtype's id. Then, this subtype's id is treated as 0.
+ if (mSubtypeId != SUBTYPE_ID_NONE) {
+ mSubtypeHashCode = mSubtypeId;
+ } else {
+ mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue,
+ mIsAuxiliary, mOverridesImplicitlyEnabledSubtype, mIsAsciiCapable);
+ }
+ }
+
+ InputMethodSubtype(Parcel source) {
+ String s;
+ mSubtypeNameResId = source.readInt();
+ mSubtypeIconResId = source.readInt();
+ s = source.readString();
+ mSubtypeLocale = s != null ? s : "";
+ s = source.readString();
+ mSubtypeLanguageTag = s != null ? s : LANGUAGE_TAG_NONE;
+ s = source.readString();
+ mSubtypeMode = s != null ? s : "";
+ s = source.readString();
+ mSubtypeExtraValue = s != null ? s : "";
+ mIsAuxiliary = (source.readInt() == 1);
+ mOverridesImplicitlyEnabledSubtype = (source.readInt() == 1);
+ mSubtypeHashCode = source.readInt();
+ mSubtypeId = source.readInt();
+ mIsAsciiCapable = (source.readInt() == 1);
+ }
+
+ /**
+ * @return Resource ID of the subtype name string.
+ */
+ public int getNameResId() {
+ return mSubtypeNameResId;
+ }
+
+ /**
+ * @return Resource ID of the subtype icon drawable.
+ */
+ public int getIconResId() {
+ return mSubtypeIconResId;
+ }
+
+ /**
+ * @return The locale of the subtype. This method returns the "locale" string parameter passed
+ * to the constructor.
+ *
+ * @deprecated Use {@link #getLanguageTag()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public String getLocale() {
+ return mSubtypeLocale;
+ }
+
+ /**
+ * @return the BCP-47 Language Tag of the subtype. Returns an empty string when no Language Tag
+ * is specified.
+ *
+ * @see Locale#forLanguageTag(String)
+ */
+ @NonNull
+ public String getLanguageTag() {
+ return mSubtypeLanguageTag;
+ }
+
+ /**
+ * @return {@link Locale} constructed from {@link #getLanguageTag()}. If the Language Tag is not
+ * specified, then try to construct from {@link #getLocale()}
+ *
+ * <p>TODO: Consider to make this a public API, or move this to support lib.</p>
+ * @hide
+ */
+ @Nullable
+ public Locale getLocaleObject() {
+ if (mCachedLocaleObj != null) {
+ return mCachedLocaleObj;
+ }
+ synchronized (mLock) {
+ if (mCachedLocaleObj != null) {
+ return mCachedLocaleObj;
+ }
+ if (!TextUtils.isEmpty(mSubtypeLanguageTag)) {
+ mCachedLocaleObj = Locale.forLanguageTag(mSubtypeLanguageTag);
+ } else {
+ mCachedLocaleObj = SubtypeLocaleUtils.constructLocaleFromString(mSubtypeLocale);
+ }
+ return mCachedLocaleObj;
+ }
+ }
+
+ /**
+ * @return The mode of the subtype.
+ */
+ public String getMode() {
+ return mSubtypeMode;
+ }
+
+ /**
+ * @return The extra value of the subtype.
+ */
+ public String getExtraValue() {
+ return mSubtypeExtraValue;
+ }
+
+ /**
+ * @return true if this subtype is auxiliary, false otherwise. An auxiliary subtype will not be
+ * shown in the list of enabled IMEs for choosing the current IME in the Settings even when this
+ * subtype is enabled. Please note that this subtype will still be shown in the list of IMEs in
+ * the IME switcher to allow the user to tentatively switch to this subtype while an IME is
+ * shown. The framework will never switch the current IME to this subtype by
+ * {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+ * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
+ * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
+ */
+ public boolean isAuxiliary() {
+ return mIsAuxiliary;
+ }
+
+ /**
+ * @return true when this subtype will be enabled by default if no other subtypes in the IME
+ * are enabled explicitly, false otherwise. Note that a subtype with this method returning true
+ * will not be shown in the list of subtypes in each IME's subtype enabler. Having an
+ * "automatic" subtype is an example use of this flag.
+ */
+ public boolean overridesImplicitlyEnabledSubtype() {
+ return mOverridesImplicitlyEnabledSubtype;
+ }
+
+ /**
+ * @return true if this subtype is Ascii capable, false otherwise. If the subtype is ASCII
+ * capable, it should guarantee that the user can input ASCII characters with this subtype.
+ * This is important because many password fields only allow ASCII-characters.
+ */
+ public boolean isAsciiCapable() {
+ return mIsAsciiCapable;
+ }
+
+ /**
+ * Returns a display name for this subtype.
+ *
+ * <p>If {@code subtypeNameResId} is specified (!= 0) text generated from that resource will
+ * be returned. The localized string resource of the label should be capitalized for inclusion
+ * in UI lists. The string resource may contain at most one {@code %s}. If present, the
+ * {@code %s} will be replaced with the display name of the subtype locale in the user's locale.
+ *
+ * <p>If {@code subtypeNameResId} is not specified (== 0) the framework returns the display name
+ * of the subtype locale, as capitalized for use in UI lists, in the user's locale.
+ *
+ * @param context {@link Context} will be used for getting {@link Locale} and
+ * {@link android.content.pm.PackageManager}.
+ * @param packageName The package name of the input method.
+ * @param appInfo The {@link ApplicationInfo} of the input method.
+ * @return a display name for this subtype.
+ */
+ @NonNull
+ public CharSequence getDisplayName(
+ Context context, String packageName, ApplicationInfo appInfo) {
+ if (mSubtypeNameResId == 0) {
+ return getLocaleDisplayName(getLocaleFromContext(context), getLocaleObject(),
+ DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU);
+ }
+
+ final CharSequence subtypeName = context.getPackageManager().getText(
+ packageName, mSubtypeNameResId, appInfo);
+ if (TextUtils.isEmpty(subtypeName)) {
+ return "";
+ }
+ final String subtypeNameString = subtypeName.toString();
+ String replacementString;
+ if (containsExtraValueKey(EXTRA_KEY_UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) {
+ replacementString = getExtraValueOf(
+ EXTRA_KEY_UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME);
+ } else {
+ final DisplayContext displayContext;
+ if (TextUtils.equals(subtypeNameString, "%s")) {
+ displayContext = DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU;
+ } else if (subtypeNameString.startsWith("%s")) {
+ displayContext = DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE;
+ } else {
+ displayContext = DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE;
+ }
+ replacementString = getLocaleDisplayName(getLocaleFromContext(context),
+ getLocaleObject(), displayContext);
+ }
+ if (replacementString == null) {
+ replacementString = "";
+ }
+ try {
+ return String.format(subtypeNameString, replacementString);
+ } catch (IllegalFormatException e) {
+ Slog.w(TAG, "Found illegal format in subtype name("+ subtypeName + "): " + e);
+ return "";
+ }
+ }
+
+ @Nullable
+ private static Locale getLocaleFromContext(@Nullable final Context context) {
+ if (context == null) {
+ return null;
+ }
+ if (context.getResources() == null) {
+ return null;
+ }
+ final Configuration configuration = context.getResources().getConfiguration();
+ if (configuration == null) {
+ return null;
+ }
+ return configuration.getLocales().get(0);
+ }
+
+ /**
+ * @param displayLocale {@link Locale} to be used to display {@code localeToDisplay}
+ * @param localeToDisplay {@link Locale} to be displayed in {@code displayLocale}
+ * @param displayContext context parameter to be used to display {@code localeToDisplay} in
+ * {@code displayLocale}
+ * @return Returns the name of the {@code localeToDisplay} in the user's current locale.
+ */
+ @NonNull
+ private static String getLocaleDisplayName(
+ @Nullable Locale displayLocale, @Nullable Locale localeToDisplay,
+ final DisplayContext displayContext) {
+ if (localeToDisplay == null) {
+ return "";
+ }
+ final Locale nonNullDisplayLocale =
+ displayLocale != null ? displayLocale : Locale.getDefault();
+ return LocaleDisplayNames
+ .getInstance(nonNullDisplayLocale, displayContext)
+ .localeDisplayName(localeToDisplay);
+ }
+
+ private HashMap<String, String> getExtraValueHashMap() {
+ synchronized (this) {
+ HashMap<String, String> extraValueMap = mExtraValueHashMapCache;
+ if (extraValueMap != null) {
+ return extraValueMap;
+ }
+ extraValueMap = new HashMap<>();
+ final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR);
+ for (int i = 0; i < pairs.length; ++i) {
+ final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR);
+ if (pair.length == 1) {
+ extraValueMap.put(pair[0], null);
+ } else if (pair.length > 1) {
+ if (pair.length > 2) {
+ Slog.w(TAG, "ExtraValue has two or more '='s");
+ }
+ extraValueMap.put(pair[0], pair[1]);
+ }
+ }
+ mExtraValueHashMapCache = extraValueMap;
+ return extraValueMap;
+ }
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key The key of extra value
+ * @return The subtype contains specified the extra value
+ */
+ public boolean containsExtraValueKey(String key) {
+ return getExtraValueHashMap().containsKey(key);
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key The key of extra value
+ * @return The value of the specified key
+ */
+ public String getExtraValueOf(String key) {
+ return getExtraValueHashMap().get(key);
+ }
+
+ @Override
+ public int hashCode() {
+ return mSubtypeHashCode;
+ }
+
+ /**
+ * @hide
+ * @return {@code true} if a valid subtype ID exists.
+ */
+ public final boolean hasSubtypeId() {
+ return mSubtypeId != SUBTYPE_ID_NONE;
+ }
+
+ /**
+ * @hide
+ * @return subtype ID. {@code 0} means that not subtype ID is specified.
+ */
+ public final int getSubtypeId() {
+ return mSubtypeId;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof InputMethodSubtype) {
+ InputMethodSubtype subtype = (InputMethodSubtype) o;
+ if (subtype.mSubtypeId != 0 || mSubtypeId != 0) {
+ return (subtype.hashCode() == hashCode());
+ }
+ return (subtype.hashCode() == hashCode())
+ && (subtype.getLocale().equals(getLocale()))
+ && (subtype.getLanguageTag().equals(getLanguageTag()))
+ && (subtype.getMode().equals(getMode()))
+ && (subtype.getExtraValue().equals(getExtraValue()))
+ && (subtype.isAuxiliary() == isAuxiliary())
+ && (subtype.overridesImplicitlyEnabledSubtype()
+ == overridesImplicitlyEnabledSubtype())
+ && (subtype.isAsciiCapable() == isAsciiCapable());
+ }
+ return false;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeInt(mSubtypeNameResId);
+ dest.writeInt(mSubtypeIconResId);
+ dest.writeString(mSubtypeLocale);
+ dest.writeString(mSubtypeLanguageTag);
+ dest.writeString(mSubtypeMode);
+ dest.writeString(mSubtypeExtraValue);
+ dest.writeInt(mIsAuxiliary ? 1 : 0);
+ dest.writeInt(mOverridesImplicitlyEnabledSubtype ? 1 : 0);
+ dest.writeInt(mSubtypeHashCode);
+ dest.writeInt(mSubtypeId);
+ dest.writeInt(mIsAsciiCapable ? 1 : 0);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<InputMethodSubtype> CREATOR
+ = new Parcelable.Creator<InputMethodSubtype>() {
+ @Override
+ public InputMethodSubtype createFromParcel(Parcel source) {
+ return new InputMethodSubtype(source);
+ }
+
+ @Override
+ public InputMethodSubtype[] newArray(int size) {
+ return new InputMethodSubtype[size];
+ }
+ };
+
+ private static int hashCodeInternal(String locale, String mode, String extraValue,
+ boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype,
+ boolean isAsciiCapable) {
+ // CAVEAT: Must revisit how to compute needsToCalculateCompatibleHashCode when a new
+ // attribute is added in order to avoid enabled subtypes being unexpectedly disabled.
+ final boolean needsToCalculateCompatibleHashCode = !isAsciiCapable;
+ if (needsToCalculateCompatibleHashCode) {
+ return Arrays.hashCode(new Object[] {locale, mode, extraValue, isAuxiliary,
+ overridesImplicitlyEnabledSubtype});
+ }
+ return Arrays.hashCode(new Object[] {locale, mode, extraValue, isAuxiliary,
+ overridesImplicitlyEnabledSubtype, isAsciiCapable});
+ }
+
+ /**
+ * Sort the list of InputMethodSubtype
+ * @param context Context will be used for getting localized strings from IME
+ * @param flags Flags for the sort order
+ * @param imi InputMethodInfo of which subtypes are subject to be sorted
+ * @param subtypeList List of InputMethodSubtype which will be sorted
+ * @return Sorted list of subtypes
+ * @hide
+ */
+ public static List<InputMethodSubtype> sort(Context context, int flags, InputMethodInfo imi,
+ List<InputMethodSubtype> subtypeList) {
+ if (imi == null) return subtypeList;
+ final HashSet<InputMethodSubtype> inputSubtypesSet = new HashSet<InputMethodSubtype>(
+ subtypeList);
+ final ArrayList<InputMethodSubtype> sortedList = new ArrayList<InputMethodSubtype>();
+ int N = imi.getSubtypeCount();
+ for (int i = 0; i < N; ++i) {
+ InputMethodSubtype subtype = imi.getSubtypeAt(i);
+ if (inputSubtypesSet.contains(subtype)) {
+ sortedList.add(subtype);
+ inputSubtypesSet.remove(subtype);
+ }
+ }
+ // If subtypes in inputSubtypesSet remain, that means these subtypes are not
+ // contained in imi, so the remaining subtypes will be appended.
+ for (InputMethodSubtype subtype: inputSubtypesSet) {
+ sortedList.add(subtype);
+ }
+ return sortedList;
+ }
+}
\ No newline at end of file
diff --git a/android/view/inputmethod/InputMethodSubtypeArray.java b/android/view/inputmethod/InputMethodSubtypeArray.java
new file mode 100644
index 0000000..50e95c8
--- /dev/null
+++ b/android/view/inputmethod/InputMethodSubtypeArray.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2007-2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.view.inputmethod;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.util.Slog;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * An array-like container that stores multiple instances of {@link InputMethodSubtype}.
+ *
+ * <p>This container is designed to reduce the risk of {@link TransactionTooLargeException}
+ * when one or more instancess of {@link InputMethodInfo} are transferred through IPC.
+ * Basically this class does following three tasks.</p>
+ * <ul>
+ * <li>Applying compression for the marshalled data</li>
+ * <li>Lazily unmarshalling objects</li>
+ * <li>Caching the marshalled data when appropriate</li>
+ * </ul>
+ *
+ * @hide
+ */
+public class InputMethodSubtypeArray {
+ private final static String TAG = "InputMethodSubtypeArray";
+
+ /**
+ * Create a new instance of {@link InputMethodSubtypeArray} from an existing list of
+ * {@link InputMethodSubtype}.
+ *
+ * @param subtypes A list of {@link InputMethodSubtype} from which
+ * {@link InputMethodSubtypeArray} will be created.
+ */
+ @UnsupportedAppUsage
+ public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) {
+ if (subtypes == null) {
+ mCount = 0;
+ return;
+ }
+ mCount = subtypes.size();
+ mInstance = subtypes.toArray(new InputMethodSubtype[mCount]);
+ }
+
+ /**
+ * Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel}
+ * object.
+ *
+ * @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be
+ * unmarshalled.
+ */
+ public InputMethodSubtypeArray(final Parcel source) {
+ mCount = source.readInt();
+ if (mCount > 0) {
+ mDecompressedSize = source.readInt();
+ mCompressedData = source.createByteArray();
+ }
+ }
+
+ /**
+ * Marshall the instance into a given {@link Parcel} object.
+ *
+ * <p>This methods may take a bit additional time to compress data lazily when called
+ * first time.</p>
+ *
+ * @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be
+ * marshalled.
+ */
+ public void writeToParcel(final Parcel dest) {
+ if (mCount == 0) {
+ dest.writeInt(mCount);
+ return;
+ }
+
+ byte[] compressedData = mCompressedData;
+ int decompressedSize = mDecompressedSize;
+ if (compressedData == null && decompressedSize == 0) {
+ synchronized (mLockObject) {
+ compressedData = mCompressedData;
+ decompressedSize = mDecompressedSize;
+ if (compressedData == null && decompressedSize == 0) {
+ final byte[] decompressedData = marshall(mInstance);
+ compressedData = compress(decompressedData);
+ if (compressedData == null) {
+ decompressedSize = -1;
+ Slog.i(TAG, "Failed to compress data.");
+ } else {
+ decompressedSize = decompressedData.length;
+ }
+ mDecompressedSize = decompressedSize;
+ mCompressedData = compressedData;
+ }
+ }
+ }
+
+ if (compressedData != null && decompressedSize > 0) {
+ dest.writeInt(mCount);
+ dest.writeInt(decompressedSize);
+ dest.writeByteArray(compressedData);
+ } else {
+ Slog.i(TAG, "Unexpected state. Behaving as an empty array.");
+ dest.writeInt(0);
+ }
+ }
+
+ /**
+ * Return {@link InputMethodSubtype} specified with the given index.
+ *
+ * <p>This methods may take a bit additional time to decompress data lazily when called
+ * first time.</p>
+ *
+ * @param index The index of {@link InputMethodSubtype}.
+ */
+ public InputMethodSubtype get(final int index) {
+ if (index < 0 || mCount <= index) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ InputMethodSubtype[] instance = mInstance;
+ if (instance == null) {
+ synchronized (mLockObject) {
+ instance = mInstance;
+ if (instance == null) {
+ final byte[] decompressedData =
+ decompress(mCompressedData, mDecompressedSize);
+ // Clear the compressed data until {@link #getMarshalled()} is called.
+ mCompressedData = null;
+ mDecompressedSize = 0;
+ if (decompressedData != null) {
+ instance = unmarshall(decompressedData);
+ } else {
+ Slog.e(TAG, "Failed to decompress data. Returns null as fallback.");
+ instance = new InputMethodSubtype[mCount];
+ }
+ mInstance = instance;
+ }
+ }
+ }
+ return instance[index];
+ }
+
+ /**
+ * Return the number of {@link InputMethodSubtype} objects.
+ */
+ public int getCount() {
+ return mCount;
+ }
+
+ private final Object mLockObject = new Object();
+ private final int mCount;
+
+ private volatile InputMethodSubtype[] mInstance;
+ private volatile byte[] mCompressedData;
+ private volatile int mDecompressedSize;
+
+ private static byte[] marshall(final InputMethodSubtype[] array) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ parcel.writeTypedArray(array, 0);
+ return parcel.marshall();
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ parcel = null;
+ }
+ }
+ }
+
+ private static InputMethodSubtype[] unmarshall(final byte[] data) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ parcel.unmarshall(data, 0, data.length);
+ parcel.setDataPosition(0);
+ return parcel.createTypedArray(InputMethodSubtype.CREATOR);
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ parcel = null;
+ }
+ }
+ }
+
+ private static byte[] compress(final byte[] data) {
+ try (final ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
+ final GZIPOutputStream zipper = new GZIPOutputStream(resultStream)) {
+ zipper.write(data);
+ zipper.finish();
+ return resultStream.toByteArray();
+ } catch(Exception e) {
+ Slog.e(TAG, "Failed to compress the data.", e);
+ return null;
+ }
+ }
+
+ private static byte[] decompress(final byte[] data, final int expectedSize) {
+ try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
+ final GZIPInputStream unzipper = new GZIPInputStream(inputStream)) {
+ final byte [] result = new byte[expectedSize];
+ int totalReadBytes = 0;
+ while (totalReadBytes < result.length) {
+ final int restBytes = result.length - totalReadBytes;
+ final int readBytes = unzipper.read(result, totalReadBytes, restBytes);
+ if (readBytes < 0) {
+ break;
+ }
+ totalReadBytes += readBytes;
+ }
+ if (expectedSize != totalReadBytes) {
+ return null;
+ }
+ return result;
+ } catch(Exception e) {
+ Slog.e(TAG, "Failed to decompress the data.", e);
+ return null;
+ }
+ }
+}
diff --git a/android/view/inputmethod/SparseRectFArray.java b/android/view/inputmethod/SparseRectFArray.java
new file mode 100644
index 0000000..07d0b06
--- /dev/null
+++ b/android/view/inputmethod/SparseRectFArray.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.view.inputmethod;
+
+import android.annotation.Nullable;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * An implementation of SparseArray specialized for {@link android.graphics.RectF}.
+ * <p>
+ * As this is a sparse array, it represents an array of {@link RectF} most of which are null. This
+ * class could be in some other packages like android.graphics or android.util but currently
+ * belong to android.view.inputmethod because this class is hidden and used only in input method
+ * framework.
+ * </p>
+ * @hide
+ */
+public final class SparseRectFArray implements Parcelable {
+ /**
+ * The keys, in ascending order, of those {@link RectF} that are not null. For example,
+ * {@code [null, null, null, Rect1, null, Rect2]} would be represented by {@code [3,5]}.
+ * @see #mCoordinates
+ */
+ private final int[] mKeys;
+
+ /**
+ * Stores coordinates of the rectangles, in the order of
+ * {@code rects[mKeys[0]].left}, {@code rects[mKeys[0]].top},
+ * {@code rects[mKeys[0]].right}, {@code rects[mKeys[0]].bottom},
+ * {@code rects[mKeys[1]].left}, {@code rects[mKeys[1]].top},
+ * {@code rects[mKeys[1]].right}, {@code rects[mKeys[1]].bottom},
+ * {@code rects[mKeys[2]].left}, {@code rects[mKeys[2]].top}, ....
+ */
+ private final float[] mCoordinates;
+
+ /**
+ * Stores visibility information.
+ */
+ private final int[] mFlagsArray;
+
+ public SparseRectFArray(final Parcel source) {
+ mKeys = source.createIntArray();
+ mCoordinates = source.createFloatArray();
+ mFlagsArray = source.createIntArray();
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(mKeys);
+ dest.writeFloatArray(mCoordinates);
+ dest.writeIntArray(mFlagsArray);
+ }
+
+ @Override
+ public int hashCode() {
+ // TODO: Improve the hash function.
+ if (mKeys == null || mKeys.length == 0) {
+ return 0;
+ }
+ int hash = mKeys.length;
+ // For performance reasons, only the first rectangle is used for the hash code now.
+ for (int i = 0; i < 4; i++) {
+ hash *= 31;
+ hash += mCoordinates[i];
+ }
+ hash *= 31;
+ hash += mFlagsArray[0];
+ return hash;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj){
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SparseRectFArray)) {
+ return false;
+ }
+ final SparseRectFArray that = (SparseRectFArray) obj;
+
+ return Arrays.equals(mKeys, that.mKeys) && Arrays.equals(mCoordinates, that.mCoordinates)
+ && Arrays.equals(mFlagsArray, that.mFlagsArray);
+ }
+
+ @Override
+ public String toString() {
+ if (mKeys == null || mCoordinates == null || mFlagsArray == null) {
+ return "SparseRectFArray{}";
+ }
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SparseRectFArray{");
+ for (int i = 0; i < mKeys.length; i++) {
+ if (i != 0) {
+ sb.append(", ");
+ }
+ final int baseIndex = i * 4;
+ sb.append(mKeys[i]);
+ sb.append(":[");
+ sb.append(mCoordinates[baseIndex + 0]);
+ sb.append(",");
+ sb.append(mCoordinates[baseIndex + 1]);
+ sb.append("],[");
+ sb.append(mCoordinates[baseIndex + 2]);
+ sb.append(",");
+ sb.append(mCoordinates[baseIndex + 3]);
+ sb.append("]:flagsArray=");
+ sb.append(mFlagsArray[i]);
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Builder for {@link SparseRectFArray}. This class is not designed to be thread-safe.
+ * @hide
+ */
+ public static final class SparseRectFArrayBuilder {
+ /**
+ * Throws {@link IllegalArgumentException} to make sure that this class is correctly used.
+ * @param key key to be checked.
+ */
+ private void checkIndex(final int key) {
+ if (mCount == 0) {
+ return;
+ }
+ if (mKeys[mCount - 1] >= key) {
+ throw new IllegalArgumentException("key must be greater than all existing keys.");
+ }
+ }
+
+ /**
+ * Extends the internal array if necessary.
+ */
+ private void ensureBufferSize() {
+ if (mKeys == null) {
+ mKeys = new int[INITIAL_SIZE];
+ }
+ if (mCoordinates == null) {
+ mCoordinates = new float[INITIAL_SIZE * 4];
+ }
+ if (mFlagsArray == null) {
+ mFlagsArray = new int[INITIAL_SIZE];
+ }
+ final int requiredIndexArraySize = mCount + 1;
+ if (mKeys.length <= requiredIndexArraySize) {
+ final int[] newArray = new int[requiredIndexArraySize * 2];
+ System.arraycopy(mKeys, 0, newArray, 0, mCount);
+ mKeys = newArray;
+ }
+ final int requiredCoordinatesArraySize = (mCount + 1) * 4;
+ if (mCoordinates.length <= requiredCoordinatesArraySize) {
+ final float[] newArray = new float[requiredCoordinatesArraySize * 2];
+ System.arraycopy(mCoordinates, 0, newArray, 0, mCount * 4);
+ mCoordinates = newArray;
+ }
+ final int requiredFlagsArraySize = requiredIndexArraySize;
+ if (mFlagsArray.length <= requiredFlagsArraySize) {
+ final int[] newArray = new int[requiredFlagsArraySize * 2];
+ System.arraycopy(mFlagsArray, 0, newArray, 0, mCount);
+ mFlagsArray = newArray;
+ }
+ }
+
+ /**
+ * Puts the rectangle with an integer key.
+ * @param key the key to be associated with the rectangle. It must be greater than all
+ * existing keys that have been previously specified.
+ * @param left left of the rectangle.
+ * @param top top of the rectangle.
+ * @param right right of the rectangle.
+ * @param bottom bottom of the rectangle.
+ * @param flags an arbitrary integer value to be associated with this rectangle.
+ * @return the receiver object itself for chaining method calls.
+ * @throws IllegalArgumentException If the index is not greater than all of existing keys.
+ */
+ public SparseRectFArrayBuilder append(final int key,
+ final float left, final float top, final float right, final float bottom,
+ final int flags) {
+ checkIndex(key);
+ ensureBufferSize();
+ final int baseCoordinatesIndex = mCount * 4;
+ mCoordinates[baseCoordinatesIndex + 0] = left;
+ mCoordinates[baseCoordinatesIndex + 1] = top;
+ mCoordinates[baseCoordinatesIndex + 2] = right;
+ mCoordinates[baseCoordinatesIndex + 3] = bottom;
+ final int flagsIndex = mCount;
+ mFlagsArray[flagsIndex] = flags;
+ mKeys[mCount] = key;
+ ++mCount;
+ return this;
+ }
+ private int mCount = 0;
+ private int[] mKeys = null;
+ private float[] mCoordinates = null;
+ private int[] mFlagsArray = null;
+ private static int INITIAL_SIZE = 16;
+
+ public boolean isEmpty() {
+ return mCount <= 0;
+ }
+
+ /**
+ * @return {@link SparseRectFArray} using parameters in this {@link SparseRectFArray}.
+ */
+ public SparseRectFArray build() {
+ return new SparseRectFArray(this);
+ }
+
+ public void reset() {
+ if (mCount == 0) {
+ mKeys = null;
+ mCoordinates = null;
+ mFlagsArray = null;
+ }
+ mCount = 0;
+ }
+ }
+
+ private SparseRectFArray(final SparseRectFArrayBuilder builder) {
+ if (builder.mCount == 0) {
+ mKeys = null;
+ mCoordinates = null;
+ mFlagsArray = null;
+ } else {
+ mKeys = new int[builder.mCount];
+ mCoordinates = new float[builder.mCount * 4];
+ mFlagsArray = new int[builder.mCount];
+ System.arraycopy(builder.mKeys, 0, mKeys, 0, builder.mCount);
+ System.arraycopy(builder.mCoordinates, 0, mCoordinates, 0, builder.mCount * 4);
+ System.arraycopy(builder.mFlagsArray, 0, mFlagsArray, 0, builder.mCount);
+ }
+ }
+
+ public RectF get(final int index) {
+ if (mKeys == null) {
+ return null;
+ }
+ if (index < 0) {
+ return null;
+ }
+ final int arrayIndex = Arrays.binarySearch(mKeys, index);
+ if (arrayIndex < 0) {
+ return null;
+ }
+ final int baseCoordIndex = arrayIndex * 4;
+ return new RectF(mCoordinates[baseCoordIndex],
+ mCoordinates[baseCoordIndex + 1],
+ mCoordinates[baseCoordIndex + 2],
+ mCoordinates[baseCoordIndex + 3]);
+ }
+
+ public int getFlags(final int index, final int valueIfKeyNotFound) {
+ if (mKeys == null) {
+ return valueIfKeyNotFound;
+ }
+ if (index < 0) {
+ return valueIfKeyNotFound;
+ }
+ final int arrayIndex = Arrays.binarySearch(mKeys, index);
+ if (arrayIndex < 0) {
+ return valueIfKeyNotFound;
+ }
+ return mFlagsArray[arrayIndex];
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<SparseRectFArray> CREATOR =
+ new Parcelable.Creator<SparseRectFArray>() {
+ @Override
+ public SparseRectFArray createFromParcel(Parcel source) {
+ return new SparseRectFArray(source);
+ }
+ @Override
+ public SparseRectFArray[] newArray(int size) {
+ return new SparseRectFArray[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
+
diff --git a/android/view/inputmethod/SurroundingText.java b/android/view/inputmethod/SurroundingText.java
new file mode 100644
index 0000000..c85a18a
--- /dev/null
+++ b/android/view/inputmethod/SurroundingText.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Information about the surrounding text around the cursor for use by an input method.
+ *
+ * <p>This contains information about the text and the selection relative to the text. </p>
+ */
+public final class SurroundingText implements Parcelable {
+ /**
+ * The surrounding text around the cursor.
+ */
+ @NonNull
+ private final CharSequence mText;
+
+ /**
+ * The text offset of the start of the selection in the surrounding text.
+ *
+ * <p>This needs to be the position relative to the {@link #mText} instead of the real position
+ * in the editor.</p>
+ */
+ @IntRange(from = 0)
+ private final int mSelectionStart;
+
+ /**
+ * The text offset of the end of the selection in the surrounding text.
+ *
+ * <p>This needs to be the position relative to the {@link #mText} instead of the real position
+ * in the editor.</p>
+ */
+ @IntRange(from = 0)
+ private final int mSelectionEnd;
+
+ /**
+ * The text offset between the start of the editor's text and the start of the surrounding text.
+ *
+ * <p>-1 indicates the offset information is unknown.</p>
+ */
+ @IntRange(from = -1)
+ private final int mOffset;
+
+ /**
+ * Constructor.
+ *
+ * @param text The surrounding text.
+ * @param selectionStart The text offset of the start of the selection in the surrounding text.
+ * Reversed selection is allowed.
+ * @param selectionEnd The text offset of the end of the selection in the surrounding text.
+ * Reversed selection is allowed.
+ * @param offset The text offset between the start of the editor's text and the start of the
+ * surrounding text. -1 indicates the offset is unknown.
+ */
+ public SurroundingText(@NonNull final CharSequence text,
+ @IntRange(from = 0) int selectionStart, @IntRange(from = 0) int selectionEnd,
+ @IntRange(from = -1) int offset) {
+ mText = copyWithParcelableSpans(text);
+ mSelectionStart = selectionStart;
+ mSelectionEnd = selectionEnd;
+ mOffset = offset;
+ }
+
+ /**
+ * Returns the surrounding text around the cursor.
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the text offset of the start of the selection in the surrounding text.
+ *
+ * <p>A selection is the current range of the text that is selected by the user, or the current
+ * position of the cursor. A cursor is a selection where the start and end are at the same
+ * offset.<p>
+ */
+ @IntRange(from = 0)
+ public int getSelectionStart() {
+ return mSelectionStart;
+ }
+
+ /**
+ * Returns the text offset of the end of the selection in the surrounding text.
+ *
+ * <p>A selection is the current range of the text that is selected by the user, or the current
+ * position of the cursor. A cursor is a selection where the start and end are at the same
+ * offset.<p>
+ */
+ @IntRange(from = 0)
+ public int getSelectionEnd() {
+ return mSelectionEnd;
+ }
+
+ /**
+ * Returns text offset between the start of the editor's text and the start of the surrounding
+ * text.
+ *
+ * <p>-1 indicates the offset information is unknown.</p>
+ */
+ @IntRange(from = -1)
+ public int getOffset() {
+ return mOffset;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ TextUtils.writeToParcel(mText, out, flags);
+ out.writeInt(mSelectionStart);
+ out.writeInt(mSelectionEnd);
+ out.writeInt(mOffset);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<SurroundingText> CREATOR =
+ new Parcelable.Creator<SurroundingText>() {
+ @NonNull
+ public SurroundingText createFromParcel(Parcel in) {
+ final CharSequence text =
+ TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ final int selectionHead = in.readInt();
+ final int selectionEnd = in.readInt();
+ final int offset = in.readInt();
+ return new SurroundingText(
+ text == null ? "" : text, selectionHead, selectionEnd, offset);
+ }
+
+ @NonNull
+ public SurroundingText[] newArray(int size) {
+ return new SurroundingText[size];
+ }
+ };
+
+ /**
+ * Create a copy of the given {@link CharSequence} object, with completely copy
+ * {@link ParcelableSpan} instances.
+ *
+ * @param source the original {@link CharSequence} to be copied.
+ * @return the copied {@link CharSequence}. {@code null} if {@code source} is {@code null}.
+ */
+ @Nullable
+ private static CharSequence copyWithParcelableSpans(@Nullable CharSequence source) {
+ if (source == null) {
+ return null;
+ }
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ TextUtils.writeToParcel(source, parcel, /* parcelableFlags= */ 0);
+ parcel.setDataPosition(0);
+ return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ }
+ }
+ }
+}
diff --git a/android/view/inspector/InspectableProperty.java b/android/view/inspector/InspectableProperty.java
new file mode 100644
index 0000000..03f1ec5
--- /dev/null
+++ b/android/view/inspector/InspectableProperty.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.TestApi;
+import android.content.res.Resources;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a getter of a property on an inspectable node.
+ *
+ * This annotation is inherited by default. If a child class doesn't add it to a getter, but a
+ * parent class does, the property will be inspected, even if the child overrides the definition
+ * of the getter. If a child class defines a property of the same name of a property on the parent
+ * but on a different getter, the inspector will use the child's getter when inspecting instances
+ * of the child, and the parent's otherwise.
+ *
+ * @see InspectionCompanion#mapProperties(PropertyMapper)
+ * @see InspectionCompanion#readProperties(Object, PropertyReader)
+ * @hide
+ */
+@Target({METHOD, FIELD})
+@Retention(SOURCE)
+@TestApi
+public @interface InspectableProperty {
+ /**
+ * The name of the property.
+ *
+ * If left empty (the default), the property name will be inferred from the name of the getter
+ * method.
+ *
+ * @return The name of the property.
+ */
+ String name() default "";
+
+ /**
+ * If the property is inflated from XML, the resource ID of its XML attribute.
+ *
+ * If left as {ID_NULL}, and {@link #hasAttributeId()} is true, the attribute ID will be
+ * inferred from {@link #name()}.
+ *
+ * @return The attribute ID of the property or {@link Resources#ID_NULL}
+ */
+ int attributeId() default Resources.ID_NULL;
+
+ /**
+ * If this property has an attribute ID.
+ *
+ * Set to false if the annotated property does not have an attribute ID, that is, it is not
+ * inflated from an XML attribute. This will prevent the automatic inference of the attribute
+ * ID if {@link #attributeId()} is set to {@link Resources#ID_NULL}.
+ *
+ * @return Whether to infer an attribute ID if not supplied
+ */
+ boolean hasAttributeId() default true;
+
+ /**
+ * Specify how to interpret a value type packed into a primitive integer.
+ *
+ * @return A {@link ValueType}
+ */
+ ValueType valueType() default ValueType.INFERRED;
+
+ /**
+ * For enumerations packed into primitive {int} properties, map the values to string names.
+ *
+ * Note that {@link #enumMapping()} cannot be used simultaneously with {@link #flagMapping()}.
+ *
+ * @return An array of {@link EnumEntry}, empty if not applicable
+ * @see android.annotation.IntDef
+ */
+ EnumEntry[] enumMapping() default {};
+
+ /**
+ * For flags packed into primitive {int} properties, model the string names of the flags.
+ *
+ * Note that {@link #flagMapping()} cannot be used simultaneously with {@link #enumMapping()}.
+ *
+ * @return An array of {@link FlagEntry}, empty if not applicable
+ * @see android.annotation.IntDef
+ * @see IntFlagMapping
+ */
+ FlagEntry[] flagMapping() default {};
+
+
+ /**
+ * One entry in an enumeration packed into a primitive {int}.
+ *
+ * @see IntEnumMapping
+ * @hide
+ */
+ @Target({TYPE})
+ @Retention(SOURCE)
+ @TestApi
+ @interface EnumEntry {
+ /**
+ * The string name of this enumeration value.
+ *
+ * @return A string name
+ */
+ String name();
+
+ /**
+ * The integer value of this enumeration value.
+ *
+ * @return An integer value
+ */
+ int value();
+ }
+
+ /**
+ * One flag value of many that may be packed into a primitive {int}.
+ *
+ * @see IntFlagMapping
+ * @hide
+ */
+ @Target({TYPE})
+ @Retention(SOURCE)
+ @TestApi
+ @interface FlagEntry {
+ /**
+ * The string name of this flag.
+ *
+ * @return A string name
+ */
+ String name();
+
+ /**
+ * A target value that the property's value must equal after masking.
+ *
+ * If a mask is not supplied (i.e., {@link #mask()} is 0), the target will be reused as the
+ * mask. This handles the common case where no flags mutually exclude each other.
+ *
+ * @return The target value to compare against
+ */
+ int target();
+
+ /**
+ * A mask that the property will be bitwise anded with before comparing to the target.
+ *
+ * If set to 0 (the default), the value of {@link #target()} will be used as a mask. Zero
+ * was chosen as the default since bitwise and with zero is always zero.
+ *
+ * @return A mask, or 0 to use the target as a mask
+ */
+ int mask() default 0;
+ }
+
+ /**
+ * The type of value packed into a primitive {int}.
+ *
+ * @hide
+ */
+ @TestApi
+ enum ValueType {
+ /**
+ * No special handling, property is considered to be a numeric value.
+ *
+ * @hide
+ */
+ @TestApi
+ NONE,
+
+ /**
+ * The default the annotation processor infers the value type from context.
+ *
+ * @hide
+ */
+ @TestApi
+ INFERRED,
+
+ /**
+ * Value packs an enumeration.
+ *
+ * This is inferred if {@link #enumMapping()} is specified.
+ *
+ * @see EnumEntry
+ * @hide
+ */
+ @TestApi
+ INT_ENUM,
+
+ /**
+ * Value packs flags, of which many may be enabled at once.
+ *
+ * This is inferred if {@link #flagMapping()} is specified.
+ *
+ * @see FlagEntry
+ * @hide
+ */
+ @TestApi
+ INT_FLAG,
+
+ /**
+ * Value packs color information.
+ *
+ * This is inferred from {@link android.annotation.ColorInt}, or
+ * {@link android.annotation.ColorLong} on the getter method.
+ *
+ * @see android.graphics.Color
+ * @hide
+ */
+ @TestApi
+ COLOR,
+
+ /**
+ * Value packs gravity information.
+ *
+ * This type is not inferred, and is non-trivial to represent using {@link FlagEntry}.
+ *
+ * @see android.view.Gravity
+ * @hide
+ */
+ @TestApi
+ GRAVITY,
+
+ /**
+ * Value is a resource ID
+ *
+ * This type is inferred from the presence of a resource ID annotation such as
+ * {@link android.annotation.AnyRes}.
+ *
+ * @hide
+ */
+ @TestApi
+ RESOURCE_ID
+ }
+}
diff --git a/android/view/inspector/InspectionCompanion.java b/android/view/inspector/InspectionCompanion.java
new file mode 100644
index 0000000..a633a58
--- /dev/null
+++ b/android/view/inspector/InspectionCompanion.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.NonNull;
+
+/**
+ * An interface for companion objects used to inspect views.
+ *
+ * Inspection companions only need to handle the properties and node name of the specific class
+ * they are defined for, not anything from a parent class. At runtime, the inspector instantiates
+ * one instance of each inspection companion, and handles visiting them in the correct inheritance
+ * order for each type it inspects.
+ *
+ * Properties are read from the top of the type tree to the bottom, so that classes that override
+ * a property in their parent class can overwrite it in the reader. In general, properties will
+ * cleanly inherit through their getters, and the inspector runtime will read the properties of a
+ * parent class via the parent's inspection companion, and the child companion will only read
+ * properties added or changed since the parent was defined.
+ *
+ * @param <T> The type of inspectable this is the companion to
+ */
+public interface InspectionCompanion<T> {
+ /**
+ * Map the string names of the properties this companion knows about to integer IDs.
+ *
+ * Each companion is responsible for storing the integer IDs of all its properties. This is the
+ * only method that is allowed to modify the stored IDs.
+ *
+ * Calling {@link #readProperties(T, PropertyReader)} before calling this results in
+ * undefined behavior.
+ *
+ * @param propertyMapper A {@link PropertyMapper} maps string names to IDs.
+ */
+ void mapProperties(@NonNull PropertyMapper propertyMapper);
+
+ /**
+ * Read the values of an instance of this companion's type into a {@link PropertyReader}.
+ *
+ * This method needs to return the property IDs stored by
+ * {@link #mapProperties(PropertyMapper)}. Implementations should track if their properties
+ * have been mapped and throw a {@link UninitializedPropertyMapException} if this method is
+ * called before {mapProperties}.
+ *
+ * @param inspectable A object of type {T} to read the properties of.
+ * @param propertyReader An object which receives the property IDs and values.
+ */
+ void readProperties(@NonNull T inspectable, @NonNull PropertyReader propertyReader);
+
+ /**
+ * Thrown by {@link #readProperties(Object, PropertyReader)} if called before
+ * {@link #mapProperties(PropertyMapper)}.
+ */
+ class UninitializedPropertyMapException extends RuntimeException {
+ public UninitializedPropertyMapException() {
+ super("Unable to read properties of an inspectable before mapping their IDs.");
+ }
+ }
+}
diff --git a/android/view/inspector/InspectionCompanionProvider.java b/android/view/inspector/InspectionCompanionProvider.java
new file mode 100644
index 0000000..c08f49c
--- /dev/null
+++ b/android/view/inspector/InspectionCompanionProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * An interface for services that can provide inspection companions for a class.
+ */
+public interface InspectionCompanionProvider {
+ /**
+ * Provide an {@link InspectionCompanion} for the supplied class.
+ *
+ * Implementing classes must not cache companion instances, and should instantiate a new one
+ * for each request.
+ *
+ * @param cls A {@link Class} representing the inspectable type
+ * @param <T> The type to find the companion for
+ * @return The inspection companion for the supplied type
+ */
+ @Nullable
+ <T> InspectionCompanion<T> provide(@NonNull Class<T> cls);
+}
diff --git a/android/view/inspector/IntFlagMapping.java b/android/view/inspector/IntFlagMapping.java
new file mode 100644
index 0000000..f501329
--- /dev/null
+++ b/android/view/inspector/IntFlagMapping.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Maps the values of an {@code int} property to sets of string for properties that encode flags.
+ *
+ * An {@link InspectionCompanion} may provide an instance of this class to a {@link PropertyMapper}
+ * for flag values packed into primitive {@code int} properties.
+ *
+ * Each flag has a mask and a target value, for non-exclusive flags, the target can also be used as
+ * the mask. A given integer value is compared against each flag to find what flags are active for
+ * it by bitwise anding it with the mask and comparing the result against the target, that is,
+ * {@code (value & mask) == target}.
+ *
+ * @see PropertyMapper#mapIntFlag(String, int, java.util.function.IntFunction)
+ */
+public final class IntFlagMapping {
+ private final List<Flag> mFlags = new ArrayList<>();
+
+ /**
+ * Get a set of the names of enabled flags for a given property value.
+ *
+ * @param value The value of the property
+ * @return The names of the enabled flags, empty if no flags enabled
+ */
+ @NonNull
+ public Set<String> get(int value) {
+ final Set<String> enabledFlagNames = new HashSet<>(mFlags.size());
+
+ for (Flag flag : mFlags) {
+ if (flag.isEnabledFor(value)) {
+ enabledFlagNames.add(flag.mName);
+ }
+ }
+
+ return Collections.unmodifiableSet(enabledFlagNames);
+ }
+
+ /**
+ * Add a mutually exclusive flag to the map.
+ *
+ * @param mask The bit mask to compare to and with a value
+ * @param target The target value to compare the masked value with
+ * @param name The name of the flag to include if enabled
+ */
+ public void add(int mask, int target, @NonNull String name) {
+ mFlags.add(new Flag(mask, target, name));
+ }
+
+ /**
+ * Inner class that holds the name, mask, and target value of a flag
+ */
+ private static final class Flag {
+ @NonNull private final String mName;
+ private final int mTarget;
+ private final int mMask;
+
+ private Flag(int mask, int target, @NonNull String name) {
+ mTarget = target;
+ mMask = mask;
+ mName = Objects.requireNonNull(name);
+ }
+
+ /**
+ * Compare the supplied property value against the mask and target.
+ *
+ * @param value The value to check
+ * @return True if this flag is enabled
+ */
+ private boolean isEnabledFor(int value) {
+ return (value & mMask) == mTarget;
+ }
+ }
+}
diff --git a/android/view/inspector/PropertyMapper.java b/android/view/inspector/PropertyMapper.java
new file mode 100644
index 0000000..cbc690e
--- /dev/null
+++ b/android/view/inspector/PropertyMapper.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+
+import java.util.Set;
+import java.util.function.IntFunction;
+
+/**
+ * An interface for mapping the string names of inspectable properties to integer identifiers.
+ *
+ * This interface is consumed by {@link InspectionCompanion#mapProperties(PropertyMapper)}.
+ *
+ * Mapping properties to IDs enables quick comparisons against shadow copies of inspectable
+ * objects without performing a large number of string comparisons.
+ *
+ * @see InspectionCompanion#mapProperties(PropertyMapper)
+ */
+public interface PropertyMapper {
+ /**
+ * Map a string name to an integer ID for a primitive boolean property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapBoolean(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive byte property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapByte(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive char property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapChar(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive double property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapDouble(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive float property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapFloat(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive int property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapInt(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive long property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapLong(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a primitive short property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapShort(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for an object property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapObject(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a color property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ * @see android.graphics.Color
+ */
+ int mapColor(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a gravity property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ * @see android.view.Gravity
+ */
+ int mapGravity(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for an enumeration packed into an int property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @param mapping A mapping from int to String
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapIntEnum(
+ @NonNull String name,
+ @AttrRes int attributeId,
+ @NonNull IntFunction<String> mapping);
+
+ /**
+ * Map a string name to an integer ID for an attribute that contains resource IDs.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapResourceId(@NonNull String name, @AttrRes int attributeId);
+
+ /**
+ * Map a string name to an integer ID for a flag set packed into an int property.
+ *
+ * @param name The name of the property
+ * @param attributeId If the property is from an XML attribute, the resource ID of the property
+ * @param mapping A mapping from int to a set of strings
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapIntFlag(
+ @NonNull String name,
+ @AttrRes int attributeId,
+ @NonNull IntFunction<Set<String>> mapping);
+ /**
+ * Thrown from a map method if a property name is already mapped as different type.
+ */
+ class PropertyConflictException extends RuntimeException {
+ public PropertyConflictException(
+ @NonNull String name,
+ @NonNull String newPropertyType,
+ @NonNull String existingPropertyType) {
+ super(String.format(
+ "Attempted to map property \"%s\" as type %s, but it is already mapped as %s.",
+ name,
+ newPropertyType,
+ existingPropertyType
+ ));
+ }
+ }
+}
diff --git a/android/view/inspector/PropertyReader.java b/android/view/inspector/PropertyReader.java
new file mode 100644
index 0000000..5be0e3f
--- /dev/null
+++ b/android/view/inspector/PropertyReader.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.AnyRes;
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Color;
+
+/**
+ * An interface for reading the properties of an inspectable object.
+ *
+ * {@code PropertyReader} is defined as an interface that will be called by
+ * {@link InspectionCompanion#readProperties(Object, PropertyReader)}. This approach allows a
+ * client inspector to read the values of primitive properties without the overhead of
+ * instantiating a class to hold the property values for each inspection pass. If an inspectable
+ * remains unchanged between reading passes, it should be possible for a {@code PropertyReader} to
+ * avoid new allocations for subsequent reading passes.
+ *
+ * It has separate methods for all primitive types to avoid autoboxing overhead if a concrete
+ * implementation is able to work with primitives. Implementations should be prepared to accept
+ * {null} as the value of {@link PropertyReader#readObject(int, Object)}.
+ *
+ * @see InspectionCompanion#readProperties(Object, PropertyReader)
+ */
+public interface PropertyReader {
+ /**
+ * Read a primitive boolean property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code boolean}
+ */
+ void readBoolean(int id, boolean value);
+
+ /**
+ * Read a primitive byte property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code byte}
+ */
+ void readByte(int id, byte value);
+
+ /**
+ * Read a primitive character property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code char}
+ */
+ void readChar(int id, char value);
+
+ /**
+ * Read a read a primitive double property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code double}
+ */
+ void readDouble(int id, double value);
+
+ /**
+ * Read a primitive float property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code float}
+ */
+ void readFloat(int id, float value);
+
+ /**
+ * Read a primitive integer property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as an {@code int}
+ */
+ void readInt(int id, int value);
+
+ /**
+ * Read a primitive long property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code long}
+ */
+ void readLong(int id, long value);
+
+ /**
+ * Read a primitive short property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {@code short}
+ */
+ void readShort(int id, short value);
+
+ /**
+ * Read any object as a property.
+ *
+ * If value is null, the property is marked as empty.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as an object
+ */
+ void readObject(int id, @Nullable Object value);
+
+ /**
+ * Read a color packed into a {@link ColorInt} as a property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a color
+ */
+ void readColor(int id, @ColorInt int value);
+
+ /**
+ * Read a color packed into a {@code ColorLong} as a property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property packed as a {@code ColorLong}. See the
+ * {@link Color} class for details of the packing.
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a color
+ */
+ void readColor(int id, @ColorLong long value);
+
+ /**
+ * Read a {@link Color} object as a property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a color
+ */
+ void readColor(int id, @Nullable Color value);
+
+ /**
+ * Read {@link android.view.Gravity} packed into an primitive {@code int}.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a gravity property
+ */
+ void readGravity(int id, int value);
+
+ /**
+ * Read an enumeration packed into a primitive {@code int}.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as an object
+ */
+ void readIntEnum(int id, int value);
+
+ /**
+ * Read a flag packed into a primitive {@code int}.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as an object
+ */
+ void readIntFlag(int id, int value);
+
+ /**
+ * Read an integer that contains a resource ID.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a resource ID.
+ */
+ void readResourceId(int id, @AnyRes int value);
+
+ /**
+ * Thrown if a client calls a typed read method for a property of a different type.
+ */
+ class PropertyTypeMismatchException extends RuntimeException {
+ public PropertyTypeMismatchException(
+ int id,
+ @NonNull String expectedPropertyType,
+ @NonNull String actualPropertyType,
+ @Nullable String propertyName) {
+ super(formatMessage(id, expectedPropertyType, actualPropertyType, propertyName));
+ }
+
+ public PropertyTypeMismatchException(
+ int id,
+ @NonNull String expectedPropertyType,
+ @NonNull String actualPropertyType) {
+ super(formatMessage(id, expectedPropertyType, actualPropertyType, null));
+ }
+
+ private static @NonNull String formatMessage(
+ int id,
+ @NonNull String expectedPropertyType,
+ @NonNull String actualPropertyType,
+ @Nullable String propertyName) {
+
+ if (propertyName == null) {
+ return String.format(
+ "Attempted to read property with ID 0x%08X as type %s, "
+ + "but the ID is of type %s.",
+ id,
+ expectedPropertyType,
+ actualPropertyType
+ );
+ } else {
+ return String.format(
+ "Attempted to read property \"%s\" with ID 0x%08X as type %s, "
+ + "but the ID is of type %s.",
+ propertyName,
+ id,
+ expectedPropertyType,
+ actualPropertyType
+ );
+ }
+ }
+ }
+}
diff --git a/android/view/inspector/StaticInspectionCompanionProvider.java b/android/view/inspector/StaticInspectionCompanionProvider.java
new file mode 100644
index 0000000..903fc13
--- /dev/null
+++ b/android/view/inspector/StaticInspectionCompanionProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * An inspection companion provider that finds companions as inner classes or generated code.
+ */
+public class StaticInspectionCompanionProvider implements InspectionCompanionProvider {
+ /**
+ * The suffix used for the generated classes and inner classes
+ */
+ private static final String COMPANION_SUFFIX = "$InspectionCompanion";
+
+ @Override
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public <T> InspectionCompanion<T> provide(@NonNull Class<T> cls) {
+ final String companionName = cls.getName() + COMPANION_SUFFIX;
+
+ try {
+ final Class<InspectionCompanion<T>> companionClass =
+ (Class<InspectionCompanion<T>>) cls.getClassLoader().loadClass(companionName);
+
+ if (InspectionCompanion.class.isAssignableFrom(companionClass)) {
+ return companionClass.newInstance();
+ } else {
+ return null;
+ }
+ } catch (ClassNotFoundException e) {
+ return null;
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InstantiationException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof RuntimeException) throw (RuntimeException) cause;
+ if (cause instanceof Error) throw (Error) cause;
+ throw new RuntimeException(cause);
+ }
+ }
+}
diff --git a/android/view/inspector/WindowInspector.java b/android/view/inspector/WindowInspector.java
new file mode 100644
index 0000000..69d004e
--- /dev/null
+++ b/android/view/inspector/WindowInspector.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inspector;
+
+import android.annotation.NonNull;
+import android.view.View;
+import android.view.WindowManagerGlobal;
+
+import java.util.List;
+
+/**
+ * Provides access to window inspection information.
+ */
+public final class WindowInspector {
+ private WindowInspector() {
+ // Non-instantiable.
+ }
+
+ /**
+ * @return the list of all window views attached to the current process
+ */
+ @NonNull
+ public static List<View> getGlobalWindowViews() {
+ return WindowManagerGlobal.getInstance().getWindowViews();
+ }
+}
diff --git a/android/view/math/Math3DHelper.java b/android/view/math/Math3DHelper.java
new file mode 100644
index 0000000..35a1ee1
--- /dev/null
+++ b/android/view/math/Math3DHelper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.math;
+
+public class Math3DHelper {
+
+ private Math3DHelper() { }
+
+ public final static int min(int x1, int x2, int x3) {
+ return (x1 > x2) ? ((x2 > x3) ? x3 : x2) : ((x1 > x3) ? x3 : x1);
+ }
+
+ public final static int max(int x1, int x2, int x3) {
+ return (x1 < x2) ? ((x2 < x3) ? x3 : x2) : ((x1 < x3) ? x3 : x1);
+ }
+
+ /**
+ * @return Rect bound of flattened (ignoring z). LTRB
+ * @param dimension - 2D or 3D
+ */
+ public static float[] flatBound(float[] poly, int dimension) {
+ int polySize = poly.length/dimension;
+ float left = poly[0];
+ float right = poly[0];
+ float top = poly[1];
+ float bottom = poly[1];
+
+ for (int i = 0; i < polySize; i++) {
+ float x = poly[i * dimension + 0];
+ float y = poly[i * dimension + 1];
+
+ if (left > x) {
+ left = x;
+ } else if (right < x) {
+ right = x;
+ }
+
+ if (top > y) {
+ top = y;
+ } else if (bottom < y) {
+ bottom = y;
+ }
+ }
+ return new float[]{left, top, right, bottom};
+ }
+
+ /**
+ * Translate the polygon to x and y
+ * @param dimension in what dimension is polygon represented (supports 2 or 3D).
+ */
+ public static void translate(float[] poly, float translateX, float translateY, int dimension) {
+ int polySize = poly.length/dimension;
+
+ for (int i = 0; i < polySize; i++) {
+ poly[i * dimension + 0] += translateX;
+ poly[i * dimension + 1] += translateY;
+ }
+ }
+
+}
+
diff --git a/android/view/shadow/AmbientShadowConfig.java b/android/view/shadow/AmbientShadowConfig.java
new file mode 100644
index 0000000..97efd86
--- /dev/null
+++ b/android/view/shadow/AmbientShadowConfig.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+/**
+ * Model for ambient shadow rendering. Assumes light sources from centroid of the object.
+ */
+class AmbientShadowConfig {
+
+ private final float mEdgeScale;
+ private final float mShadowBoundRatio;
+ private final float mShadowStrength;
+
+ private final float[] mPolygon;
+ private float[] mLightSourcePosition;
+
+ private AmbientShadowConfig(Builder builder) {
+ mEdgeScale = builder.mEdgeScale;
+ mShadowBoundRatio = builder.mShadowBoundRatio;
+ mShadowStrength = builder.mShadowStrength;
+ mPolygon = builder.mPolygon;
+ mLightSourcePosition = builder.mLightSourcePosition;
+ }
+
+ /**
+ * Returns scales intensity of the edge of the shadow (opacity) [0-100]
+ */
+ public float getEdgeScale() {
+ return mEdgeScale;
+ }
+
+ /**
+ * Returns scales the area (in xy) of the shadow [0-1]
+ */
+ public float getShadowBoundRatio() {
+ return mShadowBoundRatio;
+ }
+
+ /**
+ * Returns scales the intensity of the entire shadow (opacity) [0-1]
+ */
+ public float getShadowStrength() {
+ return mShadowStrength;
+ }
+
+ /**
+ * Returns opaque polygon to cast shadow
+ */
+ public float[] getPolygon() {
+ return mPolygon;
+ }
+
+ /**
+ * Returns 2D position of the light source
+ */
+ public float[] getLightSourcePosition() {
+ return mLightSourcePosition;
+ }
+
+ public static class Builder {
+
+ private float mEdgeScale;
+ private float mShadowBoundRatio;
+ private float mShadowStrength;
+
+ private float[] mPolygon;
+ private float[] mLightSourcePosition;
+
+ public Builder setEdgeScale(float edgeScale) {
+ mEdgeScale = edgeScale;
+ return this;
+ }
+
+ public Builder setShadowBoundRatio(float shadowBoundRatio) {
+ mShadowBoundRatio = shadowBoundRatio;
+ return this;
+ }
+
+ public Builder setShadowStrength(float shadowStrength) {
+ mShadowStrength = shadowStrength;
+ return this;
+ }
+
+ public Builder setPolygon(float[] polygon) {
+ mPolygon = polygon;
+ return this;
+ }
+
+ public Builder setLightSourcePosition(float x, float y) {
+ mLightSourcePosition = new float[] { x, y };
+ return this;
+ }
+
+ public AmbientShadowConfig build() {
+ return new AmbientShadowConfig(this);
+ }
+ }
+}
diff --git a/android/view/shadow/AmbientShadowTriangulator.java b/android/view/shadow/AmbientShadowTriangulator.java
new file mode 100644
index 0000000..d768fa3
--- /dev/null
+++ b/android/view/shadow/AmbientShadowTriangulator.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+
+/**
+ * Generates ambient shadow bitmap
+ */
+class AmbientShadowTriangulator {
+
+ private final AmbientShadowConfig mShadowConfig;
+ private final AmbientShadowVertexCalculator mCalculator;
+
+ private boolean mValid;
+
+ public AmbientShadowTriangulator(AmbientShadowConfig shadowConfig) {
+ mShadowConfig = shadowConfig;
+
+ mCalculator = new AmbientShadowVertexCalculator(mShadowConfig);
+ }
+
+ /**
+ * Populate vertices and fill the triangle buffers.
+ */
+ public void triangulate() {
+ try {
+ mCalculator.generateVertex();
+ mValid = true;
+ } catch (IndexOutOfBoundsException|ArithmeticException mathError) {
+ Bridge.getLog().warning(ILayoutLog.TAG_INFO, "Arithmetic error while drawing " +
+ "ambient shadow",
+ null, mathError);
+ } catch (Exception ex) {
+ Bridge.getLog().warning(ILayoutLog.TAG_INFO, "Error while drawing shadow",
+ null, ex);
+ }
+ }
+
+ public boolean isValid() {
+ return mValid;
+ }
+
+ public float[] getVertices() { return mCalculator.getVertex(); }
+
+ public int[] getIndices() { return mCalculator.getIndex(); }
+
+ public float[] getColors() { return mCalculator.getColor(); }
+}
diff --git a/android/view/shadow/AmbientShadowVertexCalculator.java b/android/view/shadow/AmbientShadowVertexCalculator.java
new file mode 100644
index 0000000..5928f35
--- /dev/null
+++ b/android/view/shadow/AmbientShadowVertexCalculator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+import android.view.math.Math3DHelper;
+
+/**
+ * Generates vertices, colours, and indices required for ambient shadow. Ambient shadows are
+ * assumed to be raycasted from the centroid of the polygon, and reaches upto a ratio based on
+ * the polygon's z-height.
+ */
+class AmbientShadowVertexCalculator {
+
+ private final float[] mVertex;
+ private final float[] mColor;
+ private final int[] mIndex;
+ private final AmbientShadowConfig mConfig;
+
+ public AmbientShadowVertexCalculator(AmbientShadowConfig config) {
+ mConfig = config;
+
+ int size = mConfig.getPolygon().length / 3;
+
+ mVertex = new float[size * 2 * 2];
+ mColor = new float[size * 2 * 4];
+ mIndex = new int[(size * 3 - 2) * 3];
+ }
+
+ /**
+ * Generates ambient shadow triangulation using configuration provided
+ */
+ public void generateVertex() {
+ float[] polygon = mConfig.getPolygon();
+ float cx = mConfig.getLightSourcePosition()[0];
+ float cy = mConfig.getLightSourcePosition()[1];
+
+ int polygonLength = polygon.length/3;
+
+ float opacity = .8f * (0.5f / (mConfig.getEdgeScale() / 10f));
+
+ int trShift = 0;
+ for (int i = 0; i < polygonLength; ++i, trShift += 6) {
+ int shift = i * 4;
+ int colorShift = i * 8;
+ int idxShift = i * 2;
+
+ float px = polygon[3 * i + 0];
+ float py = polygon[3 * i + 1];
+ mVertex[shift + 0] = px;
+ mVertex[shift + 1] = py;
+
+ // TODO: I do not really understand this but this matches the previous behavior.
+ // The weird bit is that for outlines with low elevation the ambient shadow is
+ // entirely drawn underneath the shadow caster. This is most probably incorrect
+ float h = polygon[3 * i + 2] * mConfig.getShadowBoundRatio();
+
+ mVertex[shift + 2] = cx + h * (px - cx);
+ mVertex[shift + 3] = cy + h * (py - cy);
+
+ mColor[colorShift + 3] = opacity;
+
+ mIndex[trShift + 0] = idxShift + 0;
+ mIndex[trShift + 1] = idxShift + 1;
+ mIndex[trShift + 2] = idxShift + 2;
+
+ mIndex[trShift + 3] = idxShift + 1;
+ mIndex[trShift + 4] = idxShift + 2;
+ mIndex[trShift + 5] = idxShift + 3;
+ }
+ // cycle back to the front
+ mIndex[trShift - 1] = 1;
+ mIndex[trShift - 2] = 0;
+ mIndex[trShift - 4] = 0;
+
+ // Filling the shadow right under the outline. Can we skip that?
+ for (int i = 1; i < polygonLength - 1; ++i, trShift += 3) {
+ mIndex[trShift + 0] = 0;
+ mIndex[trShift + 1] = 2 * i;
+ mIndex[trShift + 2] = 2 * (i+1);
+ }
+ }
+
+ public int[] getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * @return list of vertices in 2d in format : {x1, y1, x2, y2 ...}
+ */
+ public float[] getVertex() {
+ return mVertex;
+ }
+
+ public float[] getColor() {
+ return mColor;
+ }
+}
diff --git a/android/view/shadow/HighQualityShadowPainter.java b/android/view/shadow/HighQualityShadowPainter.java
new file mode 100644
index 0000000..7b8873c
--- /dev/null
+++ b/android/view/shadow/HighQualityShadowPainter.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.ViewGroup;
+import android.view.math.Math3DHelper;
+
+import static android.view.shadow.ShadowConstants.MIN_ALPHA;
+import static android.view.shadow.ShadowConstants.SCALE_DOWN;
+
+public class HighQualityShadowPainter {
+ private static final float sRoundedGap = (float) (1.0 - Math.sqrt(2.0) / 2.0);
+
+ private HighQualityShadowPainter() { }
+
+ /**
+ * Draws simple Rect shadow
+ */
+ public static void paintRectShadow(ViewGroup parent, Outline outline, float elevation,
+ Canvas canvas, float alpha, float densityDpi) {
+
+ if (!validate(elevation, densityDpi)) {
+ return;
+ }
+
+ int width = parent.getWidth() / SCALE_DOWN;
+ int height = parent.getHeight() / SCALE_DOWN;
+
+ Rect rectOriginal = new Rect();
+ Rect rectScaled = new Rect();
+ if (!outline.getRect(rectScaled) || alpha < MIN_ALPHA) {
+ // If alpha below MIN_ALPHA it's invisible (based on manual test). Save some perf.
+ return;
+ }
+
+ outline.getRect(rectOriginal);
+
+ rectScaled.left /= SCALE_DOWN;
+ rectScaled.right /= SCALE_DOWN;
+ rectScaled.top /= SCALE_DOWN;
+ rectScaled.bottom /= SCALE_DOWN;
+ float radius = outline.getRadius() / SCALE_DOWN;
+
+ if (radius > rectScaled.width() || radius > rectScaled.height()) {
+ // Rounded edge generation fails if radius is bigger than drawing box.
+ return;
+ }
+
+ // ensure alpha doesn't go over 1
+ alpha = (alpha > 1.0f) ? 1.0f : alpha;
+ boolean isOpaque = outline.getAlpha() * alpha == 1.0f;
+ float[] poly = getPoly(rectScaled, elevation / SCALE_DOWN, radius);
+
+ AmbientShadowConfig ambientConfig = new AmbientShadowConfig.Builder()
+ .setPolygon(poly)
+ .setLightSourcePosition(
+ (rectScaled.left + rectScaled.right) / 2.0f,
+ (rectScaled.top + rectScaled.bottom) / 2.0f)
+ .setEdgeScale(ShadowConstants.AMBIENT_SHADOW_EDGE_SCALE)
+ .setShadowBoundRatio(ShadowConstants.AMBIENT_SHADOW_SHADOW_BOUND)
+ .setShadowStrength(ShadowConstants.AMBIENT_SHADOW_STRENGTH * alpha)
+ .build();
+
+ AmbientShadowTriangulator ambientTriangulator = new AmbientShadowTriangulator(ambientConfig);
+ ambientTriangulator.triangulate();
+
+ SpotShadowTriangulator spotTriangulator = null;
+ float lightZHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP * (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
+ if (lightZHeightPx - elevation / SCALE_DOWN >= ShadowConstants.SPOT_SHADOW_LIGHT_Z_EPSILON) {
+
+ float lightX = (rectScaled.left + rectScaled.right) / 2;
+ float lightY = rectScaled.top;
+ // Light shouldn't be bigger than the object by too much.
+ int dynamicLightRadius = Math.min(rectScaled.width(), rectScaled.height());
+
+ SpotShadowConfig spotConfig = new SpotShadowConfig.Builder()
+ .setLightCoord(lightX, lightY, lightZHeightPx)
+ .setLightRadius(dynamicLightRadius)
+ .setShadowStrength(ShadowConstants.SPOT_SHADOW_STRENGTH * alpha)
+ .setPolygon(poly, poly.length / ShadowConstants.COORDINATE_SIZE)
+ .build();
+
+ spotTriangulator = new SpotShadowTriangulator(spotConfig);
+ spotTriangulator.triangulate();
+ }
+
+ int translateX = 0;
+ int translateY = 0;
+ int imgW = 0;
+ int imgH = 0;
+
+ if (ambientTriangulator.isValid()) {
+ float[] shadowBounds = Math3DHelper.flatBound(ambientTriangulator.getVertices(), 2);
+ // Move the shadow to the left top corner to occupy the least possible bitmap
+
+ translateX = -(int) Math.floor(shadowBounds[0]);
+ translateY = -(int) Math.floor(shadowBounds[1]);
+
+ // create bitmap of the least possible size that covers the entire shadow
+ imgW = (int) Math.ceil(shadowBounds[2] + translateX);
+ imgH = (int) Math.ceil(shadowBounds[3] + translateY);
+ }
+
+ if (spotTriangulator != null && spotTriangulator.validate()) {
+
+ // Bit of a hack to re-adjust spot shadow to fit correctly within parent canvas.
+ // Problem is that outline passed is not a final position, which throws off our
+ // whereas our shadow rendering algorithm, which requires pre-set range for
+ // optimization purposes.
+ float[] shadowBounds = Math3DHelper.flatBound(spotTriangulator.getStrips()[0], 3);
+
+ if ((shadowBounds[2] - shadowBounds[0]) > width ||
+ (shadowBounds[3] - shadowBounds[1]) > height) {
+ // Spot shadow to be casted is larger than the parent canvas,
+ // We'll let ambient shadow do the trick and skip spot shadow here.
+ spotTriangulator = null;
+ }
+
+ translateX = Math.max(-(int) Math.floor(shadowBounds[0]), translateX);
+ translateY = Math.max(-(int) Math.floor(shadowBounds[1]), translateY);
+
+ // create bitmap of the least possible size that covers the entire shadow
+ imgW = Math.max((int) Math.ceil(shadowBounds[2] + translateX), imgW);
+ imgH = Math.max((int) Math.ceil(shadowBounds[3] + translateY), imgH);
+ }
+
+ TriangleBuffer renderer = new TriangleBuffer();
+ renderer.setSize(imgW, imgH, 0);
+
+ if (ambientTriangulator.isValid()) {
+
+ Math3DHelper.translate(ambientTriangulator.getVertices(), translateX, translateY, 2);
+ renderer.drawTriangles(ambientTriangulator.getIndices(), ambientTriangulator.getVertices(),
+ ambientTriangulator.getColors(), ambientConfig.getShadowStrength());
+ }
+
+ if (spotTriangulator != null && spotTriangulator.validate()) {
+ float[][] strips = spotTriangulator.getStrips();
+ for (int i = 0; i < strips.length; ++i) {
+ Math3DHelper.translate(strips[i], translateX, translateY, 3);
+ renderer.drawTriangles(strips[i], ShadowConstants.SPOT_SHADOW_STRENGTH * alpha);
+ }
+ }
+
+ Bitmap img = renderer.createImage();
+
+ drawScaled(canvas, img, translateX, translateY, rectOriginal, radius, isOpaque);
+ }
+
+ /**
+ * High quality shadow does not work well with object that is too high in elevation. Check if
+ * the object elevation is reasonable and returns true if shadow will work well. False other
+ * wise.
+ */
+ private static boolean validate(float elevation, float densityDpi) {
+ float scaledElevationPx = elevation / SCALE_DOWN;
+ float scaledSpotLightHeightPx = ShadowConstants.SPOT_SHADOW_LIGHT_Z_HEIGHT_DP *
+ (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
+ if (scaledElevationPx > scaledSpotLightHeightPx) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Draw the bitmap scaled up.
+ * @param translateX - offset in x axis by which the bitmap is shifted.
+ * @param translateY - offset in y axis by which the bitmap is shifted.
+ * @param shadowCaster - unscaled outline of shadow caster
+ * @param radius
+ */
+ private static void drawScaled(Canvas canvas, Bitmap bitmap, int translateX, int translateY,
+ Rect shadowCaster, float radius, boolean isOpaque) {
+ int unscaledTranslateX = translateX * SCALE_DOWN;
+ int unscaledTranslateY = translateY * SCALE_DOWN;
+
+ // To the canvas
+ Rect dest = new Rect(
+ -unscaledTranslateX,
+ -unscaledTranslateY,
+ (bitmap.getWidth() * SCALE_DOWN) - unscaledTranslateX,
+ (bitmap.getHeight() * SCALE_DOWN) - unscaledTranslateY);
+ Rect destSrc = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ // We can skip drawing the shadows behind the caster if either
+ // 1) radius is 0, the shadow caster is rectangle and we can have a perfect cut
+ // 2) shadow caster is opaque and even if remove shadow only partially it won't affect
+ // the visual quality, otherwise we will observe shadow part through the translucent caster
+ // This can be improved by:
+ // TODO: do not draw the shadow behind the caster at all during the tesselation phase
+ if (radius > 0 && !isOpaque) {
+ // Rounded edge.
+ int save = canvas.save();
+ canvas.drawBitmap(bitmap, destSrc, dest, null);
+ canvas.restoreToCount(save);
+ return;
+ }
+
+ /**
+ * ----------------------------------
+ * | |
+ * | top |
+ * | |
+ * ----------------------------------
+ * | | | |
+ * | left | shadow caster | right |
+ * | | | |
+ * ----------------------------------
+ * | |
+ * | bottom |
+ * | |
+ * ----------------------------------
+ *
+ * dest == top + left + shadow caster + right + bottom
+ * Visually, canvas.drawBitmap(bitmap, destSrc, dest, paint) would achieve the same result.
+ */
+ int gap = (int) Math.ceil(radius * SCALE_DOWN * sRoundedGap);
+ shadowCaster.bottom -= gap;
+ shadowCaster.top += gap;
+ shadowCaster.left += gap;
+ shadowCaster.right -= gap;
+ Rect left = new Rect(dest.left, shadowCaster.top, shadowCaster.left,
+ shadowCaster.bottom);
+ int leftScaled = left.width() / SCALE_DOWN + destSrc.left;
+
+ Rect top = new Rect(dest.left, dest.top, dest.right, shadowCaster.top);
+ int topScaled = top.height() / SCALE_DOWN + destSrc.top;
+
+ Rect right = new Rect(shadowCaster.right, shadowCaster.top, dest.right,
+ shadowCaster.bottom);
+ int rightScaled = (shadowCaster.right - dest.left) / SCALE_DOWN + destSrc.left;
+
+ Rect bottom = new Rect(dest.left, shadowCaster.bottom, dest.right, dest.bottom);
+ int bottomScaled = (shadowCaster.bottom - dest.top) / SCALE_DOWN + destSrc.top;
+
+ // calculate parts of the middle ground that can be ignored.
+ Rect leftSrc = new Rect(destSrc.left, topScaled, leftScaled, bottomScaled);
+ Rect topSrc = new Rect(destSrc.left, destSrc.top, destSrc.right, topScaled);
+ Rect rightSrc = new Rect(rightScaled, topScaled, destSrc.right, bottomScaled);
+ Rect bottomSrc = new Rect(destSrc.left, bottomScaled, destSrc.right, destSrc.bottom);
+
+ int save = canvas.save();
+ Paint paint = new Paint();
+ canvas.drawBitmap(bitmap, leftSrc, left, paint);
+ canvas.drawBitmap(bitmap, topSrc, top, paint);
+ canvas.drawBitmap(bitmap, rightSrc, right, paint);
+ canvas.drawBitmap(bitmap, bottomSrc, bottom, paint);
+ canvas.restoreToCount(save);
+ }
+
+ private static float[] getPoly(Rect rect, float elevation, float radius) {
+ if (radius <= 0) {
+ float[] poly = new float[ShadowConstants.RECT_VERTICES_SIZE * ShadowConstants.COORDINATE_SIZE];
+
+ poly[0] = poly[9] = rect.left;
+ poly[1] = poly[4] = rect.top;
+ poly[3] = poly[6] = rect.right;
+ poly[7] = poly[10] = rect.bottom;
+ poly[2] = poly[5] = poly[8] = poly[11] = elevation;
+
+ return poly;
+ }
+
+ return buildRoundedEdges(rect, elevation, radius);
+ }
+
+ private static float[] buildRoundedEdges(
+ Rect rect, float elevation, float radius) {
+
+ float[] roundedEdgeVertices = new float[(ShadowConstants.SPLICE_ROUNDED_EDGE + 1) * 4 * 3];
+ int index = 0;
+ // 1.0 LT. From theta 0 to pi/2 in K division.
+ for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) {
+ double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
+ float x = (float) (rect.left + (radius - radius * Math.cos(theta)));
+ float y = (float) (rect.top + (radius - radius * Math.sin(theta)));
+ roundedEdgeVertices[index++] = x;
+ roundedEdgeVertices[index++] = y;
+ roundedEdgeVertices[index++] = elevation;
+ }
+
+ // 2.0 RT
+ for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) {
+ double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
+ float x = (float) (rect.right - (radius - radius * Math.cos(theta)));
+ float y = (float) (rect.top + (radius - radius * Math.sin(theta)));
+ roundedEdgeVertices[index++] = x;
+ roundedEdgeVertices[index++] = y;
+ roundedEdgeVertices[index++] = elevation;
+ }
+
+ // 3.0 RB
+ for (int i = 0; i <= ShadowConstants.SPLICE_ROUNDED_EDGE; i++) {
+ double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
+ float x = (float) (rect.right - (radius - radius * Math.cos(theta)));
+ float y = (float) (rect.bottom - (radius - radius * Math.sin(theta)));
+ roundedEdgeVertices[index++] = x;
+ roundedEdgeVertices[index++] = y;
+ roundedEdgeVertices[index++] = elevation;
+ }
+
+ // 4.0 LB
+ for (int i = ShadowConstants.SPLICE_ROUNDED_EDGE; i >= 0; i--) {
+ double theta = (Math.PI / 2.0d) * ((double) i / ShadowConstants.SPLICE_ROUNDED_EDGE);
+ float x = (float) (rect.left + (radius - radius * Math.cos(theta)));
+ float y = (float) (rect.bottom - (radius - radius * Math.sin(theta)));
+ roundedEdgeVertices[index++] = x;
+ roundedEdgeVertices[index++] = y;
+ roundedEdgeVertices[index++] = elevation;
+ }
+
+ return roundedEdgeVertices;
+ }
+}
diff --git a/android/view/shadow/ShadowConstants.java b/android/view/shadow/ShadowConstants.java
new file mode 100644
index 0000000..049a549
--- /dev/null
+++ b/android/view/shadow/ShadowConstants.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+/**
+ * Constant values for shadow related configuration
+ */
+class ShadowConstants {
+
+ /**
+ * This is used as a factor by which to scale down the shadow bitmap. If we have world
+ * Width x Height, shadow bitmap will be Width/SCALE_DOWN x Height/SCALE_DOWN and during
+ * canvas draw the shadow will be scaled up, resulting faster perf (due to smaller bitmap) but
+ * blurrier (lower quality) shadow.
+ */
+ public static final int SCALE_DOWN = 5;
+
+ public static final float MIN_ALPHA = 0.2f;
+
+ public static final int SPOT_SHADOW_LIGHT_Z_HEIGHT_DP = 50 / SCALE_DOWN;
+ public static final int SPOT_SHADOW_LIGHT_Z_EPSILON = 10 / SCALE_DOWN;
+ public static final float SPOT_SHADOW_STRENGTH = 0.3f;
+
+ public static final float AMBIENT_SHADOW_EDGE_SCALE = 60f;
+ public static final float AMBIENT_SHADOW_SHADOW_BOUND = 0.02f * SCALE_DOWN;
+ public static final float AMBIENT_SHADOW_STRENGTH = 1.0f;
+
+ public static final int COORDINATE_SIZE = 3;
+ public static final int RECT_VERTICES_SIZE = 4;
+
+ public static final int SPLICE_ROUNDED_EDGE = 5;
+}
diff --git a/android/view/shadow/SpotShadowConfig.java b/android/view/shadow/SpotShadowConfig.java
new file mode 100644
index 0000000..7b4fa4b
--- /dev/null
+++ b/android/view/shadow/SpotShadowConfig.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+
+/**
+ * Model for spot shadow rendering. Assumes single light, single object.
+ */
+class SpotShadowConfig {
+
+ // No need to be final but making it immutable for now.
+ private final int mLightRadius;
+
+
+ // No need to be final but making it immutable for now.
+ private final float[] mPoly;
+ private final int mPolyLength;
+
+ private float[] mLightCoord;
+
+ private final float mShadowStrength;
+
+ private SpotShadowConfig(SpotShadowConfig.Builder builder) {
+ mLightRadius = builder.mLightRadius;
+ mPoly = builder.mPoly;
+ mPolyLength = builder.mPolyLength;
+
+ mLightCoord = new float[3];
+ mLightCoord[0] = builder.mLightX;
+ mLightCoord[1] = builder.mLightY;
+ mLightCoord[2] = builder.mLightHeight;
+ mShadowStrength = builder.mShadowStrength;
+ }
+
+ /**
+ * @return size of the light source radius (light source is always generated as a circular shape)
+ */
+ public int getLightRadius() {
+ return mLightRadius;
+ }
+
+ /**
+ * @return object that casts shadow. xyz coordinates.
+ */
+ public float[] getPoly() {
+ return mPoly;
+ }
+
+ /**
+ * @return # of vertices in the object {@link #getPoly()} that casts shadow.
+ */
+ public int getPolyLength() {
+ return mPolyLength;
+ }
+
+ /**
+ * Update the light source coord.
+ * @param x - horizontal coordinate
+ * @param y - vertical coordinate
+ */
+ public void setLightCoord(float x, float y) {
+ mLightCoord[0] = x;
+ mLightCoord[1] = y;
+ }
+
+ /**
+ * @return shadow intensity from 0 to 1
+ */
+ public float getShadowStrength() {
+ return mShadowStrength;
+ }
+
+ public float[] getLightCoord() {
+ return mLightCoord;
+ }
+
+ public static class Builder {
+
+ // No need to be final but making it immutable for now.
+ private int mLightRadius;
+
+ // No need to be final but making it immutable for now.
+ private float[] mPoly;
+ private int mPolyLength;
+
+ private float mLightX;
+ private float mLightY;
+ private float mLightHeight;
+
+ private float mShadowStrength;
+
+ /**
+ * @param shadowStrength from 0 to 1
+ */
+ public Builder setShadowStrength(float shadowStrength) {
+ this.mShadowStrength = shadowStrength;
+ return this;
+ }
+
+ public Builder setLightRadius(int mLightRadius) {
+ this.mLightRadius = mLightRadius;
+ return this;
+ }
+
+ public Builder setPolygon(float[] poly, int polyLength) {
+ this.mPoly = poly;
+ this.mPolyLength = polyLength;
+ return this;
+ }
+
+ public Builder setLightCoord(float lightX, float lightY, float lightHeight) {
+ this.mLightX = lightX;
+ this.mLightY = lightY;
+ this.mLightHeight = lightHeight;
+ return this;
+ }
+
+ public SpotShadowConfig build() {
+ return new SpotShadowConfig(this);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/view/shadow/SpotShadowTriangulator.java b/android/view/shadow/SpotShadowTriangulator.java
new file mode 100644
index 0000000..d666153
--- /dev/null
+++ b/android/view/shadow/SpotShadowTriangulator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+import com.android.ide.common.rendering.api.ILayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+
+/**
+ * Generate spot shadow bitmap.
+ */
+class SpotShadowTriangulator {
+
+ private final SpotShadowConfig mShadowConfig;
+ private float[][] mStrips;
+
+ public SpotShadowTriangulator(SpotShadowConfig config) {
+ mShadowConfig = config;
+ }
+
+ /**
+ * Populate the shadow bitmap.
+ */
+ public void triangulate() {
+ try {
+ float[] lightSources =
+ SpotShadowVertexCalculator.calculateLight(mShadowConfig.getLightRadius(),
+ mShadowConfig.getLightCoord()[0],
+ mShadowConfig.getLightCoord()[1], mShadowConfig.getLightCoord()[2]);
+
+
+ mStrips = new float[2][];
+ int[] sizes = SpotShadowVertexCalculator.getStripSizes(mShadowConfig.getPolyLength());
+ for (int i = 0; i < sizes.length; ++i) {
+ mStrips[i] = new float[3 * sizes[i]];
+ }
+
+ SpotShadowVertexCalculator.calculateShadow(lightSources,
+ mShadowConfig.getPoly(),
+ mShadowConfig.getPolyLength(),
+ mShadowConfig.getShadowStrength(),
+ mStrips);
+ } catch (IndexOutOfBoundsException|ArithmeticException mathError) {
+ Bridge.getLog().warning(ILayoutLog.TAG_INFO, "Arithmetic error while drawing " +
+ "spot shadow",
+ null, mathError);
+ } catch (Exception ex) {
+ Bridge.getLog().warning(ILayoutLog.TAG_INFO, "Error while drawing shadow",
+ null, ex);
+ }
+ }
+ /**
+ * @return true if generated shadow poly is valid. False otherwise.
+ */
+ public boolean validate() {
+ return mStrips != null && mStrips[0].length >= 9;
+ }
+
+ public float[][] getStrips() {
+ return mStrips;
+ }
+}
diff --git a/android/view/shadow/SpotShadowVertexCalculator.java b/android/view/shadow/SpotShadowVertexCalculator.java
new file mode 100644
index 0000000..fc02d18
--- /dev/null
+++ b/android/view/shadow/SpotShadowVertexCalculator.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+import android.graphics.Rect;
+import android.view.math.Math3DHelper;
+
+/**
+ * Generates the vertices required for spot shadow and all other shadow-related rendering.
+ */
+class SpotShadowVertexCalculator {
+
+ private SpotShadowVertexCalculator() { }
+
+ /**
+ * Create evenly distributed circular light source points from x and y (on flat z plane).
+ * This is useful for ray tracing the shadow points later. Format : (x1,y1,z1,x2,y2,z2 ...)
+ *
+ * @param radius - radius of the light source
+ * @param x - center X of the light source
+ * @param y - center Y of the light source
+ * @param height - how high (z depth) the light should be
+ * @return float points (x,y,z) of light source points.
+ */
+ public static float[] calculateLight(float radius, float x, float y, float height) {
+ float[] ret = new float[4 * 3];
+ // bottom
+ ret[0] = x;
+ ret[1] = y + radius;
+ ret[2] = height;
+ // left
+ ret[3] = x - radius;
+ ret[4] = y;
+ ret[5] = height;
+ // top
+ ret[6] = x;
+ ret[7] = y - radius;
+ ret[8] = height;
+ // right
+ ret[9] = x + radius;
+ ret[10] = y;
+ ret[11] = height;
+
+ return ret;
+ }
+
+ /**
+ * @param polyLength - length of the outline polygon
+ * @return size required for shadow vertices mData array based on # of vertices in the
+ * outline polygon
+ */
+ public static int[] getStripSizes(int polyLength){
+ return new int[] { ((polyLength + 4) / 8) * 16 + 2, 4};
+ }
+
+ /**
+ * Generate shadow vertices based on params. Format : (x1,y1,z1,x2,y2,z2 ...)
+ * Precondition : Light poly must be evenly distributed on a flat surface
+ * Precondition : Poly vertices must be a convex
+ * Precondition : Light height must be higher than any poly vertices
+ *
+ * @param lightPoly - Vertices of a light source.
+ * @param poly - Vertices of opaque object casting shadow
+ * @param polyLength - Size of the vertices
+ * @param strength - Strength of the shadow overall [0-1]
+ * @param retstrips - Arrays of triplets, each triplet represents a point, thus every array to
+ * be filled in format : {x1, y1, z1, x2, y2, z2, ...},
+ * every 3 consecutive triplets constitute a triangle to fill, namely [t1, t2, t3], [t2, t3,
+ * t4], ... If at some point [t(n-1), tn, t(n+1)] is no longer a desired a triangle and
+ * there are more triangles to draw one can start a new array, hence retstrips is a 2D array.
+ */
+ public static void calculateShadow(
+ float[] lightPoly,
+ float[] poly,
+ int polyLength,
+ float strength,
+ float[][] retstrips) {
+ float[] outerStrip = retstrips[0];
+
+ // We would like to unify the cases where we have roundrects and rectangles
+ int roundedEdgeSegments = ((polyLength == 4) ? 0 : ShadowConstants.SPLICE_ROUNDED_EDGE);
+ int sideLength = (roundedEdgeSegments / 2 + 1) * 2;
+ float[] umbra = new float[4 * 2 * sideLength];
+ int idx = (roundedEdgeSegments + 1) / 2;
+ int uShift = 0;
+ // If we have even number of segments in rounded corner (0 included), the central point of
+ // rounded corner contributes to the hull twice, from 2 different light sources, thus
+ // rollBack in that case, otherwise every point contributes only once
+ int rollBack = (((polyLength % 8) == 0) ? 0 : 1);
+ // Calculate umbra - a hull of all projections
+ for (int s = 0; s < 4; ++s) { // 4 sides
+ float lx = lightPoly[s * 3 + 0];
+ float ly = lightPoly[s * 3 + 1];
+ float lz = lightPoly[s * 3 + 2];
+ for (int i = 0; i < sideLength; ++i, uShift += 2, ++idx) {
+ int shift = (idx % polyLength) * 3;
+
+ float t = lz / (lz - poly[shift + 2]);
+
+ umbra[uShift + 0] = lx - t * (lx - poly[shift + 0]);
+ umbra[uShift + 1] = ly - t * (ly - poly[shift + 1]);
+ }
+
+ idx -= rollBack;
+ }
+
+ idx = roundedEdgeSegments;
+ // An array that wil contain top, right, bottom, left coordinate of penumbra
+ float[] penumbraRect = new float[4];
+ // Calculate penumbra
+ for (int s = 0; s < 4; ++s, idx += (roundedEdgeSegments + 1)) { // 4 sides
+ int sp = (s + 2) % 4;
+
+ float lz = lightPoly[sp * 3 + 2];
+
+ int shift = (idx % polyLength) * 3;
+
+ float t = lz / (lz - poly[shift + 2]);
+
+ // We are interested in just one coordinate: x or y, depending on the light source
+ int c = (s + 1) % 2;
+ penumbraRect[s] =
+ lightPoly[sp * 3 + c] - t * (lightPoly[sp * 3 + c] - poly[shift + c]);
+ }
+ if (penumbraRect[0] > penumbraRect[2]) {
+ float tmp = (penumbraRect[0] + penumbraRect[2]) / 2.0f;
+ penumbraRect[0] = penumbraRect[2] = tmp;
+ }
+ if (penumbraRect[3] > penumbraRect[1]) {
+ float tmp = (penumbraRect[1] + penumbraRect[3]) / 2.0f;
+ penumbraRect[1] = penumbraRect[3] = tmp;
+ }
+
+ // Now just connect umbra points (at least 8 of them) with the closest points from
+ // penumbra (only 4 of them) to form triangles to fill the entire space between umbra and
+ // penumbra
+ idx = sideLength * 4 - sideLength / 2;
+ int rsShift = 0;
+ for (int s = 0; s < 4; ++s) {
+ int xidx = (((s + 3) % 4) / 2) * 2 + 1;
+ int yidx = (s / 2) * 2;
+ float penumbraX = penumbraRect[xidx];
+ float penumbraY = penumbraRect[yidx];
+ for (int i = 0; i < sideLength; ++i, rsShift += 6, ++idx) {
+ int shift = (idx % (sideLength * 4)) * 2;
+
+ outerStrip[rsShift + 0] = umbra[shift + 0];
+ outerStrip[rsShift + 1] = umbra[shift + 1];
+ outerStrip[rsShift + 3] = penumbraX;
+ outerStrip[rsShift + 4] = penumbraY;
+ outerStrip[rsShift + 5] = strength;
+ }
+ }
+ // Connect with the beginning
+ outerStrip[rsShift + 0] = outerStrip[0];
+ outerStrip[rsShift + 1] = outerStrip[1];
+ // outerStrip[rsShift + 2] = 0;
+ outerStrip[rsShift + 3] = outerStrip[3];
+ outerStrip[rsShift + 4] = outerStrip[4];
+ outerStrip[rsShift + 5] = strength;
+
+ float[] innerStrip = retstrips[1];
+ // Covering penumbra rectangle
+ // left, top
+ innerStrip[0] = penumbraRect[3];
+ innerStrip[1] = penumbraRect[0];
+ innerStrip[2] = strength;
+ // right, top
+ innerStrip[3] = penumbraRect[1];
+ innerStrip[4] = penumbraRect[0];
+ innerStrip[5] = strength;
+ // left, bottom
+ innerStrip[6] = penumbraRect[3];
+ innerStrip[7] = penumbraRect[2];
+ innerStrip[8] = strength;
+ // right, bottom
+ innerStrip[9] = penumbraRect[1];
+ innerStrip[10] = penumbraRect[2];
+ innerStrip[11] = strength;
+ }
+}
\ No newline at end of file
diff --git a/android/view/shadow/TriangleBuffer.java b/android/view/shadow/TriangleBuffer.java
new file mode 100644
index 0000000..3c01171
--- /dev/null
+++ b/android/view/shadow/TriangleBuffer.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.shadow;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.view.math.Math3DHelper;
+
+import java.util.Arrays;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+/**
+ * 2D Triangle buffer element that colours using z value. (z scale set).
+ */
+class TriangleBuffer {
+ int mWidth;
+ int mHeight;
+ int mImgWidth;
+ int mImgHeight;
+ int mBorder;
+ Bitmap mBitmap;
+ int mData[];
+
+ public void setSize(int width, int height, int border) {
+ if (mWidth == width && mHeight == height) {
+ return;
+ }
+ mWidth = width-2*border;
+ mHeight = height-2*border;
+ mBorder = border;
+ mImgWidth = width;
+ mImgHeight = height;
+
+ mBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
+ mData = new int[width * height];
+ }
+
+ public void drawTriangles(int[] index, float[] vert, float[] color,float scale) {
+ int indexSize = index.length / 3;
+ for (int i = 0; i < indexSize; i++) {
+ int vIndex = index[i * 3 + 0];
+ float vx = vert[vIndex * 2 + 0];
+ float vy = vert[vIndex * 2 + 1];
+ float c = scale*color[vIndex * 4 + 3];
+ float fx3 = vx, fy3 = vy, fz3 = c;
+
+ vIndex = index[i * 3 + 1];
+ vx = vert[vIndex * 2 + 0];
+ vy = vert[vIndex * 2 + 1];
+ c = scale*color[vIndex * 4 + 3];
+ float fx2 = vx, fy2 = vy, fz2 = c;
+
+ vIndex = index[i * 3 + 2];
+ vx = vert[vIndex * 2 + 0];
+ vy = vert[vIndex * 2 + 1];
+ c = scale*color[vIndex * 4 + 3];
+ float fx1 = vx, fy1 = vy, fz1 = c;
+
+ triangleZBuffMin(mData, mImgWidth, mImgHeight, fx1, fy1, fz1, fx2, fy2,
+ fz2, fx3, fy3, fz3);
+ }
+ }
+
+ public void drawTriangles(float[] strip,float scale) {
+ for (int i = 0; i < strip.length-8; i+=3) {
+ float fx3 = strip[i], fy3 = strip[i+1], fz3 = scale* strip[i+2];
+ float fx2 = strip[i+3], fy2 = strip[i+4], fz2 = scale* strip[i+5];
+ float fx1 = strip[i+6], fy1 = strip[i+7], fz1 = scale* strip[i+8];
+
+ if (fx1*(fy2-fy3)+fx2*(fy3-fy1)+fx3*(fy1-fy2) ==0) {
+ continue;
+ }
+ triangleZBuffMin(mData, mImgWidth, mImgHeight, fx3, fy3, fz3, fx2, fy2,
+ fz2, fx1, fy1, fz1);
+ }
+ }
+
+ public Bitmap createImage() {
+ mBitmap.setPixels(mData, 0, mWidth, 0, 0, mWidth, mHeight);
+ return mBitmap;
+ }
+
+ private static void triangleZBuffMin(int[] buff, int w, int h, float fx3,
+ float fy3, float fz3, float fx2, float fy2, float fz2, float fx1,
+ float fy1, float fz1) {
+ if (((fx1 - fx2) * (fy3 - fy2) - (fy1 - fy2) * (fx3 - fx2)) < 0) {
+ float tmpx = fx1;
+ float tmpy = fy1;
+ float tmpz = fz1;
+ fx1 = fx2;
+ fy1 = fy2;
+ fz1 = fz2;
+ fx2 = tmpx;
+ fy2 = tmpy;
+ fz2 = tmpz;
+ }
+ // using maxmima
+ // solve([x1*dx+y1*dy+zoff=z1,x2*dx+y2*dy+zoff=z2,x3*dx+y3*dy+zoff=z3],[dx,dy,zoff]);
+ double d = (fx1 * (fy3 - fy2) - fx2 * fy3 + fx3 * fy2 + (fx2 - fx3) * fy1);
+ if (d == 0) {
+ return;
+ }
+ float dx = (float) (-(fy1 * (fz3 - fz2) - fy2 * fz3 + fy3 * fz2 + (fy2 - fy3)
+ * fz1) / d);
+ float dy = (float) ((fx1 * (fz3 - fz2) - fx2 * fz3 + fx3 * fz2 + (fx2 - fx3)
+ * fz1) / d);
+ float zoff = (float) ((fx1 * (fy3 * fz2 - fy2 * fz3) + fy1
+ * (fx2 * fz3 - fx3 * fz2) + (fx3 * fy2 - fx2 * fy3) * fz1) / d);
+
+ // 28.4 fixed-point coordinates
+ int y1 = (int) (16.0f * fy1 + .5f);
+ int y2 = (int) (16.0f * fy2 + .5f);
+ int y3 = (int) (16.0f * fy3 + .5f);
+
+ int x1 = (int) (16.0f * fx1 + .5f);
+ int x2 = (int) (16.0f * fx2 + .5f);
+ int x3 = (int) (16.0f * fx3 + .5f);
+
+ int dx12 = x1 - x2;
+ int dx23 = x2 - x3;
+ int dx31 = x3 - x1;
+
+ int dy12 = y1 - y2;
+ int dy23 = y2 - y3;
+ int dy31 = y3 - y1;
+
+ int fdx12 = dx12 << 4;
+ int fdx23 = dx23 << 4;
+ int fdx31 = dx31 << 4;
+
+ int fdy12 = dy12 << 4;
+ int fdy23 = dy23 << 4;
+ int fdy31 = dy31 << 4;
+
+ int minx = (Math3DHelper.min(x1, x2, x3) + 0xF) >> 4;
+ int maxx = (Math3DHelper.max(x1, x2, x3) + 0xF) >> 4;
+ int miny = (Math3DHelper.min(y1, y2, y3) + 0xF) >> 4;
+ int maxy = (Math3DHelper.max(y1, y2, y3) + 0xF) >> 4;
+
+ if (miny < 0) {
+ miny = 0;
+ }
+ if (minx < 0) {
+ minx = 0;
+ }
+ if (maxx > w) {
+ maxx = w;
+ }
+ if (maxy > h) {
+ maxy = h;
+ }
+ int off = miny * w;
+
+ int c1 = dy12 * x1 - dx12 * y1;
+ int c2 = dy23 * x2 - dx23 * y2;
+ int c3 = dy31 * x3 - dx31 * y3;
+
+ if (dy12 < 0 || (dy12 == 0 && dx12 > 0)) {
+ c1++;
+ }
+ if (dy23 < 0 || (dy23 == 0 && dx23 > 0)) {
+ c2++;
+ }
+ if (dy31 < 0 || (dy31 == 0 && dx31 > 0)) {
+ c3++;
+ }
+ int cy1 = c1 + dx12 * (miny << 4) - dy12 * (minx << 4);
+ int cy2 = c2 + dx23 * (miny << 4) - dy23 * (minx << 4);
+ int cy3 = c3 + dx31 * (miny << 4) - dy31 * (minx << 4);
+
+ for (int y = miny; y < maxy; y++) {
+ int cx1 = cy1;
+ int cx2 = cy2;
+ int cx3 = cy3;
+ float p = zoff + dy * y;
+
+ int startx = start(cx1, fdy12, minx, minx, maxx);
+ startx = start(cx2, fdy23, minx, startx, maxx);
+ startx = start(cx3, fdy31, minx, startx, maxx);
+
+ cx1 -= (startx - minx) * fdy12;
+ cx2 -= (startx - minx) * fdy23;
+ cx3 -= (startx - minx) * fdy31;
+
+ int endx = end(cx1, fdy12, startx, minx, maxx);
+ endx = end(cx2, fdy23, startx, minx, endx);
+ endx = end(cx3, fdy31, startx, minx, endx);
+
+ for (int x = startx; x < endx; x++) {
+ int point = x + off;
+ float zval = p + dx * x;
+ // Simple alpha-blending
+ int prev = (buff[point] >> 24) & 0xFF;
+ int res = (int) (zval * (255 - prev )) + prev;
+ buff[point] = res << 24;
+ }
+ cy1 += fdx12;
+ cy2 += fdx23;
+ cy3 += fdx31;
+ off += w;
+ }
+ }
+
+ /**
+ * Returns the minimum value of x in the range [minx, maxx]: y0 - dy * (x - x0) > 0
+ * If no value satisfies the expression, maxx is returned
+ *
+ * @param y0 - value in x0
+ * @param dy - delta y
+ * @param x0 - some position, for which value is known (y0)
+ * @param minx - start of the range
+ * @param maxx - end of the range
+ * @return minimum x
+ */
+ private static int start(int y0, int dy, int x0, int minx, int maxx) {
+ if (y0 > 0) {
+ return minx;
+ }
+ if (dy >= 0) {
+ return maxx;
+ }
+ return max(x0 + y0 / dy + 1, minx);
+ }
+
+ /**
+ * Returns the minimum value of x in range [minx, maxx]: y0 - dy * (x - x0) <= 0
+ * If no value satisfies the expression maxx is returned
+ *
+ * @param y0 - value in x0
+ * @param dy - delta y
+ * @param x0 - some position, for which value is known (y0)
+ * @param minx - start of the range
+ * @param maxx - end of the range
+ * @return minimum x
+ */
+ private static int end(int y0, int dy, int x0, int minx, int maxx) {
+ if (y0 <= 0) {
+ return minx;
+ }
+ if (dy <= 0) {
+ return maxx;
+ }
+ return min(x0 + (y0 - 1) / dy + 1, maxx);
+ }
+
+ public void clear() {
+ Arrays.fill(mData, 0);
+ }
+}
\ No newline at end of file
diff --git a/android/view/textclassifier/ConversationAction.java b/android/view/textclassifier/ConversationAction.java
new file mode 100644
index 0000000..bf0409d
--- /dev/null
+++ b/android/view/textclassifier/ConversationAction.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view.textclassifier;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.app.RemoteAction;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.util.Objects;
+
+/** Represents the action suggested by a {@link TextClassifier} on a given conversation. */
+public final class ConversationAction implements Parcelable {
+
+ /** @hide */
+ @Retention(SOURCE)
+ @StringDef(
+ value = {
+ TYPE_VIEW_CALENDAR,
+ TYPE_VIEW_MAP,
+ TYPE_TRACK_FLIGHT,
+ TYPE_OPEN_URL,
+ TYPE_SEND_SMS,
+ TYPE_CALL_PHONE,
+ TYPE_SEND_EMAIL,
+ TYPE_TEXT_REPLY,
+ TYPE_CREATE_REMINDER,
+ TYPE_SHARE_LOCATION
+ },
+ prefix = "TYPE_")
+ public @interface ActionType {}
+
+ /**
+ * Indicates an action to view a calendar at a specified time.
+ */
+ public static final String TYPE_VIEW_CALENDAR = "view_calendar";
+ /**
+ * Indicates an action to view the map at a specified location.
+ */
+ public static final String TYPE_VIEW_MAP = "view_map";
+ /**
+ * Indicates an action to track a flight.
+ */
+ public static final String TYPE_TRACK_FLIGHT = "track_flight";
+ /**
+ * Indicates an action to open an URL.
+ */
+ public static final String TYPE_OPEN_URL = "open_url";
+ /**
+ * Indicates an action to send a SMS.
+ */
+ public static final String TYPE_SEND_SMS = "send_sms";
+ /**
+ * Indicates an action to call a phone number.
+ */
+ public static final String TYPE_CALL_PHONE = "call_phone";
+ /**
+ * Indicates an action to send an email.
+ */
+ public static final String TYPE_SEND_EMAIL = "send_email";
+ /**
+ * Indicates an action to reply with a text message.
+ */
+ public static final String TYPE_TEXT_REPLY = "text_reply";
+ /**
+ * Indicates an action to create a reminder.
+ */
+ public static final String TYPE_CREATE_REMINDER = "create_reminder";
+ /**
+ * Indicates an action to reply with a location.
+ */
+ public static final String TYPE_SHARE_LOCATION = "share_location";
+
+ // TODO: Make this public API
+ /** @hide **/
+ public static final String TYPE_ADD_CONTACT = "add_contact";
+
+ // TODO: Make this public API
+ /** @hide **/
+ public static final String TYPE_COPY = "copy";
+
+ public static final @NonNull Creator<ConversationAction> CREATOR =
+ new Creator<ConversationAction>() {
+ @Override
+ public ConversationAction createFromParcel(Parcel in) {
+ return new ConversationAction(in);
+ }
+
+ @Override
+ public ConversationAction[] newArray(int size) {
+ return new ConversationAction[size];
+ }
+ };
+
+ @NonNull
+ @ActionType
+ private final String mType;
+ @NonNull
+ private final CharSequence mTextReply;
+ @Nullable
+ private final RemoteAction mAction;
+
+ @FloatRange(from = 0, to = 1)
+ private final float mScore;
+
+ @NonNull
+ private final Bundle mExtras;
+
+ private ConversationAction(
+ @NonNull String type,
+ @Nullable RemoteAction action,
+ @Nullable CharSequence textReply,
+ float score,
+ @NonNull Bundle extras) {
+ mType = Objects.requireNonNull(type);
+ mAction = action;
+ mTextReply = textReply;
+ mScore = score;
+ mExtras = Objects.requireNonNull(extras);
+ }
+
+ private ConversationAction(Parcel in) {
+ mType = in.readString();
+ mAction = in.readParcelable(null);
+ mTextReply = in.readCharSequence();
+ mScore = in.readFloat();
+ mExtras = in.readBundle();
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mType);
+ parcel.writeParcelable(mAction, flags);
+ parcel.writeCharSequence(mTextReply);
+ parcel.writeFloat(mScore);
+ parcel.writeBundle(mExtras);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Returns the type of this action, for example, {@link #TYPE_VIEW_CALENDAR}. */
+ @NonNull
+ @ActionType
+ public String getType() {
+ return mType;
+ }
+
+ /**
+ * Returns a RemoteAction object, which contains the icon, label and a PendingIntent, for
+ * the specified action type.
+ */
+ @Nullable
+ public RemoteAction getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns the confidence score for the specified action. The value ranges from 0 (low
+ * confidence) to 1 (high confidence).
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getConfidenceScore() {
+ return mScore;
+ }
+
+ /**
+ * Returns the text reply that could be sent as a reply to the given conversation.
+ * <p>
+ * This is only available when the type of the action is {@link #TYPE_TEXT_REPLY}.
+ */
+ @Nullable
+ public CharSequence getTextReply() {
+ return mTextReply;
+ }
+
+ /**
+ * Returns the extended data related to this conversation action.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** @hide */
+ public Builder toBuilder() {
+ return new Builder(mType)
+ .setTextReply(mTextReply)
+ .setAction(mAction)
+ .setConfidenceScore(mScore)
+ .setExtras(mExtras);
+ }
+
+ /** Builder class to construct {@link ConversationAction}. */
+ public static final class Builder {
+ @Nullable
+ @ActionType
+ private String mType;
+ @Nullable
+ private RemoteAction mAction;
+ @Nullable
+ private CharSequence mTextReply;
+ private float mScore;
+ @Nullable
+ private Bundle mExtras;
+
+ public Builder(@NonNull @ActionType String actionType) {
+ mType = Objects.requireNonNull(actionType);
+ }
+
+ /**
+ * Sets an action that may be performed on the given conversation.
+ */
+ @NonNull
+ public Builder setAction(@Nullable RemoteAction action) {
+ mAction = action;
+ return this;
+ }
+
+ /**
+ * Sets a text reply that may be performed on the given conversation.
+ */
+ @NonNull
+ public Builder setTextReply(@Nullable CharSequence textReply) {
+ mTextReply = textReply;
+ return this;
+ }
+
+ /** Sets the confident score. */
+ @NonNull
+ public Builder setConfidenceScore(@FloatRange(from = 0, to = 1) float score) {
+ mScore = score;
+ return this;
+ }
+
+ /**
+ * Sets the extended data for the conversation action object.
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /** Builds the {@link ConversationAction} object. */
+ @NonNull
+ public ConversationAction build() {
+ return new ConversationAction(
+ mType,
+ mAction,
+ mTextReply,
+ mScore,
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+}
diff --git a/android/view/textclassifier/ConversationActions.java b/android/view/textclassifier/ConversationActions.java
new file mode 100644
index 0000000..6ad5cb9
--- /dev/null
+++ b/android/view/textclassifier/ConversationActions.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view.textclassifier;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.app.Person;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.SpannedString;
+
+import java.lang.annotation.Retention;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a list of actions suggested by a {@link TextClassifier} on a given conversation.
+ *
+ * @see TextClassifier#suggestConversationActions(Request)
+ */
+public final class ConversationActions implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<ConversationActions> CREATOR =
+ new Creator<ConversationActions>() {
+ @Override
+ public ConversationActions createFromParcel(Parcel in) {
+ return new ConversationActions(in);
+ }
+
+ @Override
+ public ConversationActions[] newArray(int size) {
+ return new ConversationActions[size];
+ }
+ };
+
+ private final List<ConversationAction> mConversationActions;
+ private final String mId;
+
+ /** Constructs a {@link ConversationActions} object. */
+ public ConversationActions(
+ @NonNull List<ConversationAction> conversationActions, @Nullable String id) {
+ mConversationActions =
+ Collections.unmodifiableList(Objects.requireNonNull(conversationActions));
+ mId = id;
+ }
+
+ private ConversationActions(Parcel in) {
+ mConversationActions =
+ Collections.unmodifiableList(in.createTypedArrayList(ConversationAction.CREATOR));
+ mId = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeTypedList(mConversationActions);
+ parcel.writeString(mId);
+ }
+
+ /**
+ * Returns an immutable list of {@link ConversationAction} objects, which are ordered from high
+ * confidence to low confidence.
+ */
+ @NonNull
+ public List<ConversationAction> getConversationActions() {
+ return mConversationActions;
+ }
+
+ /**
+ * Returns the id, if one exists, for this object.
+ */
+ @Nullable
+ public String getId() {
+ return mId;
+ }
+
+ /** Represents a message in the conversation. */
+ public static final class Message implements Parcelable {
+ /**
+ * Represents the local user.
+ *
+ * @see Builder#Builder(Person)
+ */
+ @NonNull
+ public static final Person PERSON_USER_SELF =
+ new Person.Builder()
+ .setKey("text-classifier-conversation-actions-user-self")
+ .build();
+
+ /**
+ * Represents the remote user.
+ * <p>
+ * If possible, you are suggested to create a {@link Person} object that can identify
+ * the remote user better, so that the underlying model could differentiate between
+ * different remote users.
+ *
+ * @see Builder#Builder(Person)
+ */
+ @NonNull
+ public static final Person PERSON_USER_OTHERS =
+ new Person.Builder()
+ .setKey("text-classifier-conversation-actions-user-others")
+ .build();
+
+ @Nullable
+ private final Person mAuthor;
+ @Nullable
+ private final ZonedDateTime mReferenceTime;
+ @Nullable
+ private final CharSequence mText;
+ @NonNull
+ private final Bundle mExtras;
+
+ private Message(
+ @Nullable Person author,
+ @Nullable ZonedDateTime referenceTime,
+ @Nullable CharSequence text,
+ @NonNull Bundle bundle) {
+ mAuthor = author;
+ mReferenceTime = referenceTime;
+ mText = text;
+ mExtras = Objects.requireNonNull(bundle);
+ }
+
+ private Message(Parcel in) {
+ mAuthor = in.readParcelable(null);
+ mReferenceTime =
+ in.readInt() == 0
+ ? null
+ : ZonedDateTime.parse(
+ in.readString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);
+ mText = in.readCharSequence();
+ mExtras = in.readBundle();
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mAuthor, flags);
+ parcel.writeInt(mReferenceTime != null ? 1 : 0);
+ if (mReferenceTime != null) {
+ parcel.writeString(mReferenceTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+ }
+ parcel.writeCharSequence(mText);
+ parcel.writeBundle(mExtras);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Creator<Message> CREATOR =
+ new Creator<Message>() {
+ @Override
+ public Message createFromParcel(Parcel in) {
+ return new Message(in);
+ }
+
+ @Override
+ public Message[] newArray(int size) {
+ return new Message[size];
+ }
+ };
+
+ /** Returns the person that composed the message. */
+ @NonNull
+ public Person getAuthor() {
+ return mAuthor;
+ }
+
+ /**
+ * Returns the reference time of the message, for example it could be the compose or send
+ * time of this message.
+ */
+ @Nullable
+ public ZonedDateTime getReferenceTime() {
+ return mReferenceTime;
+ }
+
+ /** Returns the text of the message. */
+ @Nullable
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the extended data related to this conversation action.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** Builder class to construct a {@link Message} */
+ public static final class Builder {
+ @Nullable
+ private Person mAuthor;
+ @Nullable
+ private ZonedDateTime mReferenceTime;
+ @Nullable
+ private CharSequence mText;
+ @Nullable
+ private Bundle mExtras;
+
+ /**
+ * Constructs a builder.
+ *
+ * @param author the person that composed the message, use {@link #PERSON_USER_SELF}
+ * to represent the local user. If it is not possible to identify the
+ * remote user that the local user is conversing with, use
+ * {@link #PERSON_USER_OTHERS} to represent a remote user.
+ */
+ public Builder(@NonNull Person author) {
+ mAuthor = Objects.requireNonNull(author);
+ }
+
+ /** Sets the text of this message. */
+ @NonNull
+ public Builder setText(@Nullable CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets the reference time of this message, for example it could be the compose or send
+ * time of this message.
+ */
+ @NonNull
+ public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
+ mReferenceTime = referenceTime;
+ return this;
+ }
+
+ /** Sets a set of extended data to the message. */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle bundle) {
+ this.mExtras = bundle;
+ return this;
+ }
+
+ /** Builds the {@link Message} object. */
+ @NonNull
+ public Message build() {
+ return new Message(
+ mAuthor,
+ mReferenceTime,
+ mText == null ? null : new SpannedString(mText),
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+ }
+
+ /**
+ * A request object for generating conversation action suggestions.
+ *
+ * @see TextClassifier#suggestConversationActions(Request)
+ */
+ public static final class Request implements Parcelable {
+
+ /** @hide */
+ @Retention(SOURCE)
+ @StringDef(
+ value = {
+ HINT_FOR_NOTIFICATION,
+ HINT_FOR_IN_APP,
+ },
+ prefix = "HINT_")
+ public @interface Hint {}
+
+ /**
+ * To indicate the generated actions will be used within the app.
+ */
+ public static final String HINT_FOR_IN_APP = "in_app";
+ /**
+ * To indicate the generated actions will be used for notification.
+ */
+ public static final String HINT_FOR_NOTIFICATION = "notification";
+
+ @NonNull
+ private final List<Message> mConversation;
+ @NonNull
+ private final TextClassifier.EntityConfig mTypeConfig;
+ private final int mMaxSuggestions;
+ @NonNull
+ @Hint
+ private final List<String> mHints;
+ @NonNull
+ private Bundle mExtras;
+ @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ private Request(
+ @NonNull List<Message> conversation,
+ @NonNull TextClassifier.EntityConfig typeConfig,
+ int maxSuggestions,
+ @Nullable @Hint List<String> hints,
+ @NonNull Bundle extras) {
+ mConversation = Objects.requireNonNull(conversation);
+ mTypeConfig = Objects.requireNonNull(typeConfig);
+ mMaxSuggestions = maxSuggestions;
+ mHints = hints;
+ mExtras = extras;
+ }
+
+ private static Request readFromParcel(Parcel in) {
+ List<Message> conversation = new ArrayList<>();
+ in.readParcelableList(conversation, null);
+ TextClassifier.EntityConfig typeConfig = in.readParcelable(null);
+ int maxSuggestions = in.readInt();
+ List<String> hints = new ArrayList<>();
+ in.readStringList(hints);
+ Bundle extras = in.readBundle();
+ SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null);
+
+ Request request = new Request(
+ conversation,
+ typeConfig,
+ maxSuggestions,
+ hints,
+ extras);
+ request.setSystemTextClassifierMetadata(systemTcMetadata);
+ return request;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelableList(mConversation, flags);
+ parcel.writeParcelable(mTypeConfig, flags);
+ parcel.writeInt(mMaxSuggestions);
+ parcel.writeStringList(mHints);
+ parcel.writeBundle(mExtras);
+ parcel.writeParcelable(mSystemTcMetadata, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Creator<Request> CREATOR =
+ new Creator<Request>() {
+ @Override
+ public Request createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public Request[] newArray(int size) {
+ return new Request[size];
+ }
+ };
+
+ /** Returns the type config. */
+ @NonNull
+ public TextClassifier.EntityConfig getTypeConfig() {
+ return mTypeConfig;
+ }
+
+ /** Returns an immutable list of messages that make up the conversation. */
+ @NonNull
+ public List<Message> getConversation() {
+ return mConversation;
+ }
+
+ /**
+ * Return the maximal number of suggestions the caller wants, value -1 means no restriction
+ * and this is the default.
+ */
+ @IntRange(from = -1)
+ public int getMaxSuggestions() {
+ return mMaxSuggestions;
+ }
+
+ /** Returns an immutable list of hints */
+ @NonNull
+ @Hint
+ public List<String> getHints() {
+ return mHints;
+ }
+
+ /**
+ * Returns the name of the package that sent this request.
+ * This returns {@code null} if no calling package name is set.
+ */
+ @Nullable
+ public String getCallingPackageName() {
+ return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ void setSystemTextClassifierMetadata(@Nullable SystemTextClassifierMetadata systemTcData) {
+ mSystemTcMetadata = systemTcData;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns the extended data related to this request.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** Builder object to construct the {@link Request} object. */
+ public static final class Builder {
+ @NonNull
+ private List<Message> mConversation;
+ @Nullable
+ private TextClassifier.EntityConfig mTypeConfig;
+ private int mMaxSuggestions = -1;
+ @Nullable
+ @Hint
+ private List<String> mHints;
+ @Nullable
+ private Bundle mExtras;
+
+ /**
+ * Constructs a builder.
+ *
+ * @param conversation the conversation that the text classifier is going to generate
+ * actions for.
+ */
+ public Builder(@NonNull List<Message> conversation) {
+ mConversation = Objects.requireNonNull(conversation);
+ }
+
+ /**
+ * Sets the hints to help text classifier to generate actions. It could be used to help
+ * text classifier to infer what types of actions the caller may be interested in.
+ */
+ @NonNull
+ public Builder setHints(@Nullable @Hint List<String> hints) {
+ mHints = hints;
+ return this;
+ }
+
+ /** Sets the type config. */
+ @NonNull
+ public Builder setTypeConfig(@Nullable TextClassifier.EntityConfig typeConfig) {
+ mTypeConfig = typeConfig;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of suggestions you want. Value -1 means no restriction and
+ * this is the default.
+ */
+ @NonNull
+ public Builder setMaxSuggestions(@IntRange(from = -1) int maxSuggestions) {
+ if (maxSuggestions < -1) {
+ throw new IllegalArgumentException("maxSuggestions has to be greater than or "
+ + "equal to -1.");
+ }
+ mMaxSuggestions = maxSuggestions;
+ return this;
+ }
+
+ /** Sets a set of extended data to the request. */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle bundle) {
+ mExtras = bundle;
+ return this;
+ }
+
+ /** Builds the {@link Request} object. */
+ @NonNull
+ public Request build() {
+ return new Request(
+ Collections.unmodifiableList(mConversation),
+ mTypeConfig == null
+ ? new TextClassifier.EntityConfig.Builder().build()
+ : mTypeConfig,
+ mMaxSuggestions,
+ mHints == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(mHints),
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+ }
+}
diff --git a/android/view/textclassifier/EntityConfidence.java b/android/view/textclassifier/EntityConfidence.java
new file mode 100644
index 0000000..b4313b7
--- /dev/null
+++ b/android/view/textclassifier/EntityConfidence.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Helper object for setting and getting entity scores for classified text.
+ *
+ * @hide
+ */
+final class EntityConfidence implements Parcelable {
+
+ private final ArrayMap<String, Float> mEntityConfidence = new ArrayMap<>();
+ private final ArrayList<String> mSortedEntities = new ArrayList<>();
+
+ EntityConfidence() {}
+
+ EntityConfidence(@NonNull EntityConfidence source) {
+ Objects.requireNonNull(source);
+ mEntityConfidence.putAll(source.mEntityConfidence);
+ mSortedEntities.addAll(source.mSortedEntities);
+ }
+
+ /**
+ * Constructs an EntityConfidence from a map of entity to confidence.
+ *
+ * Map entries that have 0 confidence are removed, and values greater than 1 are clamped to 1.
+ *
+ * @param source a map from entity to a confidence value in the range 0 (low confidence) to
+ * 1 (high confidence).
+ */
+ EntityConfidence(@NonNull Map<String, Float> source) {
+ Objects.requireNonNull(source);
+
+ // Prune non-existent entities and clamp to 1.
+ mEntityConfidence.ensureCapacity(source.size());
+ for (Map.Entry<String, Float> it : source.entrySet()) {
+ if (it.getValue() <= 0) continue;
+ mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue()));
+ }
+ resetSortedEntitiesFromMap();
+ }
+
+ /**
+ * Returns an immutable list of entities found in the classified text ordered from
+ * high confidence to low confidence.
+ */
+ @NonNull
+ public List<String> getEntities() {
+ return Collections.unmodifiableList(mSortedEntities);
+ }
+
+ /**
+ * Returns the confidence score for the specified entity. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
+ * classified text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(String entity) {
+ if (mEntityConfidence.containsKey(entity)) {
+ return mEntityConfidence.get(entity);
+ }
+ return 0;
+ }
+
+ public Map<String, Float> toMap() {
+ return new ArrayMap(mEntityConfidence);
+ }
+
+ @Override
+ public String toString() {
+ return mEntityConfidence.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityConfidence.size());
+ for (Map.Entry<String, Float> entry : mEntityConfidence.entrySet()) {
+ dest.writeString(entry.getKey());
+ dest.writeFloat(entry.getValue());
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<EntityConfidence> CREATOR =
+ new Parcelable.Creator<EntityConfidence>() {
+ @Override
+ public EntityConfidence createFromParcel(Parcel in) {
+ return new EntityConfidence(in);
+ }
+
+ @Override
+ public EntityConfidence[] newArray(int size) {
+ return new EntityConfidence[size];
+ }
+ };
+
+ private EntityConfidence(Parcel in) {
+ final int numEntities = in.readInt();
+ mEntityConfidence.ensureCapacity(numEntities);
+ for (int i = 0; i < numEntities; ++i) {
+ mEntityConfidence.put(in.readString(), in.readFloat());
+ }
+ resetSortedEntitiesFromMap();
+ }
+
+ private void resetSortedEntitiesFromMap() {
+ mSortedEntities.clear();
+ mSortedEntities.ensureCapacity(mEntityConfidence.size());
+ mSortedEntities.addAll(mEntityConfidence.keySet());
+ mSortedEntities.sort((e1, e2) -> {
+ float score1 = mEntityConfidence.get(e1);
+ float score2 = mEntityConfidence.get(e2);
+ return Float.compare(score2, score1);
+ });
+ }
+}
diff --git a/android/view/textclassifier/ExtrasUtils.java b/android/view/textclassifier/ExtrasUtils.java
new file mode 100644
index 0000000..9e2b642
--- /dev/null
+++ b/android/view/textclassifier/ExtrasUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.Nullable;
+import android.app.RemoteAction;
+import android.content.Intent;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class for inserting and retrieving data in TextClassifier request/response extras.
+ * @hide
+ */
+// TODO: Make this a TestApi for CTS testing.
+public final class ExtrasUtils {
+
+ // Keys for response objects.
+ private static final String ACTION_INTENT = "action-intent";
+ private static final String ACTIONS_INTENTS = "actions-intents";
+ private static final String FOREIGN_LANGUAGE = "foreign-language";
+ private static final String ENTITY_TYPE = "entity-type";
+ private static final String SCORE = "score";
+ private static final String MODEL_NAME = "model-name";
+
+ private ExtrasUtils() {
+ }
+
+ /**
+ * Returns foreign language detection information contained in the TextClassification object.
+ * responses.
+ */
+ @Nullable
+ public static Bundle getForeignLanguageExtra(@Nullable TextClassification classification) {
+ if (classification == null) {
+ return null;
+ }
+ return classification.getExtras().getBundle(FOREIGN_LANGUAGE);
+ }
+
+ /**
+ * Returns {@code actionIntent} information contained in a TextClassifier response object.
+ */
+ @Nullable
+ public static Intent getActionIntent(Bundle container) {
+ return container.getParcelable(ACTION_INTENT);
+ }
+
+ /**
+ * Returns {@code actionIntents} information contained in the TextClassification object.
+ */
+ @Nullable
+ public static ArrayList<Intent> getActionsIntents(@Nullable TextClassification classification) {
+ if (classification == null) {
+ return null;
+ }
+ return classification.getExtras().getParcelableArrayList(ACTIONS_INTENTS);
+ }
+
+ /**
+ * Returns the first action found in the {@code classification} object with an intent
+ * action string, {@code intentAction}.
+ */
+ @Nullable
+ private static RemoteAction findAction(
+ @Nullable TextClassification classification, @Nullable String intentAction) {
+ if (classification == null || intentAction == null) {
+ return null;
+ }
+ final ArrayList<Intent> actionIntents = getActionsIntents(classification);
+ if (actionIntents != null) {
+ final int size = actionIntents.size();
+ for (int i = 0; i < size; i++) {
+ final Intent intent = actionIntents.get(i);
+ if (intent != null && intentAction.equals(intent.getAction())) {
+ return classification.getActions().get(i);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first "translate" action found in the {@code classification} object.
+ */
+ @Nullable
+ public static RemoteAction findTranslateAction(@Nullable TextClassification classification) {
+ return findAction(classification, Intent.ACTION_TRANSLATE);
+ }
+
+ /**
+ * Returns the entity type contained in the {@code extra}.
+ */
+ @Nullable
+ public static String getEntityType(@Nullable Bundle extra) {
+ if (extra == null) {
+ return null;
+ }
+ return extra.getString(ENTITY_TYPE);
+ }
+
+ /**
+ * Returns the score contained in the {@code extra}.
+ */
+ @Nullable
+ public static float getScore(Bundle extra) {
+ final int defaultValue = -1;
+ if (extra == null) {
+ return defaultValue;
+ }
+ return extra.getFloat(SCORE, defaultValue);
+ }
+
+ /**
+ * Returns the model name contained in the {@code extra}.
+ */
+ @Nullable
+ public static String getModelName(@Nullable Bundle extra) {
+ if (extra == null) {
+ return null;
+ }
+ return extra.getString(MODEL_NAME);
+ }
+}
\ No newline at end of file
diff --git a/android/view/textclassifier/Log.java b/android/view/textclassifier/Log.java
new file mode 100644
index 0000000..98ee09c
--- /dev/null
+++ b/android/view/textclassifier/Log.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+/**
+ * Logging for android.view.textclassifier package.
+ * <p>
+ * To enable full log:
+ * 1. adb shell setprop log.tag.androidtc VERBOSE
+ * 2. adb shell stop && adb shell start
+ *
+ * @hide
+ */
+public final class Log {
+
+ /**
+ * true: Enables full logging.
+ * false: Limits logging to debug level.
+ */
+ static final boolean ENABLE_FULL_LOGGING =
+ android.util.Log.isLoggable(TextClassifier.LOG_TAG, android.util.Log.VERBOSE);
+
+ private Log() {
+ }
+
+ public static void v(String tag, String msg) {
+ if (ENABLE_FULL_LOGGING) {
+ android.util.Log.v(tag, msg);
+ }
+ }
+
+ public static void d(String tag, String msg) {
+ android.util.Log.d(tag, msg);
+ }
+
+ public static void w(String tag, String msg) {
+ android.util.Log.w(tag, msg);
+ }
+
+ public static void e(String tag, String msg, Throwable tr) {
+ if (ENABLE_FULL_LOGGING) {
+ android.util.Log.e(tag, msg, tr);
+ } else {
+ final String trString = (tr != null) ? tr.getClass().getSimpleName() : "??";
+ android.util.Log.d(tag, String.format("%s (%s)", msg, trString));
+ }
+ }
+}
diff --git a/android/view/textclassifier/SelectionEvent.java b/android/view/textclassifier/SelectionEvent.java
new file mode 100644
index 0000000..858825b
--- /dev/null
+++ b/android/view/textclassifier/SelectionEvent.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.textclassifier.TextClassifier.EntityType;
+import android.view.textclassifier.TextClassifier.WidgetType;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * A selection event.
+ * Specify index parameters as word token indices.
+ */
+public final class SelectionEvent implements Parcelable {
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
+ ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
+ ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET})
+ // NOTE: ActionType values should not be lower than 100 to avoid colliding with the other
+ // EventTypes declared below.
+ public @interface ActionType {
+ /*
+ * Terminal event types range: [100,200).
+ * Non-terminal event types range: [200,300).
+ */
+ }
+
+ /** User typed over the selection. */
+ public static final int ACTION_OVERTYPE = 100;
+ /** User copied the selection. */
+ public static final int ACTION_COPY = 101;
+ /** User pasted over the selection. */
+ public static final int ACTION_PASTE = 102;
+ /** User cut the selection. */
+ public static final int ACTION_CUT = 103;
+ /** User shared the selection. */
+ public static final int ACTION_SHARE = 104;
+ /** User clicked the textAssist menu item. */
+ public static final int ACTION_SMART_SHARE = 105;
+ /** User dragged+dropped the selection. */
+ public static final int ACTION_DRAG = 106;
+ /** User abandoned the selection. */
+ public static final int ACTION_ABANDON = 107;
+ /** User performed an action on the selection. */
+ public static final int ACTION_OTHER = 108;
+
+ // Non-terminal actions.
+ /** User activated Select All */
+ public static final int ACTION_SELECT_ALL = 200;
+ /** User reset the smart selection. */
+ public static final int ACTION_RESET = 201;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
+ ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
+ ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET,
+ EVENT_SELECTION_STARTED, EVENT_SELECTION_MODIFIED,
+ EVENT_SMART_SELECTION_SINGLE, EVENT_SMART_SELECTION_MULTI,
+ EVENT_AUTO_SELECTION})
+ // NOTE: EventTypes declared here must be less than 100 to avoid colliding with the
+ // ActionTypes declared above.
+ public @interface EventType {
+ /*
+ * Range: 1 -> 99.
+ */
+ }
+
+ /** User started a new selection. */
+ public static final int EVENT_SELECTION_STARTED = 1;
+ /** User modified an existing selection. */
+ public static final int EVENT_SELECTION_MODIFIED = 2;
+ /** Smart selection triggered for a single token (word). */
+ public static final int EVENT_SMART_SELECTION_SINGLE = 3;
+ /** Smart selection triggered spanning multiple tokens (words). */
+ public static final int EVENT_SMART_SELECTION_MULTI = 4;
+ /** Something else other than User or the default TextClassifier triggered a selection. */
+ public static final int EVENT_AUTO_SELECTION = 5;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({INVOCATION_MANUAL, INVOCATION_LINK, INVOCATION_UNKNOWN})
+ public @interface InvocationMethod {}
+
+ /** Selection was invoked by the user long pressing, double tapping, or dragging to select. */
+ public static final int INVOCATION_MANUAL = 1;
+ /** Selection was invoked by the user tapping on a link. */
+ public static final int INVOCATION_LINK = 2;
+ /** Unknown invocation method */
+ public static final int INVOCATION_UNKNOWN = 0;
+
+ static final String NO_SIGNATURE = "";
+
+ private final int mAbsoluteStart;
+ private final int mAbsoluteEnd;
+
+ private @EntityType String mEntityType;
+ private @EventType int mEventType;
+ private String mPackageName = "";
+ private String mWidgetType = TextClassifier.WIDGET_TYPE_UNKNOWN;
+ private @InvocationMethod int mInvocationMethod;
+ @Nullable private String mWidgetVersion;
+ @Nullable private String mResultId;
+ private long mEventTime;
+ private long mDurationSinceSessionStart;
+ private long mDurationSincePreviousEvent;
+ private int mEventIndex;
+ @Nullable private TextClassificationSessionId mSessionId;
+ private int mStart;
+ private int mEnd;
+ private int mSmartStart;
+ private int mSmartEnd;
+ @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ SelectionEvent(
+ int start, int end,
+ @EventType int eventType, @EntityType String entityType,
+ @InvocationMethod int invocationMethod, @Nullable String resultId) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ mAbsoluteStart = start;
+ mAbsoluteEnd = end;
+ mEventType = eventType;
+ mEntityType = Objects.requireNonNull(entityType);
+ mResultId = resultId;
+ mInvocationMethod = invocationMethod;
+ }
+
+ private SelectionEvent(Parcel in) {
+ mAbsoluteStart = in.readInt();
+ mAbsoluteEnd = in.readInt();
+ mEventType = in.readInt();
+ mEntityType = in.readString();
+ mWidgetVersion = in.readInt() > 0 ? in.readString() : null;
+ mPackageName = in.readString();
+ mWidgetType = in.readString();
+ mInvocationMethod = in.readInt();
+ mResultId = in.readString();
+ mEventTime = in.readLong();
+ mDurationSinceSessionStart = in.readLong();
+ mDurationSincePreviousEvent = in.readLong();
+ mEventIndex = in.readInt();
+ mSessionId = in.readInt() > 0
+ ? TextClassificationSessionId.CREATOR.createFromParcel(in) : null;
+ mStart = in.readInt();
+ mEnd = in.readInt();
+ mSmartStart = in.readInt();
+ mSmartEnd = in.readInt();
+ mSystemTcMetadata = in.readParcelable(null);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mAbsoluteStart);
+ dest.writeInt(mAbsoluteEnd);
+ dest.writeInt(mEventType);
+ dest.writeString(mEntityType);
+ dest.writeInt(mWidgetVersion != null ? 1 : 0);
+ if (mWidgetVersion != null) {
+ dest.writeString(mWidgetVersion);
+ }
+ dest.writeString(mPackageName);
+ dest.writeString(mWidgetType);
+ dest.writeInt(mInvocationMethod);
+ dest.writeString(mResultId);
+ dest.writeLong(mEventTime);
+ dest.writeLong(mDurationSinceSessionStart);
+ dest.writeLong(mDurationSincePreviousEvent);
+ dest.writeInt(mEventIndex);
+ dest.writeInt(mSessionId != null ? 1 : 0);
+ if (mSessionId != null) {
+ mSessionId.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mStart);
+ dest.writeInt(mEnd);
+ dest.writeInt(mSmartStart);
+ dest.writeInt(mSmartEnd);
+ dest.writeParcelable(mSystemTcMetadata, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Creates a "selection started" event.
+ *
+ * @param invocationMethod the way the selection was triggered
+ * @param start the index of the selected text
+ */
+ @NonNull
+ public static SelectionEvent createSelectionStartedEvent(
+ @SelectionEvent.InvocationMethod int invocationMethod, int start) {
+ return new SelectionEvent(
+ start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
+ TextClassifier.TYPE_UNKNOWN, invocationMethod, NO_SIGNATURE);
+ }
+
+ /**
+ * Creates a "selection modified" event.
+ * Use when the user modifies the selection.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionModifiedEvent(int start, int end) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ return new SelectionEvent(
+ start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
+ TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN, NO_SIGNATURE);
+ }
+
+ /**
+ * Creates a "selection modified" event.
+ * Use when the user modifies the selection and the selection's entity type is known.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param classification the TextClassification object returned by the TextClassifier that
+ * classified the selected text
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionModifiedEvent(
+ int start, int end, @NonNull TextClassification classification) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Objects.requireNonNull(classification);
+ final String entityType = classification.getEntityCount() > 0
+ ? classification.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ return new SelectionEvent(
+ start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
+ entityType, INVOCATION_UNKNOWN, classification.getId());
+ }
+
+ /**
+ * Creates a "selection modified" event.
+ * Use when a TextClassifier modifies the selection.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param selection the TextSelection object returned by the TextClassifier for the
+ * specified selection
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionModifiedEvent(
+ int start, int end, @NonNull TextSelection selection) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Objects.requireNonNull(selection);
+ final String entityType = selection.getEntityCount() > 0
+ ? selection.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ return new SelectionEvent(
+ start, end, SelectionEvent.EVENT_AUTO_SELECTION,
+ entityType, INVOCATION_UNKNOWN, selection.getId());
+ }
+
+ /**
+ * Creates an event specifying an action taken on a selection.
+ * Use when the user clicks on an action to act on the selected text.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param actionType the action that was performed on the selection
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionActionEvent(
+ int start, int end, @SelectionEvent.ActionType int actionType) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ checkActionType(actionType);
+ return new SelectionEvent(
+ start, end, actionType, TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN,
+ NO_SIGNATURE);
+ }
+
+ /**
+ * Creates an event specifying an action taken on a selection.
+ * Use when the user clicks on an action to act on the selected text and the selection's
+ * entity type is known.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param actionType the action that was performed on the selection
+ * @param classification the TextClassification object returned by the TextClassifier that
+ * classified the selected text
+ *
+ * @throws IllegalArgumentException if end is less than start
+ * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
+ */
+ @NonNull
+ public static SelectionEvent createSelectionActionEvent(
+ int start, int end, @SelectionEvent.ActionType int actionType,
+ @NonNull TextClassification classification) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Objects.requireNonNull(classification);
+ checkActionType(actionType);
+ final String entityType = classification.getEntityCount() > 0
+ ? classification.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ return new SelectionEvent(start, end, actionType, entityType, INVOCATION_UNKNOWN,
+ classification.getId());
+ }
+
+ /**
+ * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
+ */
+ private static void checkActionType(@SelectionEvent.EventType int eventType)
+ throws IllegalArgumentException {
+ switch (eventType) {
+ case SelectionEvent.ACTION_OVERTYPE: // fall through
+ case SelectionEvent.ACTION_COPY: // fall through
+ case SelectionEvent.ACTION_PASTE: // fall through
+ case SelectionEvent.ACTION_CUT: // fall through
+ case SelectionEvent.ACTION_SHARE: // fall through
+ case SelectionEvent.ACTION_SMART_SHARE: // fall through
+ case SelectionEvent.ACTION_DRAG: // fall through
+ case SelectionEvent.ACTION_ABANDON: // fall through
+ case SelectionEvent.ACTION_SELECT_ALL: // fall through
+ case SelectionEvent.ACTION_RESET: // fall through
+ case SelectionEvent.ACTION_OTHER: // fall through
+ return;
+ default:
+ throw new IllegalArgumentException(
+ String.format(Locale.US, "%d is not an eventType", eventType));
+ }
+ }
+
+ int getAbsoluteStart() {
+ return mAbsoluteStart;
+ }
+
+ int getAbsoluteEnd() {
+ return mAbsoluteEnd;
+ }
+
+ /**
+ * Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}.
+ */
+ @EventType
+ public int getEventType() {
+ return mEventType;
+ }
+
+ /**
+ * Sets the event type.
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setEventType(@EventType int eventType) {
+ mEventType = eventType;
+ }
+
+ /**
+ * Returns the type of entity that is associated with this event. e.g.
+ * {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}.
+ */
+ @EntityType
+ @NonNull
+ public String getEntityType() {
+ return mEntityType;
+ }
+
+ void setEntityType(@EntityType String entityType) {
+ mEntityType = Objects.requireNonNull(entityType);
+ }
+
+ /**
+ * Returns the package name of the app that this event originated in.
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ void setSystemTextClassifierMetadata(@Nullable SystemTextClassifierMetadata systemTcMetadata) {
+ mSystemTcMetadata = systemTcMetadata;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns the type of widget that was involved in triggering this event.
+ */
+ @WidgetType
+ @NonNull
+ public String getWidgetType() {
+ return mWidgetType;
+ }
+
+ /**
+ * Returns a string version info for the widget this event was triggered in.
+ */
+ @Nullable
+ public String getWidgetVersion() {
+ return mWidgetVersion;
+ }
+
+ /**
+ * Sets the {@link TextClassificationContext} for this event.
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setTextClassificationSessionContext(TextClassificationContext context) {
+ mPackageName = context.getPackageName();
+ mWidgetType = context.getWidgetType();
+ mWidgetVersion = context.getWidgetVersion();
+ mSystemTcMetadata = context.getSystemTextClassifierMetadata();
+ }
+
+ /**
+ * Returns the way the selection mode was invoked.
+ */
+ public @InvocationMethod int getInvocationMethod() {
+ return mInvocationMethod;
+ }
+
+ /**
+ * Sets the invocationMethod for this event.
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setInvocationMethod(@InvocationMethod int invocationMethod) {
+ mInvocationMethod = invocationMethod;
+ }
+
+ /**
+ * Returns the id of the text classifier result associated with this event.
+ */
+ @Nullable
+ public String getResultId() {
+ return mResultId;
+ }
+
+ SelectionEvent setResultId(@Nullable String resultId) {
+ mResultId = resultId;
+ return this;
+ }
+
+ /**
+ * Returns the time this event was triggered.
+ */
+ public long getEventTime() {
+ return mEventTime;
+ }
+
+ SelectionEvent setEventTime(long timeMs) {
+ mEventTime = timeMs;
+ return this;
+ }
+
+ /**
+ * Returns the duration in ms between when this event was triggered and when the first event in
+ * the selection session was triggered.
+ */
+ public long getDurationSinceSessionStart() {
+ return mDurationSinceSessionStart;
+ }
+
+ SelectionEvent setDurationSinceSessionStart(long durationMs) {
+ mDurationSinceSessionStart = durationMs;
+ return this;
+ }
+
+ /**
+ * Returns the duration in ms between when this event was triggered and when the previous event
+ * in the selection session was triggered.
+ */
+ public long getDurationSincePreviousEvent() {
+ return mDurationSincePreviousEvent;
+ }
+
+ SelectionEvent setDurationSincePreviousEvent(long durationMs) {
+ this.mDurationSincePreviousEvent = durationMs;
+ return this;
+ }
+
+ /**
+ * Returns the index (e.g. 1st event, 2nd event, etc.) of this event in the selection session.
+ */
+ public int getEventIndex() {
+ return mEventIndex;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public SelectionEvent setEventIndex(int index) {
+ mEventIndex = index;
+ return this;
+ }
+
+ /**
+ * Returns the selection session id.
+ */
+ @Nullable
+ public TextClassificationSessionId getSessionId() {
+ return mSessionId;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public SelectionEvent setSessionId(@Nullable TextClassificationSessionId id) {
+ mSessionId = id;
+ return this;
+ }
+
+ /**
+ * Returns the start index of this events relative to the index of the start selection
+ * event in the selection session.
+ */
+ public int getStart() {
+ return mStart;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public SelectionEvent setStart(int start) {
+ mStart = start;
+ return this;
+ }
+
+ /**
+ * Returns the end index of this events relative to the index of the start selection
+ * event in the selection session.
+ */
+ public int getEnd() {
+ return mEnd;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public SelectionEvent setEnd(int end) {
+ mEnd = end;
+ return this;
+ }
+
+ /**
+ * Returns the start index of this events relative to the index of the smart selection
+ * event in the selection session.
+ */
+ public int getSmartStart() {
+ return mSmartStart;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public SelectionEvent setSmartStart(int start) {
+ this.mSmartStart = start;
+ return this;
+ }
+
+ /**
+ * Returns the end index of this events relative to the index of the smart selection
+ * event in the selection session.
+ */
+ public int getSmartEnd() {
+ return mSmartEnd;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public SelectionEvent setSmartEnd(int end) {
+ mSmartEnd = end;
+ return this;
+ }
+
+ boolean isTerminal() {
+ return isTerminal(mEventType);
+ }
+
+ /**
+ * Returns true if the eventType is a terminal event type. Otherwise returns false.
+ * A terminal event is an event that ends a selection interaction.
+ */
+ public static boolean isTerminal(@EventType int eventType) {
+ switch (eventType) {
+ case ACTION_OVERTYPE: // fall through
+ case ACTION_COPY: // fall through
+ case ACTION_PASTE: // fall through
+ case ACTION_CUT: // fall through
+ case ACTION_SHARE: // fall through
+ case ACTION_SMART_SHARE: // fall through
+ case ACTION_DRAG: // fall through
+ case ACTION_ABANDON: // fall through
+ case ACTION_OTHER: // fall through
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
+ mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mResultId,
+ mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
+ mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd, mSystemTcMetadata);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SelectionEvent)) {
+ return false;
+ }
+
+ final SelectionEvent other = (SelectionEvent) obj;
+ return mAbsoluteStart == other.mAbsoluteStart
+ && mAbsoluteEnd == other.mAbsoluteEnd
+ && mEventType == other.mEventType
+ && Objects.equals(mEntityType, other.mEntityType)
+ && Objects.equals(mWidgetVersion, other.mWidgetVersion)
+ && Objects.equals(mPackageName, other.mPackageName)
+ && Objects.equals(mWidgetType, other.mWidgetType)
+ && mInvocationMethod == other.mInvocationMethod
+ && Objects.equals(mResultId, other.mResultId)
+ && mEventTime == other.mEventTime
+ && mDurationSinceSessionStart == other.mDurationSinceSessionStart
+ && mDurationSincePreviousEvent == other.mDurationSincePreviousEvent
+ && mEventIndex == other.mEventIndex
+ && Objects.equals(mSessionId, other.mSessionId)
+ && mStart == other.mStart
+ && mEnd == other.mEnd
+ && mSmartStart == other.mSmartStart
+ && mSmartEnd == other.mSmartEnd
+ && mSystemTcMetadata == other.mSystemTcMetadata;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, "
+ + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, "
+ + "resultId=%s, eventTime=%d, durationSinceSessionStart=%d, "
+ + "durationSincePreviousEvent=%d, eventIndex=%d,"
+ + "sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d, "
+ + "systemTcMetadata=%s}",
+ mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
+ mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod,
+ mResultId, mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
+ mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd, mSystemTcMetadata);
+ }
+
+ public static final @android.annotation.NonNull Creator<SelectionEvent> CREATOR = new Creator<SelectionEvent>() {
+ @Override
+ public SelectionEvent createFromParcel(Parcel in) {
+ return new SelectionEvent(in);
+ }
+
+ @Override
+ public SelectionEvent[] newArray(int size) {
+ return new SelectionEvent[size];
+ }
+ };
+}
diff --git a/android/view/textclassifier/SelectionSessionLogger.java b/android/view/textclassifier/SelectionSessionLogger.java
new file mode 100644
index 0000000..e7d896e
--- /dev/null
+++ b/android/view/textclassifier/SelectionSessionLogger.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * A helper for logging selection session events.
+ *
+ * @hide
+ */
+public final class SelectionSessionLogger {
+ // Keep this in sync with the ResultIdUtils in libtextclassifier.
+ private static final String CLASSIFIER_ID = "androidtc";
+
+ static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
+ return SelectionSessionLogger.CLASSIFIER_ID.equals(
+ SelectionSessionLogger.SignatureParser.getClassifierId(signature));
+ }
+
+ /**
+ * Helper for creating and parsing string ids for
+ * {@link android.view.textclassifier.TextClassifierImpl}.
+ */
+ @VisibleForTesting
+ public static final class SignatureParser {
+
+ static String getClassifierId(@Nullable String signature) {
+ if (signature == null) {
+ return "";
+ }
+ final int end = signature.indexOf("|");
+ if (end >= 0) {
+ return signature.substring(0, end);
+ }
+ return "";
+ }
+ }
+}
diff --git a/android/view/textclassifier/SystemTextClassifier.java b/android/view/textclassifier/SystemTextClassifier.java
new file mode 100644
index 0000000..2c844eb
--- /dev/null
+++ b/android/view/textclassifier/SystemTextClassifier.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.textclassifier.ITextClassifierCallback;
+import android.service.textclassifier.ITextClassifierService;
+import android.service.textclassifier.TextClassifierService;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * proxy to the request to TextClassifierService via the TextClassificationManagerService.
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = Visibility.PACKAGE)
+public final class SystemTextClassifier implements TextClassifier {
+
+ private static final String LOG_TAG = TextClassifier.LOG_TAG;
+
+ private final ITextClassifierService mManagerService;
+ private final TextClassificationConstants mSettings;
+ private final TextClassifier mFallback;
+ private TextClassificationSessionId mSessionId;
+ // NOTE: Always set this before sending a request to the manager service otherwise the
+ // manager service will throw a remote exception.
+ @NonNull
+ private final SystemTextClassifierMetadata mSystemTcMetadata;
+
+ /**
+ * Constructor of {@link SystemTextClassifier}
+ *
+ * @param context the context of the request.
+ * @param settings TextClassifier specific settings.
+ * @param useDefault whether to use the default text classifier to handle this request
+ */
+ public SystemTextClassifier(
+ Context context,
+ TextClassificationConstants settings,
+ boolean useDefault) throws ServiceManager.ServiceNotFoundException {
+ mManagerService = ITextClassifierService.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.TEXT_CLASSIFICATION_SERVICE));
+ mSettings = Objects.requireNonNull(settings);
+ mFallback = TextClassifier.NO_OP;
+ // NOTE: Always set this before sending a request to the manager service otherwise the
+ // manager service will throw a remote exception.
+ mSystemTcMetadata = new SystemTextClassifierMetadata(
+ Objects.requireNonNull(context.getOpPackageName()), context.getUserId(),
+ useDefault);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ @WorkerThread
+ public TextSelection suggestSelection(TextSelection.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ try {
+ request.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ final BlockingCallback<TextSelection> callback =
+ new BlockingCallback<>("textselection", mSettings);
+ mManagerService.onSuggestSelection(mSessionId, request, callback);
+ final TextSelection selection = callback.get();
+ if (selection != null) {
+ return selection;
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error suggesting selection for text. Using fallback.", e);
+ }
+ return mFallback.suggestSelection(request);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ @WorkerThread
+ public TextClassification classifyText(TextClassification.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ try {
+ request.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ final BlockingCallback<TextClassification> callback =
+ new BlockingCallback<>("textclassification", mSettings);
+ mManagerService.onClassifyText(mSessionId, request, callback);
+ final TextClassification classification = callback.get();
+ if (classification != null) {
+ return classification;
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error classifying text. Using fallback.", e);
+ }
+ return mFallback.classifyText(request);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ @WorkerThread
+ public TextLinks generateLinks(@NonNull TextLinks.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ if (!Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength())) {
+ return mFallback.generateLinks(request);
+ }
+ if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
+ return Utils.generateLegacyLinks(request);
+ }
+
+ try {
+ request.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ final BlockingCallback<TextLinks> callback =
+ new BlockingCallback<>("textlinks", mSettings);
+ mManagerService.onGenerateLinks(mSessionId, request, callback);
+ final TextLinks links = callback.get();
+ if (links != null) {
+ return links;
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error generating links. Using fallback.", e);
+ }
+ return mFallback.generateLinks(request);
+ }
+
+ @Override
+ public void onSelectionEvent(SelectionEvent event) {
+ Objects.requireNonNull(event);
+ Utils.checkMainThread();
+
+ try {
+ event.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ mManagerService.onSelectionEvent(mSessionId, event);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error reporting selection event.", e);
+ }
+ }
+
+ @Override
+ public void onTextClassifierEvent(@NonNull TextClassifierEvent event) {
+ Objects.requireNonNull(event);
+ Utils.checkMainThread();
+
+ try {
+ final TextClassificationContext tcContext =
+ event.getEventContext() == null ? new TextClassificationContext.Builder(
+ mSystemTcMetadata.getCallingPackageName(), WIDGET_TYPE_UNKNOWN).build()
+ : event.getEventContext();
+ tcContext.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ event.setEventContext(tcContext);
+ mManagerService.onTextClassifierEvent(mSessionId, event);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error reporting textclassifier event.", e);
+ }
+ }
+
+ @Override
+ public TextLanguage detectLanguage(TextLanguage.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+
+ try {
+ request.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ final BlockingCallback<TextLanguage> callback =
+ new BlockingCallback<>("textlanguage", mSettings);
+ mManagerService.onDetectLanguage(mSessionId, request, callback);
+ final TextLanguage textLanguage = callback.get();
+ if (textLanguage != null) {
+ return textLanguage;
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error detecting language.", e);
+ }
+ return mFallback.detectLanguage(request);
+ }
+
+ @Override
+ public ConversationActions suggestConversationActions(ConversationActions.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+
+ try {
+ request.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ final BlockingCallback<ConversationActions> callback =
+ new BlockingCallback<>("conversation-actions", mSettings);
+ mManagerService.onSuggestConversationActions(mSessionId, request, callback);
+ final ConversationActions conversationActions = callback.get();
+ if (conversationActions != null) {
+ return conversationActions;
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error reporting selection event.", e);
+ }
+ return mFallback.suggestConversationActions(request);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ @WorkerThread
+ public int getMaxGenerateLinksTextLength() {
+ // TODO: retrieve this from the bound service.
+ return mSettings.getGenerateLinksMaxTextLength();
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (mSessionId != null) {
+ mManagerService.onDestroyTextClassificationSession(mSessionId);
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error destroying classification session.", e);
+ }
+ }
+
+ @Override
+ public void dump(@NonNull IndentingPrintWriter printWriter) {
+ printWriter.println("SystemTextClassifier:");
+ printWriter.increaseIndent();
+ printWriter.printPair("mFallback", mFallback);
+ printWriter.printPair("mSessionId", mSessionId);
+ printWriter.printPair("mSystemTcMetadata", mSystemTcMetadata);
+ printWriter.decreaseIndent();
+ printWriter.println();
+ }
+
+ /**
+ * Attempts to initialize a new classification session.
+ *
+ * @param classificationContext the classification context
+ * @param sessionId the session's id
+ */
+ void initializeRemoteSession(
+ @NonNull TextClassificationContext classificationContext,
+ @NonNull TextClassificationSessionId sessionId) {
+ mSessionId = Objects.requireNonNull(sessionId);
+ try {
+ classificationContext.setSystemTextClassifierMetadata(mSystemTcMetadata);
+ mManagerService.onCreateTextClassificationSession(classificationContext, mSessionId);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error starting a new classification session.", e);
+ }
+ }
+
+ private static final class BlockingCallback<T extends Parcelable>
+ extends ITextClassifierCallback.Stub {
+ private final ResponseReceiver<T> mReceiver;
+
+ BlockingCallback(String name, TextClassificationConstants settings) {
+ mReceiver = new ResponseReceiver<>(name, settings);
+ }
+
+ @Override
+ public void onSuccess(Bundle result) {
+ mReceiver.onSuccess(TextClassifierService.getResponse(result));
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+
+ public T get() {
+ return mReceiver.get();
+ }
+
+ }
+
+ private static final class ResponseReceiver<T> {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private final String mName;
+ private final TextClassificationConstants mSettings;
+ private T mResponse;
+
+ private ResponseReceiver(String name, TextClassificationConstants settings) {
+ mName = name;
+ mSettings = settings;
+ }
+
+ public void onSuccess(T response) {
+ mResponse = response;
+ mLatch.countDown();
+ }
+
+ public void onFailure() {
+ Log.e(LOG_TAG, "Request failed at " + mName, null);
+ mLatch.countDown();
+ }
+
+ @Nullable
+ public T get() {
+ // If this is running on the main thread, do not block for a response.
+ // The response will unfortunately be null and the TextClassifier should depend on its
+ // fallback.
+ // NOTE that TextClassifier calls should preferably always be called on a worker thread.
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ try {
+ boolean success = mLatch.await(
+ mSettings.getSystemTextClassifierApiTimeoutInSecond(),
+ TimeUnit.SECONDS);
+ if (!success) {
+ Log.w(LOG_TAG, "Timeout in ResponseReceiver.get(): " + mName);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.e(LOG_TAG, "Interrupted during ResponseReceiver.get(): " + mName, e);
+ }
+ }
+ return mResponse;
+ }
+ }
+}
diff --git a/android/view/textclassifier/SystemTextClassifierMetadata.java b/android/view/textclassifier/SystemTextClassifierMetadata.java
new file mode 100644
index 0000000..971e3e2
--- /dev/null
+++ b/android/view/textclassifier/SystemTextClassifierMetadata.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * SystemTextClassifier specific information.
+ * <p>
+ * This contains information requires for the TextClassificationManagerService to process the
+ * requests from the application, e.g. user id, calling package name and etc. Centrialize the data
+ * into this class helps to extend the scalability if we want to add new fields.
+ * @hide
+ */
+@VisibleForTesting(visibility = Visibility.PACKAGE)
+public final class SystemTextClassifierMetadata implements Parcelable {
+
+ /* The name of the package that sent the TC request. */
+ @NonNull
+ private final String mCallingPackageName;
+ /* The id of the user that sent the TC request. */
+ @UserIdInt
+ private final int mUserId;
+ /* Whether to use the default text classifier to handle the request. */
+ private final boolean mUseDefaultTextClassifier;
+
+ public SystemTextClassifierMetadata(@NonNull String packageName, @UserIdInt int userId,
+ boolean useDefaultTextClassifier) {
+ Objects.requireNonNull(packageName);
+ mCallingPackageName = packageName;
+ mUserId = userId;
+ mUseDefaultTextClassifier = useDefaultTextClassifier;
+ }
+
+ /**
+ * Returns the id of the user that sent the TC request.
+ */
+ @UserIdInt
+ public int getUserId() {
+ return mUserId;
+ }
+
+ /**
+ * Returns the name of the package that sent the TC request.
+ * This returns {@code null} if no calling package name is set.
+ */
+ @NonNull
+ public String getCallingPackageName() {
+ return mCallingPackageName;
+ }
+
+ /**
+ * Returns whether to use the default text classifier to handle TC request.
+ */
+ public boolean useDefaultTextClassifier() {
+ return mUseDefaultTextClassifier;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "SystemTextClassifierMetadata {callingPackageName=%s, userId=%d, "
+ + "useDefaultTextClassifier=%b}",
+ mCallingPackageName, mUserId, mUseDefaultTextClassifier);
+ }
+
+ private static SystemTextClassifierMetadata readFromParcel(Parcel in) {
+ final String packageName = in.readString();
+ final int userId = in.readInt();
+ final boolean useDefaultTextClassifier = in.readBoolean();
+ return new SystemTextClassifierMetadata(packageName, userId, useDefaultTextClassifier);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mCallingPackageName);
+ dest.writeInt(mUserId);
+ dest.writeBoolean(mUseDefaultTextClassifier);
+ }
+
+ public static final @NonNull Creator<SystemTextClassifierMetadata> CREATOR =
+ new Creator<SystemTextClassifierMetadata>() {
+ @Override
+ public SystemTextClassifierMetadata createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public SystemTextClassifierMetadata[] newArray(int size) {
+ return new SystemTextClassifierMetadata[size];
+ }
+ };
+}
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
new file mode 100644
index 0000000..7db35d4
--- /dev/null
+++ b/android/view/textclassifier/TextClassification.java
@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.SpannedString;
+import android.util.ArrayMap;
+import android.view.View.OnClickListener;
+import android.view.textclassifier.TextClassifier.EntityType;
+import android.view.textclassifier.TextClassifier.Utils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Information for generating a widget to handle classified text.
+ *
+ * <p>A TextClassification object contains icons, labels, onClickListeners and intents that may
+ * be used to build a widget that can be used to act on classified text. There is the concept of a
+ * <i>primary action</i> and other <i>secondary actions</i>.
+ *
+ * <p>e.g. building a view that, when clicked, shares the classified text with the preferred app:
+ *
+ * <pre>{@code
+ * // Called preferably outside the UiThread.
+ * TextClassification classification = textClassifier.classifyText(allText, 10, 25);
+ *
+ * // Called on the UiThread.
+ * Button button = new Button(context);
+ * button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
+ * button.setText(classification.getLabel());
+ * button.setOnClickListener(v -> classification.getActions().get(0).getActionIntent().send());
+ * }</pre>
+ *
+ * <p>e.g. starting an action mode with menu items that can handle the classified text:
+ *
+ * <pre>{@code
+ * // Called preferably outside the UiThread.
+ * final TextClassification classification = textClassifier.classifyText(allText, 10, 25);
+ *
+ * // Called on the UiThread.
+ * view.startActionMode(new ActionMode.Callback() {
+ *
+ * public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ * for (int i = 0; i < classification.getActions().size(); ++i) {
+ * RemoteAction action = classification.getActions().get(i);
+ * menu.add(Menu.NONE, i, 20, action.getTitle())
+ * .setIcon(action.getIcon());
+ * }
+ * return true;
+ * }
+ *
+ * public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ * classification.getActions().get(item.getItemId()).getActionIntent().send();
+ * return true;
+ * }
+ *
+ * ...
+ * });
+ * }</pre>
+ */
+public final class TextClassification implements Parcelable {
+
+ /**
+ * @hide
+ */
+ public static final TextClassification EMPTY = new TextClassification.Builder().build();
+
+ private static final String LOG_TAG = "TextClassification";
+ // TODO(toki): investigate a way to derive this based on device properties.
+ private static final int MAX_LEGACY_ICON_SIZE = 192;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {IntentType.UNSUPPORTED, IntentType.ACTIVITY, IntentType.SERVICE})
+ private @interface IntentType {
+ int UNSUPPORTED = -1;
+ int ACTIVITY = 0;
+ int SERVICE = 1;
+ }
+
+ @NonNull private final String mText;
+ @Nullable private final Drawable mLegacyIcon;
+ @Nullable private final String mLegacyLabel;
+ @Nullable private final Intent mLegacyIntent;
+ @Nullable private final OnClickListener mLegacyOnClickListener;
+ @NonNull private final List<RemoteAction> mActions;
+ @NonNull private final EntityConfidence mEntityConfidence;
+ @Nullable private final String mId;
+ @NonNull private final Bundle mExtras;
+
+ private TextClassification(
+ @Nullable String text,
+ @Nullable Drawable legacyIcon,
+ @Nullable String legacyLabel,
+ @Nullable Intent legacyIntent,
+ @Nullable OnClickListener legacyOnClickListener,
+ @NonNull List<RemoteAction> actions,
+ @NonNull EntityConfidence entityConfidence,
+ @Nullable String id,
+ @NonNull Bundle extras) {
+ mText = text;
+ mLegacyIcon = legacyIcon;
+ mLegacyLabel = legacyLabel;
+ mLegacyIntent = legacyIntent;
+ mLegacyOnClickListener = legacyOnClickListener;
+ mActions = Collections.unmodifiableList(actions);
+ mEntityConfidence = Objects.requireNonNull(entityConfidence);
+ mId = id;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the classified text.
+ */
+ @Nullable
+ public String getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the number of entities found in the classified text.
+ */
+ @IntRange(from = 0)
+ public int getEntityCount() {
+ return mEntityConfidence.getEntities().size();
+ }
+
+ /**
+ * Returns the entity at the specified index. Entities are ordered from high confidence
+ * to low confidence.
+ *
+ * @throws IndexOutOfBoundsException if the specified index is out of range.
+ * @see #getEntityCount() for the number of entities available.
+ */
+ @NonNull
+ public @EntityType String getEntity(int index) {
+ return mEntityConfidence.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for the specified entity. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
+ * classified text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(@EntityType String entity) {
+ return mEntityConfidence.getConfidenceScore(entity);
+ }
+
+ /**
+ * Returns a list of actions that may be performed on the text. The list is ordered based on
+ * the likelihood that a user will use the action, with the most likely action appearing first.
+ */
+ public List<RemoteAction> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Returns an icon that may be rendered on a widget used to act on the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelable and only represents the icon of the
+ * first {@link RemoteAction} (if one exists) when this object is read from a parcel.
+ *
+ * @deprecated Use {@link #getActions()} instead.
+ */
+ @Deprecated
+ @Nullable
+ public Drawable getIcon() {
+ return mLegacyIcon;
+ }
+
+ /**
+ * Returns a label that may be rendered on a widget used to act on the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelable and only represents the label of the
+ * first {@link RemoteAction} (if one exists) when this object is read from a parcel.
+ *
+ * @deprecated Use {@link #getActions()} instead.
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getLabel() {
+ return mLegacyLabel;
+ }
+
+ /**
+ * Returns an intent that may be fired to act on the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelled and will always return null when this
+ * object is read from a parcel.
+ *
+ * @deprecated Use {@link #getActions()} instead.
+ */
+ @Deprecated
+ @Nullable
+ public Intent getIntent() {
+ return mLegacyIntent;
+ }
+
+ /**
+ * Returns the OnClickListener that may be triggered to act on the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelable and only represents the first
+ * {@link RemoteAction} (if one exists) when this object is read from a parcel.
+ *
+ * @deprecated Use {@link #getActions()} instead.
+ */
+ @Nullable
+ public OnClickListener getOnClickListener() {
+ return mLegacyOnClickListener;
+ }
+
+ /**
+ * Returns the id, if one exists, for this object.
+ */
+ @Nullable
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the extended data.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** @hide */
+ public Builder toBuilder() {
+ return new Builder()
+ .setId(mId)
+ .setText(mText)
+ .addActions(mActions)
+ .setEntityConfidence(mEntityConfidence)
+ .setIcon(mLegacyIcon)
+ .setLabel(mLegacyLabel)
+ .setIntent(mLegacyIntent)
+ .setOnClickListener(mLegacyOnClickListener)
+ .setExtras(mExtras);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "TextClassification {text=%s, entities=%s, actions=%s, id=%s, extras=%s}",
+ mText, mEntityConfidence, mActions, mId, mExtras);
+ }
+
+ /**
+ * Creates an OnClickListener that triggers the specified PendingIntent.
+ *
+ * @hide
+ */
+ public static OnClickListener createIntentOnClickListener(@NonNull final PendingIntent intent) {
+ Objects.requireNonNull(intent);
+ return v -> {
+ try {
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(LOG_TAG, "Error sending PendingIntent", e);
+ }
+ };
+ }
+
+ /**
+ * Creates a PendingIntent for the specified intent.
+ * Returns null if the intent is not supported for the specified context.
+ *
+ * @throws IllegalArgumentException if context or intent is null
+ * @hide
+ */
+ public static PendingIntent createPendingIntent(
+ @NonNull final Context context, @NonNull final Intent intent, int requestCode) {
+ return PendingIntent.getActivity(
+ context, requestCode, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ /**
+ * Builder for building {@link TextClassification} objects.
+ *
+ * <p>e.g.
+ *
+ * <pre>{@code
+ * TextClassification classification = new TextClassification.Builder()
+ * .setText(classifiedText)
+ * .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
+ * .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
+ * .addAction(remoteAction1)
+ * .addAction(remoteAction2)
+ * .build();
+ * }</pre>
+ */
+ public static final class Builder {
+
+ @NonNull private final List<RemoteAction> mActions = new ArrayList<>();
+ @NonNull private final Map<String, Float> mTypeScoreMap = new ArrayMap<>();
+ @Nullable private String mText;
+ @Nullable private Drawable mLegacyIcon;
+ @Nullable private String mLegacyLabel;
+ @Nullable private Intent mLegacyIntent;
+ @Nullable private OnClickListener mLegacyOnClickListener;
+ @Nullable private String mId;
+ @Nullable private Bundle mExtras;
+
+ /**
+ * Sets the classified text.
+ */
+ @NonNull
+ public Builder setText(@Nullable String text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets an entity type for the classification result and assigns a confidence score.
+ * If a confidence score had already been set for the specified entity type, this will
+ * override that score.
+ *
+ * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
+ * 0 implies the entity does not exist for the classified text.
+ * Values greater than 1 are clamped to 1.
+ */
+ @NonNull
+ public Builder setEntityType(
+ @NonNull @EntityType String type,
+ @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
+ mTypeScoreMap.put(type, confidenceScore);
+ return this;
+ }
+
+ Builder setEntityConfidence(EntityConfidence scores) {
+ mTypeScoreMap.clear();
+ mTypeScoreMap.putAll(scores.toMap());
+ return this;
+ }
+
+ /** @hide */
+ public Builder clearEntityTypes() {
+ mTypeScoreMap.clear();
+ return this;
+ }
+
+ /**
+ * Adds an action that may be performed on the classified text. Actions should be added in
+ * order of likelihood that the user will use them, with the most likely action being added
+ * first.
+ */
+ @NonNull
+ public Builder addAction(@NonNull RemoteAction action) {
+ Preconditions.checkArgument(action != null);
+ mActions.add(action);
+ return this;
+ }
+
+ /** @hide */
+ public Builder addActions(Collection<RemoteAction> actions) {
+ Objects.requireNonNull(actions);
+ mActions.addAll(actions);
+ return this;
+ }
+
+ /** @hide */
+ public Builder clearActions() {
+ mActions.clear();
+ return this;
+ }
+
+ /**
+ * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
+ * on the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
+ * returned icon represents the icon of the first {@link RemoteAction} (if one exists).
+ *
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setIcon(@Nullable Drawable icon) {
+ mLegacyIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
+ * act on the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
+ * returned label represents the label of the first {@link RemoteAction} (if one exists).
+ *
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setLabel(@Nullable String label) {
+ mLegacyLabel = label;
+ return this;
+ }
+
+ /**
+ * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
+ * text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelled.
+ *
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setIntent(@Nullable Intent intent) {
+ mLegacyIntent = intent;
+ return this;
+ }
+
+ /**
+ * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
+ * the classified text.
+ *
+ * <p><strong>NOTE: </strong>This field is not parcelable. If read from a parcel, the
+ * returned OnClickListener represents the first {@link RemoteAction} (if one exists).
+ *
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+ mLegacyOnClickListener = onClickListener;
+ return this;
+ }
+
+ /**
+ * Sets an id for the TextClassification object.
+ */
+ @NonNull
+ public Builder setId(@Nullable String id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the extended data.
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link TextClassification} object.
+ */
+ @NonNull
+ public TextClassification build() {
+ EntityConfidence entityConfidence = new EntityConfidence(mTypeScoreMap);
+ return new TextClassification(mText, mLegacyIcon, mLegacyLabel, mLegacyIntent,
+ mLegacyOnClickListener, mActions, entityConfidence, mId,
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+
+ /**
+ * A request object for generating TextClassification.
+ */
+ public static final class Request implements Parcelable {
+
+ private final CharSequence mText;
+ private final int mStartIndex;
+ private final int mEndIndex;
+ @Nullable private final LocaleList mDefaultLocales;
+ @Nullable private final ZonedDateTime mReferenceTime;
+ @NonNull private final Bundle mExtras;
+ @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ private Request(
+ CharSequence text,
+ int startIndex,
+ int endIndex,
+ LocaleList defaultLocales,
+ ZonedDateTime referenceTime,
+ Bundle extras) {
+ mText = text;
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ mDefaultLocales = defaultLocales;
+ mReferenceTime = referenceTime;
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the text providing context for the text to classify (which is specified
+ * by the sub sequence starting at startIndex and ending at endIndex)
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns start index of the text to classify.
+ */
+ @IntRange(from = 0)
+ public int getStartIndex() {
+ return mStartIndex;
+ }
+
+ /**
+ * Returns end index of the text to classify.
+ */
+ @IntRange(from = 0)
+ public int getEndIndex() {
+ return mEndIndex;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate
+ * the provided text.
+ */
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ /**
+ * @return reference time based on which relative dates (e.g. "tomorrow") should be
+ * interpreted.
+ */
+ @Nullable
+ public ZonedDateTime getReferenceTime() {
+ return mReferenceTime;
+ }
+
+ /**
+ * Returns the name of the package that sent this request.
+ * This returns {@code null} if no calling package name is set.
+ */
+ @Nullable
+ public String getCallingPackageName() {
+ return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setSystemTextClassifierMetadata(
+ @Nullable SystemTextClassifierMetadata systemTcMetadata) {
+ mSystemTcMetadata = systemTcMetadata;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns the extended data.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * A builder for building TextClassification requests.
+ */
+ public static final class Builder {
+
+ private final CharSequence mText;
+ private final int mStartIndex;
+ private final int mEndIndex;
+ private Bundle mExtras;
+
+ @Nullable private LocaleList mDefaultLocales;
+ @Nullable private ZonedDateTime mReferenceTime;
+
+ /**
+ * @param text text providing context for the text to classify (which is specified
+ * by the sub sequence starting at startIndex and ending at endIndex)
+ * @param startIndex start index of the text to classify
+ * @param endIndex end index of the text to classify
+ */
+ public Builder(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex) {
+ Utils.checkArgument(text, startIndex, endIndex);
+ mText = text;
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ }
+
+ /**
+ * @param defaultLocales ordered list of locale preferences that may be used to
+ * disambiguate the provided text. If no locale preferences exist, set this to null
+ * or an empty locale list.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /**
+ * @param referenceTime reference time based on which relative dates (e.g. "tomorrow"
+ * should be interpreted. This should usually be the time when the text was
+ * originally composed. If no reference time is set, now is used.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
+ mReferenceTime = referenceTime;
+ return this;
+ }
+
+ /**
+ * Sets the extended data.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds and returns the request object.
+ */
+ @NonNull
+ public Request build() {
+ return new Request(new SpannedString(mText), mStartIndex, mEndIndex,
+ mDefaultLocales, mReferenceTime,
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mText);
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ dest.writeParcelable(mDefaultLocales, flags);
+ dest.writeString(mReferenceTime == null ? null : mReferenceTime.toString());
+ dest.writeBundle(mExtras);
+ dest.writeParcelable(mSystemTcMetadata, flags);
+ }
+
+ private static Request readFromParcel(Parcel in) {
+ final CharSequence text = in.readCharSequence();
+ final int startIndex = in.readInt();
+ final int endIndex = in.readInt();
+ final LocaleList defaultLocales = in.readParcelable(null);
+ final String referenceTimeString = in.readString();
+ final ZonedDateTime referenceTime = referenceTimeString == null
+ ? null : ZonedDateTime.parse(referenceTimeString);
+ final Bundle extras = in.readBundle();
+ final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null);
+
+ final Request request = new Request(text, startIndex, endIndex,
+ defaultLocales, referenceTime, extras);
+ request.setSystemTextClassifierMetadata(systemTcMetadata);
+ return request;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR =
+ new Parcelable.Creator<Request>() {
+ @Override
+ public Request createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public Request[] newArray(int size) {
+ return new Request[size];
+ }
+ };
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText);
+ // NOTE: legacy fields are not parcelled.
+ dest.writeTypedList(mActions);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mId);
+ dest.writeBundle(mExtras);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextClassification> CREATOR =
+ new Parcelable.Creator<TextClassification>() {
+ @Override
+ public TextClassification createFromParcel(Parcel in) {
+ return new TextClassification(in);
+ }
+
+ @Override
+ public TextClassification[] newArray(int size) {
+ return new TextClassification[size];
+ }
+ };
+
+ private TextClassification(Parcel in) {
+ mText = in.readString();
+ mActions = in.createTypedArrayList(RemoteAction.CREATOR);
+ if (!mActions.isEmpty()) {
+ final RemoteAction action = mActions.get(0);
+ mLegacyIcon = maybeLoadDrawable(action.getIcon());
+ mLegacyLabel = action.getTitle().toString();
+ mLegacyOnClickListener = createIntentOnClickListener(mActions.get(0).getActionIntent());
+ } else {
+ mLegacyIcon = null;
+ mLegacyLabel = null;
+ mLegacyOnClickListener = null;
+ }
+ mLegacyIntent = null; // mLegacyIntent is not parcelled.
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mId = in.readString();
+ mExtras = in.readBundle();
+ }
+
+ // Best effort attempt to try to load a drawable from the provided icon.
+ @Nullable
+ private static Drawable maybeLoadDrawable(Icon icon) {
+ if (icon == null) {
+ return null;
+ }
+ switch (icon.getType()) {
+ case Icon.TYPE_BITMAP:
+ return new BitmapDrawable(Resources.getSystem(), icon.getBitmap());
+ case Icon.TYPE_ADAPTIVE_BITMAP:
+ return new AdaptiveIconDrawable(null,
+ new BitmapDrawable(Resources.getSystem(), icon.getBitmap()));
+ case Icon.TYPE_DATA:
+ return new BitmapDrawable(
+ Resources.getSystem(),
+ BitmapFactory.decodeByteArray(
+ icon.getDataBytes(), icon.getDataOffset(), icon.getDataLength()));
+ }
+ return null;
+ }
+}
diff --git a/android/view/textclassifier/TextClassificationConstants.java b/android/view/textclassifier/TextClassificationConstants.java
new file mode 100644
index 0000000..5f3159c
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationConstants.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.Nullable;
+import android.provider.DeviceConfig;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+/**
+ * TextClassifier specific settings.
+ *
+ * <p>Currently, this class does not guarantee co-diverted flags are updated atomically.
+ *
+ * <pre>
+ * adb shell cmd device_config put textclassifier system_textclassifier_enabled true
+ * </pre>
+ *
+ * @see android.provider.DeviceConfig#NAMESPACE_TEXTCLASSIFIER
+ * @hide
+ */
+// TODO: Rename to TextClassifierSettings.
+public final class TextClassificationConstants {
+ /**
+ * Whether the smart linkify feature is enabled.
+ */
+ private static final String SMART_LINKIFY_ENABLED = "smart_linkify_enabled";
+ /**
+ * Whether SystemTextClassifier is enabled.
+ */
+ static final String SYSTEM_TEXT_CLASSIFIER_ENABLED = "system_textclassifier_enabled";
+ /**
+ * Whether TextClassifierImpl is enabled.
+ */
+ @VisibleForTesting
+ static final String LOCAL_TEXT_CLASSIFIER_ENABLED = "local_textclassifier_enabled";
+ /**
+ * Enable smart selection without a visible UI changes.
+ */
+ private static final String MODEL_DARK_LAUNCH_ENABLED = "model_dark_launch_enabled";
+ /**
+ * Whether the smart selection feature is enabled.
+ */
+ private static final String SMART_SELECTION_ENABLED = "smart_selection_enabled";
+ /**
+ * Whether the smart text share feature is enabled.
+ */
+ private static final String SMART_TEXT_SHARE_ENABLED = "smart_text_share_enabled";
+ /**
+ * Whether animation for smart selection is enabled.
+ */
+ private static final String SMART_SELECT_ANIMATION_ENABLED =
+ "smart_select_animation_enabled";
+ /**
+ * Max length of text that generateLinks can accept.
+ */
+ @VisibleForTesting
+ static final String GENERATE_LINKS_MAX_TEXT_LENGTH = "generate_links_max_text_length";
+ /**
+ * The TextClassifierService which would like to use. Example of setting the package:
+ * <pre>
+ * adb shell cmd device_config put textclassifier textclassifier_service_package_override \
+ * com.android.textclassifier
+ * </pre>
+ */
+ @VisibleForTesting
+ static final String TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE =
+ "textclassifier_service_package_override";
+
+ /**
+ * The timeout value in seconds used by {@link SystemTextClassifier} for each TextClassifier
+ * API calls.
+ */
+ @VisibleForTesting
+ static final String SYSTEM_TEXT_CLASSIFIER_API_TIMEOUT_IN_SECOND =
+ "system_textclassifier_api_timeout_in_second";
+
+ /**
+ * The maximum amount of characters before and after the selected text that is passed to the
+ * TextClassifier for the smart selection. e.g. If this value is 100, then 100 characters before
+ * the selection and 100 characters after the selection will be passed to the TextClassifier.
+ */
+ private static final String SMART_SELECTION_TRIM_DELTA = "smart_selection_trim_delta";
+
+ private static final String DEFAULT_TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE = null;
+ private static final boolean LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
+ private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
+ private static final boolean MODEL_DARK_LAUNCH_ENABLED_DEFAULT = false;
+ private static final boolean SMART_SELECTION_ENABLED_DEFAULT = true;
+ private static final boolean SMART_TEXT_SHARE_ENABLED_DEFAULT = true;
+ private static final boolean SMART_LINKIFY_ENABLED_DEFAULT = true;
+ private static final boolean SMART_SELECT_ANIMATION_ENABLED_DEFAULT = true;
+ private static final int GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT = 100 * 1000;
+ private static final long SYSTEM_TEXT_CLASSIFIER_API_TIMEOUT_IN_SECOND_DEFAULT = 60;
+ private static final int SMART_SELECTION_TRIM_DELTA_DEFAULT = 120;
+
+ @Nullable
+ public String getTextClassifierServicePackageOverride() {
+ return DeviceConfig.getString(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE,
+ DEFAULT_TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE);
+ }
+
+ public boolean isLocalTextClassifierEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ LOCAL_TEXT_CLASSIFIER_ENABLED, LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT);
+ }
+
+ public boolean isSystemTextClassifierEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ SYSTEM_TEXT_CLASSIFIER_ENABLED,
+ SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT);
+ }
+
+ public boolean isModelDarkLaunchEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ MODEL_DARK_LAUNCH_ENABLED, MODEL_DARK_LAUNCH_ENABLED_DEFAULT);
+ }
+
+ public boolean isSmartSelectionEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ SMART_SELECTION_ENABLED, SMART_SELECTION_ENABLED_DEFAULT);
+ }
+
+ public boolean isSmartTextShareEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ SMART_TEXT_SHARE_ENABLED, SMART_TEXT_SHARE_ENABLED_DEFAULT);
+ }
+
+ public boolean isSmartLinkifyEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, SMART_LINKIFY_ENABLED,
+ SMART_LINKIFY_ENABLED_DEFAULT);
+ }
+
+ public boolean isSmartSelectionAnimationEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ SMART_SELECT_ANIMATION_ENABLED, SMART_SELECT_ANIMATION_ENABLED_DEFAULT);
+ }
+
+ public int getGenerateLinksMaxTextLength() {
+ return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ GENERATE_LINKS_MAX_TEXT_LENGTH, GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT);
+ }
+
+ public long getSystemTextClassifierApiTimeoutInSecond() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ SYSTEM_TEXT_CLASSIFIER_API_TIMEOUT_IN_SECOND,
+ SYSTEM_TEXT_CLASSIFIER_API_TIMEOUT_IN_SECOND_DEFAULT);
+ }
+
+ public int getSmartSelectionTrimDelta() {
+ return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ SMART_SELECTION_TRIM_DELTA,
+ SMART_SELECTION_TRIM_DELTA_DEFAULT);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("TextClassificationConstants:");
+ pw.increaseIndent();
+ pw.print(GENERATE_LINKS_MAX_TEXT_LENGTH, getGenerateLinksMaxTextLength()).println();
+ pw.print(LOCAL_TEXT_CLASSIFIER_ENABLED, isLocalTextClassifierEnabled()).println();
+ pw.print(MODEL_DARK_LAUNCH_ENABLED, isModelDarkLaunchEnabled()).println();
+ pw.print(SMART_LINKIFY_ENABLED, isSmartLinkifyEnabled()).println();
+ pw.print(SMART_SELECT_ANIMATION_ENABLED, isSmartSelectionAnimationEnabled()).println();
+ pw.print(SMART_SELECTION_ENABLED, isSmartSelectionEnabled()).println();
+ pw.print(SMART_TEXT_SHARE_ENABLED, isSmartTextShareEnabled()).println();
+ pw.print(SYSTEM_TEXT_CLASSIFIER_ENABLED, isSystemTextClassifierEnabled()).println();
+ pw.print(TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE,
+ getTextClassifierServicePackageOverride()).println();
+ pw.print(SYSTEM_TEXT_CLASSIFIER_API_TIMEOUT_IN_SECOND,
+ getSystemTextClassifierApiTimeoutInSecond()).println();
+ pw.print(SMART_SELECTION_TRIM_DELTA, getSmartSelectionTrimDelta()).println();
+ pw.decreaseIndent();
+ }
+}
\ No newline at end of file
diff --git a/android/view/textclassifier/TextClassificationContext.java b/android/view/textclassifier/TextClassificationContext.java
new file mode 100644
index 0000000..5d5683f
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationContext.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.textclassifier.TextClassifier.WidgetType;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * A representation of the context in which text classification would be performed.
+ * @see TextClassificationManager#createTextClassificationSession(TextClassificationContext)
+ */
+public final class TextClassificationContext implements Parcelable {
+
+ private String mPackageName;
+ private final String mWidgetType;
+ @Nullable private final String mWidgetVersion;
+ private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ private TextClassificationContext(
+ String packageName,
+ String widgetType,
+ String widgetVersion) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mWidgetType = Objects.requireNonNull(widgetType);
+ mWidgetVersion = widgetVersion;
+ }
+
+ /**
+ * Returns the package name of the app that this context originated in.
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ void setSystemTextClassifierMetadata(@Nullable SystemTextClassifierMetadata systemTcMetadata) {
+ mSystemTcMetadata = systemTcMetadata;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns the widget type for this classification context.
+ */
+ @NonNull
+ @WidgetType
+ public String getWidgetType() {
+ return mWidgetType;
+ }
+
+ /**
+ * Returns a custom version string for the widget type.
+ *
+ * @see #getWidgetType()
+ */
+ @Nullable
+ public String getWidgetVersion() {
+ return mWidgetVersion;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "TextClassificationContext{"
+ + "packageName=%s, widgetType=%s, widgetVersion=%s, systemTcMetadata=%s}",
+ mPackageName, mWidgetType, mWidgetVersion, mSystemTcMetadata);
+ }
+
+ /**
+ * A builder for building a TextClassification context.
+ */
+ public static final class Builder {
+
+ private final String mPackageName;
+ private final String mWidgetType;
+
+ @Nullable private String mWidgetVersion;
+
+ /**
+ * Initializes a new builder for text classification context objects.
+ *
+ * @param packageName the name of the calling package
+ * @param widgetType the type of widget e.g. {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}
+ *
+ * @return this builder
+ */
+ public Builder(@NonNull String packageName, @NonNull @WidgetType String widgetType) {
+ mPackageName = Objects.requireNonNull(packageName);
+ mWidgetType = Objects.requireNonNull(widgetType);
+ }
+
+ /**
+ * Sets an optional custom version string for the widget type.
+ *
+ * @return this builder
+ */
+ public Builder setWidgetVersion(@Nullable String widgetVersion) {
+ mWidgetVersion = widgetVersion;
+ return this;
+ }
+
+ /**
+ * Builds the text classification context object.
+ *
+ * @return the built TextClassificationContext object
+ */
+ @NonNull
+ public TextClassificationContext build() {
+ return new TextClassificationContext(mPackageName, mWidgetType, mWidgetVersion);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mPackageName);
+ parcel.writeString(mWidgetType);
+ parcel.writeString(mWidgetVersion);
+ parcel.writeParcelable(mSystemTcMetadata, flags);
+ }
+
+ private TextClassificationContext(Parcel in) {
+ mPackageName = in.readString();
+ mWidgetType = in.readString();
+ mWidgetVersion = in.readString();
+ mSystemTcMetadata = in.readParcelable(null);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextClassificationContext> CREATOR =
+ new Parcelable.Creator<TextClassificationContext>() {
+ @Override
+ public TextClassificationContext createFromParcel(Parcel parcel) {
+ return new TextClassificationContext(parcel);
+ }
+
+ @Override
+ public TextClassificationContext[] newArray(int size) {
+ return new TextClassificationContext[size];
+ }
+ };
+}
diff --git a/android/view/textclassifier/TextClassificationManager.java b/android/view/textclassifier/TextClassificationManager.java
new file mode 100644
index 0000000..b606340
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationManager.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.ServiceManager;
+import android.view.textclassifier.TextClassifier.TextClassifierType;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.util.Objects;
+
+/**
+ * Interface to the text classification service.
+ */
+@SystemService(Context.TEXT_CLASSIFICATION_SERVICE)
+public final class TextClassificationManager {
+
+ private static final String LOG_TAG = TextClassifier.LOG_TAG;
+
+ private static final TextClassificationConstants sDefaultSettings =
+ new TextClassificationConstants();
+
+ private final Object mLock = new Object();
+ private final TextClassificationSessionFactory mDefaultSessionFactory =
+ classificationContext -> new TextClassificationSession(
+ classificationContext, getTextClassifier());
+
+ private final Context mContext;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private TextClassifier mCustomTextClassifier;
+ @GuardedBy("mLock")
+ private TextClassificationSessionFactory mSessionFactory;
+ @GuardedBy("mLock")
+ private TextClassificationConstants mSettings;
+
+ /** @hide */
+ public TextClassificationManager(Context context) {
+ mContext = Objects.requireNonNull(context);
+ mSessionFactory = mDefaultSessionFactory;
+ }
+
+ /**
+ * Returns the text classifier that was set via {@link #setTextClassifier(TextClassifier)}.
+ * If this is null, this method returns a default text classifier (i.e. either the system text
+ * classifier if one exists, or a local text classifier running in this process.)
+ * <p>
+ * Note that requests to the TextClassifier may be handled in an OEM-provided process rather
+ * than in the calling app's process.
+ *
+ * @see #setTextClassifier(TextClassifier)
+ */
+ @NonNull
+ public TextClassifier getTextClassifier() {
+ synchronized (mLock) {
+ if (mCustomTextClassifier != null) {
+ return mCustomTextClassifier;
+ } else if (getSettings().isSystemTextClassifierEnabled()) {
+ return getSystemTextClassifier(SystemTextClassifier.SYSTEM);
+ } else {
+ return getLocalTextClassifier();
+ }
+ }
+ }
+
+ /**
+ * Sets the text classifier.
+ * Set to null to use the system default text classifier.
+ * Set to {@link TextClassifier#NO_OP} to disable text classifier features.
+ */
+ public void setTextClassifier(@Nullable TextClassifier textClassifier) {
+ synchronized (mLock) {
+ mCustomTextClassifier = textClassifier;
+ }
+ }
+
+ /**
+ * Returns a specific type of text classifier.
+ * If the specified text classifier cannot be found, this returns {@link TextClassifier#NO_OP}.
+ *
+ * @see TextClassifier#LOCAL
+ * @see TextClassifier#SYSTEM
+ * @see TextClassifier#DEFAULT_SYSTEM
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public TextClassifier getTextClassifier(@TextClassifierType int type) {
+ switch (type) {
+ case TextClassifier.LOCAL:
+ return getLocalTextClassifier();
+ default:
+ return getSystemTextClassifier(type);
+ }
+ }
+
+ private TextClassificationConstants getSettings() {
+ synchronized (mLock) {
+ if (mSettings == null) {
+ mSettings = new TextClassificationConstants();
+ }
+ return mSettings;
+ }
+ }
+
+ /**
+ * Call this method to start a text classification session with the given context.
+ * A session is created with a context helping the classifier better understand
+ * what the user needs and consists of queries and feedback events. The queries
+ * are directly related to providing useful functionality to the user and the events
+ * are a feedback loop back to the classifier helping it learn and better serve
+ * future queries.
+ *
+ * <p> All interactions with the returned classifier are considered part of a single
+ * session and are logically grouped. For example, when a text widget is focused
+ * all user interactions around text editing (selection, editing, etc) can be
+ * grouped together to allow the classifier get better.
+ *
+ * @param classificationContext The context in which classification would occur
+ *
+ * @return An instance to perform classification in the given context
+ */
+ @NonNull
+ public TextClassifier createTextClassificationSession(
+ @NonNull TextClassificationContext classificationContext) {
+ Objects.requireNonNull(classificationContext);
+ final TextClassifier textClassifier =
+ mSessionFactory.createTextClassificationSession(classificationContext);
+ Objects.requireNonNull(textClassifier, "Session Factory should never return null");
+ return textClassifier;
+ }
+
+ /**
+ * @see #createTextClassificationSession(TextClassificationContext, TextClassifier)
+ * @hide
+ */
+ public TextClassifier createTextClassificationSession(
+ TextClassificationContext classificationContext, TextClassifier textClassifier) {
+ Objects.requireNonNull(classificationContext);
+ Objects.requireNonNull(textClassifier);
+ return new TextClassificationSession(classificationContext, textClassifier);
+ }
+
+ /**
+ * Sets a TextClassificationSessionFactory to be used to create session-aware TextClassifiers.
+ *
+ * @param factory the textClassification session factory. If this is null, the default factory
+ * will be used.
+ */
+ public void setTextClassificationSessionFactory(
+ @Nullable TextClassificationSessionFactory factory) {
+ synchronized (mLock) {
+ if (factory != null) {
+ mSessionFactory = factory;
+ } else {
+ mSessionFactory = mDefaultSessionFactory;
+ }
+ }
+ }
+
+ /** @hide */
+ private TextClassifier getSystemTextClassifier(@TextClassifierType int type) {
+ synchronized (mLock) {
+ if (getSettings().isSystemTextClassifierEnabled()) {
+ try {
+ Log.d(LOG_TAG, "Initializing SystemTextClassifier, type = "
+ + TextClassifier.typeToString(type));
+ return new SystemTextClassifier(
+ mContext,
+ getSettings(),
+ /* useDefault= */ type == TextClassifier.DEFAULT_SYSTEM);
+ } catch (ServiceManager.ServiceNotFoundException e) {
+ Log.e(LOG_TAG, "Could not initialize SystemTextClassifier", e);
+ }
+ }
+ return TextClassifier.NO_OP;
+ }
+ }
+
+ /**
+ * Returns a local textclassifier, which is running in this process.
+ */
+ @NonNull
+ private TextClassifier getLocalTextClassifier() {
+ Log.d(LOG_TAG, "Local text-classifier not supported. Returning a no-op text-classifier.");
+ return TextClassifier.NO_OP;
+ }
+
+ /** @hide **/
+ public void dump(IndentingPrintWriter pw) {
+ getSystemTextClassifier(TextClassifier.DEFAULT_SYSTEM).dump(pw);
+ getSystemTextClassifier(TextClassifier.SYSTEM).dump(pw);
+ getSettings().dump(pw);
+ }
+
+ /** @hide */
+ public static TextClassificationConstants getSettings(Context context) {
+ Objects.requireNonNull(context);
+ final TextClassificationManager tcm =
+ context.getSystemService(TextClassificationManager.class);
+ if (tcm != null) {
+ return tcm.getSettings();
+ } else {
+ // Use default settings if there is no tcm.
+ return sDefaultSettings;
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextClassificationManagerPerfTest.java b/android/view/textclassifier/TextClassificationManagerPerfTest.java
new file mode 100644
index 0000000..46250d7
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationManagerPerfTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view.textclassifier;
+
+import android.content.Context;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.provider.DeviceConfig;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+
+@LargeTest
+public class TextClassificationManagerPerfTest {
+ private static final String WRITE_DEVICE_CONFIG_PERMISSION =
+ "android.permission.WRITE_DEVICE_CONFIG";
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private String mOriginalSystemTextclassifierStatus;
+
+ @BeforeClass
+ public static void setUpClass() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(
+ WRITE_DEVICE_CONFIG_PERMISSION);
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Before
+ public void setUp() {
+ // Saves config original value.
+ mOriginalSystemTextclassifierStatus = DeviceConfig.getProperty(
+ DeviceConfig.NAMESPACE_TEXTCLASSIFIER, "system_textclassifier_enabled");
+ }
+
+ @After
+ public void tearDown() {
+ // Restores config original value.
+ enableSystemTextclassifier(mOriginalSystemTextclassifierStatus);
+ }
+
+ @Test
+ public void testGetTextClassifier_systemTextClassifierDisabled() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ enableSystemTextclassifier(String.valueOf(false));
+ TextClassificationManager textClassificationManager =
+ context.getSystemService(TextClassificationManager.class);
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ textClassificationManager.getTextClassifier();
+ }
+ }
+
+ @Test
+ public void testGetTextClassifier_systemTextClassifierEnabled() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ enableSystemTextclassifier(String.valueOf(true));
+ TextClassificationManager textClassificationManager =
+ context.getSystemService(TextClassificationManager.class);
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ textClassificationManager.getTextClassifier();
+ }
+ }
+
+ private void enableSystemTextclassifier(String enabled) {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+ "system_textclassifier_enabled", enabled, /* makeDefault */ false);
+ }
+}
diff --git a/android/view/textclassifier/TextClassificationSession.java b/android/view/textclassifier/TextClassificationSession.java
new file mode 100644
index 0000000..0008658
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationSession.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.view.textclassifier.SelectionEvent.InvocationMethod;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import sun.misc.Cleaner;
+
+/**
+ * Session-aware TextClassifier.
+ */
+@WorkerThread
+final class TextClassificationSession implements TextClassifier {
+
+ private static final String LOG_TAG = "TextClassificationSession";
+
+ private final TextClassifier mDelegate;
+ private final SelectionEventHelper mEventHelper;
+ private final TextClassificationSessionId mSessionId;
+ private final TextClassificationContext mClassificationContext;
+ private final Cleaner mCleaner;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private boolean mDestroyed;
+
+ TextClassificationSession(TextClassificationContext context, TextClassifier delegate) {
+ mClassificationContext = Objects.requireNonNull(context);
+ mDelegate = Objects.requireNonNull(delegate);
+ mSessionId = new TextClassificationSessionId();
+ mEventHelper = new SelectionEventHelper(mSessionId, mClassificationContext);
+ initializeRemoteSession();
+ // This ensures destroy() is called if the client forgot to do so.
+ mCleaner = Cleaner.create(this, new CleanerRunnable(mEventHelper, mDelegate));
+ }
+
+ @Override
+ public TextSelection suggestSelection(TextSelection.Request request) {
+ return checkDestroyedAndRun(() -> mDelegate.suggestSelection(request));
+ }
+
+ private void initializeRemoteSession() {
+ if (mDelegate instanceof SystemTextClassifier) {
+ ((SystemTextClassifier) mDelegate).initializeRemoteSession(
+ mClassificationContext, mSessionId);
+ }
+ }
+
+ @Override
+ public TextClassification classifyText(TextClassification.Request request) {
+ return checkDestroyedAndRun(() -> mDelegate.classifyText(request));
+ }
+
+ @Override
+ public TextLinks generateLinks(TextLinks.Request request) {
+ return checkDestroyedAndRun(() -> mDelegate.generateLinks(request));
+ }
+
+ @Override
+ public ConversationActions suggestConversationActions(ConversationActions.Request request) {
+ return checkDestroyedAndRun(() -> mDelegate.suggestConversationActions(request));
+ }
+
+ @Override
+ public TextLanguage detectLanguage(TextLanguage.Request request) {
+ return checkDestroyedAndRun(() -> mDelegate.detectLanguage(request));
+ }
+
+ @Override
+ public int getMaxGenerateLinksTextLength() {
+ return checkDestroyedAndRun(mDelegate::getMaxGenerateLinksTextLength);
+ }
+
+ @Override
+ public void onSelectionEvent(SelectionEvent event) {
+ checkDestroyedAndRun(() -> {
+ try {
+ if (mEventHelper.sanitizeEvent(event)) {
+ mDelegate.onSelectionEvent(event);
+ }
+ } catch (Exception e) {
+ // Avoid crashing for event reporting.
+ Log.e(LOG_TAG, "Error reporting text classifier selection event", e);
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public void onTextClassifierEvent(TextClassifierEvent event) {
+ checkDestroyedAndRun(() -> {
+ try {
+ event.mHiddenTempSessionId = mSessionId;
+ mDelegate.onTextClassifierEvent(event);
+ } catch (Exception e) {
+ // Avoid crashing for event reporting.
+ Log.e(LOG_TAG, "Error reporting text classifier event", e);
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public void destroy() {
+ synchronized (mLock) {
+ if (!mDestroyed) {
+ mCleaner.clean();
+ mDestroyed = true;
+ }
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ synchronized (mLock) {
+ return mDestroyed;
+ }
+ }
+
+ /**
+ * Check whether the TextClassification Session was destroyed before and after the actual API
+ * invocation, and return response if not.
+ *
+ * @param responseSupplier a Supplier that represents a TextClassifier call
+ * @return the response of the TextClassifier call
+ * @throws IllegalStateException if this TextClassification session was destroyed before the
+ * call returned
+ * @see #isDestroyed()
+ * @see #destroy()
+ */
+ private <T> T checkDestroyedAndRun(Supplier<T> responseSupplier) {
+ if (!isDestroyed()) {
+ T response = responseSupplier.get();
+ synchronized (mLock) {
+ if (!mDestroyed) {
+ return response;
+ }
+ }
+ }
+ throw new IllegalStateException(
+ "This TextClassification session has been destroyed");
+ }
+
+ /**
+ * Helper class for updating SelectionEvent fields.
+ */
+ private static final class SelectionEventHelper {
+
+ private final TextClassificationSessionId mSessionId;
+ private final TextClassificationContext mContext;
+
+ @InvocationMethod
+ private int mInvocationMethod = SelectionEvent.INVOCATION_UNKNOWN;
+ private SelectionEvent mPrevEvent;
+ private SelectionEvent mSmartEvent;
+ private SelectionEvent mStartEvent;
+
+ SelectionEventHelper(
+ TextClassificationSessionId sessionId, TextClassificationContext context) {
+ mSessionId = Objects.requireNonNull(sessionId);
+ mContext = Objects.requireNonNull(context);
+ }
+
+ /**
+ * Updates the necessary fields in the event for the current session.
+ *
+ * @return true if the event should be reported. false if the event should be ignored
+ */
+ boolean sanitizeEvent(SelectionEvent event) {
+ updateInvocationMethod(event);
+ modifyAutoSelectionEventType(event);
+
+ if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED
+ && mStartEvent == null) {
+ Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
+ return false;
+ }
+
+ final long now = System.currentTimeMillis();
+ switch (event.getEventType()) {
+ case SelectionEvent.EVENT_SELECTION_STARTED:
+ Preconditions.checkArgument(
+ event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
+ event.setSessionId(mSessionId);
+ mStartEvent = event;
+ break;
+ case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through
+ case SelectionEvent.EVENT_SMART_SELECTION_MULTI: // fall through
+ case SelectionEvent.EVENT_AUTO_SELECTION:
+ mSmartEvent = event;
+ break;
+ case SelectionEvent.ACTION_ABANDON:
+ case SelectionEvent.ACTION_OVERTYPE:
+ if (mPrevEvent != null) {
+ event.setEntityType(mPrevEvent.getEntityType());
+ }
+ break;
+ case SelectionEvent.EVENT_SELECTION_MODIFIED:
+ if (mPrevEvent != null
+ && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart()
+ && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) {
+ // Selection did not change. Ignore event.
+ return false;
+ }
+ break;
+ default:
+ // do nothing.
+ }
+
+ event.setEventTime(now);
+ if (mStartEvent != null) {
+ event.setSessionId(mStartEvent.getSessionId())
+ .setDurationSinceSessionStart(now - mStartEvent.getEventTime())
+ .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
+ .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
+ }
+ if (mSmartEvent != null) {
+ event.setResultId(mSmartEvent.getResultId())
+ .setSmartStart(
+ mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
+ .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
+ }
+ if (mPrevEvent != null) {
+ event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime())
+ .setEventIndex(mPrevEvent.getEventIndex() + 1);
+ }
+ mPrevEvent = event;
+ return true;
+ }
+
+ void endSession() {
+ mPrevEvent = null;
+ mSmartEvent = null;
+ mStartEvent = null;
+ }
+
+ private void updateInvocationMethod(SelectionEvent event) {
+ event.setTextClassificationSessionContext(mContext);
+ if (event.getInvocationMethod() == SelectionEvent.INVOCATION_UNKNOWN) {
+ event.setInvocationMethod(mInvocationMethod);
+ } else {
+ mInvocationMethod = event.getInvocationMethod();
+ }
+ }
+
+ private void modifyAutoSelectionEventType(SelectionEvent event) {
+ switch (event.getEventType()) {
+ case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through
+ case SelectionEvent.EVENT_SMART_SELECTION_MULTI: // fall through
+ case SelectionEvent.EVENT_AUTO_SELECTION:
+ if (SelectionSessionLogger.isPlatformLocalTextClassifierSmartSelection(
+ event.getResultId())) {
+ if (event.getAbsoluteEnd() - event.getAbsoluteStart() > 1) {
+ event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI);
+ } else {
+ event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_SINGLE);
+ }
+ } else {
+ event.setEventType(SelectionEvent.EVENT_AUTO_SELECTION);
+ }
+ return;
+ default:
+ return;
+ }
+ }
+ }
+
+ // We use a static nested class here to avoid retaining the object reference of the outer
+ // class. Otherwise. the Cleaner would never be triggered.
+ private static class CleanerRunnable implements Runnable {
+ @NonNull
+ private final SelectionEventHelper mEventHelper;
+ @NonNull
+ private final TextClassifier mDelegate;
+
+ CleanerRunnable(
+ @NonNull SelectionEventHelper eventHelper, @NonNull TextClassifier delegate) {
+ mEventHelper = Objects.requireNonNull(eventHelper);
+ mDelegate = Objects.requireNonNull(delegate);
+ }
+
+ @Override
+ public void run() {
+ mEventHelper.endSession();
+ mDelegate.destroy();
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextClassificationSessionFactory.java b/android/view/textclassifier/TextClassificationSessionFactory.java
new file mode 100644
index 0000000..c0914b6
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationSessionFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+
+/**
+ * An interface for creating a session-aware TextClassifier.
+ *
+ * @see TextClassificationManager#createTextClassificationSession(TextClassificationContext)
+ */
+public interface TextClassificationSessionFactory {
+
+ /**
+ * Creates and returns a session-aware TextClassifier.
+ *
+ * @param classificationContext the classification context
+ */
+ @NonNull
+ TextClassifier createTextClassificationSession(
+ @NonNull TextClassificationContext classificationContext);
+}
diff --git a/android/view/textclassifier/TextClassificationSessionId.java b/android/view/textclassifier/TextClassificationSessionId.java
new file mode 100644
index 0000000..5fdcc31
--- /dev/null
+++ b/android/view/textclassifier/TextClassificationSessionId.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * This class represents the id of a text classification session.
+ */
+public final class TextClassificationSessionId implements Parcelable {
+ @NonNull
+ private final String mValue;
+ @NonNull
+ private final IBinder mToken;
+
+ /**
+ * Creates a new instance.
+ *
+ * @hide
+ */
+ public TextClassificationSessionId() {
+ this(UUID.randomUUID().toString(), new Binder());
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param value The internal value.
+ *
+ * @hide
+ */
+ public TextClassificationSessionId(@NonNull String value, @NonNull IBinder token) {
+ mValue = Objects.requireNonNull(value);
+ mToken = Objects.requireNonNull(token);
+ }
+
+ /** @hide */
+ @NonNull
+ public IBinder getToken() {
+ return mToken;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TextClassificationSessionId that = (TextClassificationSessionId) o;
+ return Objects.equals(mValue, that.mValue) && Objects.equals(mToken, that.mToken);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mValue, mToken);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "TextClassificationSessionId {%s}", mValue);
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mValue);
+ parcel.writeStrongBinder(mToken);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns the value of this ID.
+ */
+ @NonNull
+ public String getValue() {
+ return mValue;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<TextClassificationSessionId> CREATOR =
+ new Parcelable.Creator<TextClassificationSessionId>() {
+ @Override
+ public TextClassificationSessionId createFromParcel(Parcel parcel) {
+ return new TextClassificationSessionId(
+ parcel.readString(), parcel.readStrongBinder());
+ }
+
+ @Override
+ public TextClassificationSessionId[] newArray(int size) {
+ return new TextClassificationSessionId[size];
+ }
+ };
+}
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
new file mode 100644
index 0000000..ef50045
--- /dev/null
+++ b/android/view/textclassifier/TextClassifier.java
@@ -0,0 +1,798 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.WorkerThread;
+import android.os.LocaleList;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.text.util.Linkify.LinkifyMask;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Interface for providing text classification related features.
+ * <p>
+ * The TextClassifier may be used to understand the meaning of text, as well as generating predicted
+ * next actions based on the text.
+ *
+ * <p><strong>NOTE: </strong>Unless otherwise stated, methods of this interface are blocking
+ * operations. Call on a worker thread.
+ */
+public interface TextClassifier {
+
+ /** @hide */
+ String LOG_TAG = "androidtc";
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {LOCAL, SYSTEM, DEFAULT_SYSTEM})
+ @interface TextClassifierType {} // TODO: Expose as system APIs.
+ /** Specifies a TextClassifier that runs locally in the app's process. @hide */
+ int LOCAL = 0;
+ /** Specifies a TextClassifier that runs in the system process and serves all apps. @hide */
+ int SYSTEM = 1;
+ /** Specifies the default TextClassifier that runs in the system process. @hide */
+ int DEFAULT_SYSTEM = 2;
+
+ /** @hide */
+ static String typeToString(@TextClassifierType int type) {
+ switch (type) {
+ case LOCAL:
+ return "Local";
+ case SYSTEM:
+ return "System";
+ case DEFAULT_SYSTEM:
+ return "Default system";
+ }
+ return "Unknown";
+ }
+
+ /** The TextClassifier failed to run. */
+ String TYPE_UNKNOWN = "";
+ /** The classifier ran, but didn't recognize a known entity. */
+ String TYPE_OTHER = "other";
+ /** E-mail address (e.g. "[email protected]"). */
+ String TYPE_EMAIL = "email";
+ /** Phone number (e.g. "555-123 456"). */
+ String TYPE_PHONE = "phone";
+ /** Physical address. */
+ String TYPE_ADDRESS = "address";
+ /** Web URL. */
+ String TYPE_URL = "url";
+ /** Time reference that is no more specific than a date. May be absolute such as "01/01/2000" or
+ * relative like "tomorrow". **/
+ String TYPE_DATE = "date";
+ /** Time reference that includes a specific time. May be absolute such as "01/01/2000 5:30pm" or
+ * relative like "tomorrow at 5:30pm". **/
+ String TYPE_DATE_TIME = "datetime";
+ /** Flight number in IATA format. */
+ String TYPE_FLIGHT_NUMBER = "flight";
+ /**
+ * Word that users may be interested to look up for meaning.
+ * @hide
+ */
+ String TYPE_DICTIONARY = "dictionary";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = { "TYPE_" }, value = {
+ TYPE_UNKNOWN,
+ TYPE_OTHER,
+ TYPE_EMAIL,
+ TYPE_PHONE,
+ TYPE_ADDRESS,
+ TYPE_URL,
+ TYPE_DATE,
+ TYPE_DATE_TIME,
+ TYPE_FLIGHT_NUMBER,
+ TYPE_DICTIONARY
+ })
+ @interface EntityType {}
+
+ /** Designates that the text in question is editable. **/
+ String HINT_TEXT_IS_EDITABLE = "android.text_is_editable";
+ /** Designates that the text in question is not editable. **/
+ String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = { "HINT_" }, value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE})
+ @interface Hints {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({WIDGET_TYPE_TEXTVIEW, WIDGET_TYPE_EDITTEXT, WIDGET_TYPE_UNSELECTABLE_TEXTVIEW,
+ WIDGET_TYPE_WEBVIEW, WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW,
+ WIDGET_TYPE_CUSTOM_EDITTEXT, WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW,
+ WIDGET_TYPE_NOTIFICATION, WIDGET_TYPE_CLIPBOARD, WIDGET_TYPE_UNKNOWN })
+ @interface WidgetType {}
+
+ /** The widget involved in the text classification context is a standard
+ * {@link android.widget.TextView}. */
+ String WIDGET_TYPE_TEXTVIEW = "textview";
+ /** The widget involved in the text classification context is a standard
+ * {@link android.widget.EditText}. */
+ String WIDGET_TYPE_EDITTEXT = "edittext";
+ /** The widget involved in the text classification context is a standard non-selectable
+ * {@link android.widget.TextView}. */
+ String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
+ /** The widget involved in the text classification context is a standard
+ * {@link android.webkit.WebView}. */
+ String WIDGET_TYPE_WEBVIEW = "webview";
+ /** The widget involved in the text classification context is a standard editable
+ * {@link android.webkit.WebView}. */
+ String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
+ /** The widget involved in the text classification context is a custom text widget. */
+ String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
+ /** The widget involved in the text classification context is a custom editable text widget. */
+ String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
+ /** The widget involved in the text classification context is a custom non-selectable text
+ * widget. */
+ String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
+ /** The widget involved in the text classification context is a notification */
+ String WIDGET_TYPE_NOTIFICATION = "notification";
+ /** The text classification context is for use with the system clipboard. */
+ String WIDGET_TYPE_CLIPBOARD = "clipboard";
+ /** The widget involved in the text classification context is of an unknown/unspecified type. */
+ String WIDGET_TYPE_UNKNOWN = "unknown";
+
+ /**
+ * No-op TextClassifier.
+ * This may be used to turn off TextClassifier features.
+ */
+ TextClassifier NO_OP = new TextClassifier() {
+ @Override
+ public String toString() {
+ return "TextClassifier.NO_OP";
+ }
+ };
+
+ /**
+ * Extra that is included on activity intents coming from a TextClassifier when
+ * it suggests actions to its caller.
+ * <p>
+ * All {@link TextClassifier} implementations should make sure this extra exists in their
+ * generated intents.
+ */
+ String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
+
+ /**
+ * Returns suggested text selection start and end indices, recognized entity types, and their
+ * associated confidence scores. The entity types are ordered from highest to lowest scoring.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * @param request the text selection request
+ */
+ @WorkerThread
+ @NonNull
+ default TextSelection suggestSelection(@NonNull TextSelection.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build();
+ }
+
+ /**
+ * Returns suggested text selection start and end indices, recognized entity types, and their
+ * associated confidence scores. The entity types are ordered from highest to lowest scoring.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
+ * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method,
+ * a stack overflow error will happen.
+ *
+ * @param text text providing context for the selected text (which is specified
+ * by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
+ * @param selectionStartIndex start index of the selected part of text
+ * @param selectionEndIndex end index of the selected part of text
+ * @param defaultLocales ordered list of locale preferences that may be used to
+ * disambiguate the provided text. If no locale preferences exist, set this to null
+ * or an empty locale list.
+ *
+ * @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
+ * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
+ *
+ * @see #suggestSelection(TextSelection.Request)
+ */
+ @WorkerThread
+ @NonNull
+ default TextSelection suggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable LocaleList defaultLocales) {
+ final TextSelection.Request request = new TextSelection.Request.Builder(
+ text, selectionStartIndex, selectionEndIndex)
+ .setDefaultLocales(defaultLocales)
+ .build();
+ return suggestSelection(request);
+ }
+
+ /**
+ * Classifies the specified text and returns a {@link TextClassification} object that can be
+ * used to generate a widget for handling the classified text.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * @param request the text classification request
+ */
+ @WorkerThread
+ @NonNull
+ default TextClassification classifyText(@NonNull TextClassification.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ return TextClassification.EMPTY;
+ }
+
+ /**
+ * Classifies the specified text and returns a {@link TextClassification} object that can be
+ * used to generate a widget for handling the classified text.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
+ * {@link #classifyText(TextClassification.Request)}. If that method calls this method,
+ * a stack overflow error will happen.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * @param text text providing context for the text to classify (which is specified
+ * by the sub sequence starting at startIndex and ending at endIndex)
+ * @param startIndex start index of the text to classify
+ * @param endIndex end index of the text to classify
+ * @param defaultLocales ordered list of locale preferences that may be used to
+ * disambiguate the provided text. If no locale preferences exist, set this to null
+ * or an empty locale list.
+ *
+ * @throws IllegalArgumentException if text is null; startIndex is negative;
+ * endIndex is greater than text.length() or not greater than startIndex
+ *
+ * @see #classifyText(TextClassification.Request)
+ */
+ @WorkerThread
+ @NonNull
+ default TextClassification classifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable LocaleList defaultLocales) {
+ final TextClassification.Request request = new TextClassification.Request.Builder(
+ text, startIndex, endIndex)
+ .setDefaultLocales(defaultLocales)
+ .build();
+ return classifyText(request);
+ }
+
+ /**
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * @param request the text links request
+ *
+ * @see #getMaxGenerateLinksTextLength()
+ */
+ @WorkerThread
+ @NonNull
+ default TextLinks generateLinks(@NonNull TextLinks.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ return new TextLinks.Builder(request.getText().toString()).build();
+ }
+
+ /**
+ * Returns the maximal length of text that can be processed by generateLinks.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * @see #generateLinks(TextLinks.Request)
+ */
+ @WorkerThread
+ default int getMaxGenerateLinksTextLength() {
+ return Integer.MAX_VALUE;
+ }
+
+ /**
+ * Detects the language of the text in the given request.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * @param request the {@link TextLanguage} request.
+ * @return the {@link TextLanguage} result.
+ */
+ @WorkerThread
+ @NonNull
+ default TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ return TextLanguage.EMPTY;
+ }
+
+ /**
+ * Suggests and returns a list of actions according to the given conversation.
+ */
+ @WorkerThread
+ @NonNull
+ default ConversationActions suggestConversationActions(
+ @NonNull ConversationActions.Request request) {
+ Objects.requireNonNull(request);
+ Utils.checkMainThread();
+ return new ConversationActions(Collections.emptyList(), null);
+ }
+
+ /**
+ * <strong>NOTE: </strong>Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead.
+ * <p>
+ * Reports a selection event.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ */
+ default void onSelectionEvent(@NonNull SelectionEvent event) {
+ // TODO: Consider rerouting to onTextClassifierEvent()
+ }
+
+ /**
+ * Reports a text classifier event.
+ * <p>
+ * <strong>NOTE: </strong>Call on a worker thread.
+ *
+ * @throws IllegalStateException if this TextClassifier has been destroyed.
+ * @see #isDestroyed()
+ */
+ default void onTextClassifierEvent(@NonNull TextClassifierEvent event) {}
+
+ /**
+ * Destroys this TextClassifier.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * <p>Subsequent calls to this method are no-ops.
+ */
+ default void destroy() {}
+
+ /**
+ * Returns whether or not this TextClassifier has been destroyed.
+ *
+ * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact
+ * with the classifier and an attempt to do so would throw an {@link IllegalStateException}.
+ * However, this method should never throw an {@link IllegalStateException}.
+ *
+ * @see #destroy()
+ */
+ default boolean isDestroyed() {
+ return false;
+ }
+
+ /** @hide **/
+ default void dump(@NonNull IndentingPrintWriter printWriter) {}
+
+ /**
+ * Configuration object for specifying what entity types to identify.
+ *
+ * Configs are initially based on a predefined preset, and can be modified from there.
+ */
+ final class EntityConfig implements Parcelable {
+ private final List<String> mIncludedTypes;
+ private final List<String> mExcludedTypes;
+ private final List<String> mHints;
+ private final boolean mIncludeTypesFromTextClassifier;
+
+ private EntityConfig(
+ List<String> includedEntityTypes,
+ List<String> excludedEntityTypes,
+ List<String> hints,
+ boolean includeTypesFromTextClassifier) {
+ mIncludedTypes = Objects.requireNonNull(includedEntityTypes);
+ mExcludedTypes = Objects.requireNonNull(excludedEntityTypes);
+ mHints = Objects.requireNonNull(hints);
+ mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
+ }
+
+ private EntityConfig(Parcel in) {
+ mIncludedTypes = new ArrayList<>();
+ in.readStringList(mIncludedTypes);
+ mExcludedTypes = new ArrayList<>();
+ in.readStringList(mExcludedTypes);
+ List<String> tmpHints = new ArrayList<>();
+ in.readStringList(tmpHints);
+ mHints = Collections.unmodifiableList(tmpHints);
+ mIncludeTypesFromTextClassifier = in.readByte() != 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeStringList(mIncludedTypes);
+ parcel.writeStringList(mExcludedTypes);
+ parcel.writeStringList(mHints);
+ parcel.writeByte((byte) (mIncludeTypesFromTextClassifier ? 1 : 0));
+ }
+
+ /**
+ * Creates an EntityConfig.
+ *
+ * @param hints Hints for the TextClassifier to determine what types of entities to find.
+ *
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public static EntityConfig createWithHints(@Nullable Collection<String> hints) {
+ return new EntityConfig.Builder()
+ .includeTypesFromTextClassifier(true)
+ .setHints(hints)
+ .build();
+ }
+
+ /**
+ * Creates an EntityConfig.
+ *
+ * @param hints Hints for the TextClassifier to determine what types of entities to find
+ * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include
+ * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude
+ *
+ *
+ * Note that if an entity has been excluded, the exclusion will take precedence.
+ *
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public static EntityConfig create(@Nullable Collection<String> hints,
+ @Nullable Collection<String> includedEntityTypes,
+ @Nullable Collection<String> excludedEntityTypes) {
+ return new EntityConfig.Builder()
+ .setIncludedTypes(includedEntityTypes)
+ .setExcludedTypes(excludedEntityTypes)
+ .setHints(hints)
+ .includeTypesFromTextClassifier(true)
+ .build();
+ }
+
+ /**
+ * Creates an EntityConfig with an explicit entity list.
+ *
+ * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find.
+ *
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public static EntityConfig createWithExplicitEntityList(
+ @Nullable Collection<String> entityTypes) {
+ return new EntityConfig.Builder()
+ .setIncludedTypes(entityTypes)
+ .includeTypesFromTextClassifier(false)
+ .build();
+ }
+
+ /**
+ * Returns a final list of entity types to find.
+ *
+ * @param entityTypes Entity types we think should be found before factoring in
+ * includes/excludes
+ *
+ * This method is intended for use by TextClassifier implementations.
+ */
+ public Collection<String> resolveEntityListModifications(
+ @NonNull Collection<String> entityTypes) {
+ final Set<String> finalSet = new HashSet<>();
+ if (mIncludeTypesFromTextClassifier) {
+ finalSet.addAll(entityTypes);
+ }
+ finalSet.addAll(mIncludedTypes);
+ finalSet.removeAll(mExcludedTypes);
+ return finalSet;
+ }
+
+ /**
+ * Retrieves the list of hints.
+ *
+ * @return An unmodifiable collection of the hints.
+ */
+ public Collection<String> getHints() {
+ return mHints;
+ }
+
+ /**
+ * Return whether the client allows the text classifier to include its own list of
+ * default types. If this function returns {@code true}, a default list of types suggested
+ * from a text classifier will be taking into account.
+ *
+ * <p>NOTE: This method is intended for use by a text classifier.
+ *
+ * @see #resolveEntityListModifications(Collection)
+ */
+ public boolean shouldIncludeTypesFromTextClassifier() {
+ return mIncludeTypesFromTextClassifier;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<EntityConfig> CREATOR =
+ new Parcelable.Creator<EntityConfig>() {
+ @Override
+ public EntityConfig createFromParcel(Parcel in) {
+ return new EntityConfig(in);
+ }
+
+ @Override
+ public EntityConfig[] newArray(int size) {
+ return new EntityConfig[size];
+ }
+ };
+
+
+
+ /** Builder class to construct the {@link EntityConfig} object. */
+ public static final class Builder {
+ @Nullable
+ private Collection<String> mIncludedTypes;
+ @Nullable
+ private Collection<String> mExcludedTypes;
+ @Nullable
+ private Collection<String> mHints;
+ private boolean mIncludeTypesFromTextClassifier = true;
+
+ /**
+ * Sets a collection of types that are explicitly included.
+ */
+ @NonNull
+ public Builder setIncludedTypes(@Nullable Collection<String> includedTypes) {
+ mIncludedTypes = includedTypes;
+ return this;
+ }
+
+ /**
+ * Sets a collection of types that are explicitly excluded.
+ */
+ @NonNull
+ public Builder setExcludedTypes(@Nullable Collection<String> excludedTypes) {
+ mExcludedTypes = excludedTypes;
+ return this;
+ }
+
+ /**
+ * Specifies whether or not to include the types suggested by the text classifier. By
+ * default, it is included.
+ */
+ @NonNull
+ public Builder includeTypesFromTextClassifier(boolean includeTypesFromTextClassifier) {
+ mIncludeTypesFromTextClassifier = includeTypesFromTextClassifier;
+ return this;
+ }
+
+
+ /**
+ * Sets the hints for the TextClassifier to determine what types of entities to find.
+ * These hints will only be used if {@link #includeTypesFromTextClassifier} is
+ * set to be true.
+ */
+ @NonNull
+ public Builder setHints(@Nullable Collection<String> hints) {
+ mHints = hints;
+ return this;
+ }
+
+ /**
+ * Combines all of the options that have been set and returns a new {@link EntityConfig}
+ * object.
+ */
+ @NonNull
+ public EntityConfig build() {
+ return new EntityConfig(
+ mIncludedTypes == null
+ ? Collections.emptyList()
+ : new ArrayList<>(mIncludedTypes),
+ mExcludedTypes == null
+ ? Collections.emptyList()
+ : new ArrayList<>(mExcludedTypes),
+ mHints == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(new ArrayList<>(mHints)),
+ mIncludeTypesFromTextClassifier);
+ }
+ }
+ }
+
+ /**
+ * Utility functions for TextClassifier methods.
+ *
+ * <ul>
+ * <li>Provides validation of input parameters to TextClassifier methods
+ * </ul>
+ *
+ * Intended to be used only for TextClassifier purposes.
+ * @hide
+ */
+ final class Utils {
+
+ @GuardedBy("WORD_ITERATOR")
+ private static final BreakIterator WORD_ITERATOR = BreakIterator.getWordInstance();
+
+ /**
+ * @throws IllegalArgumentException if text is null; startIndex is negative;
+ * endIndex is greater than text.length() or is not greater than startIndex;
+ * options is null
+ */
+ static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) {
+ Preconditions.checkArgument(text != null);
+ Preconditions.checkArgument(startIndex >= 0);
+ Preconditions.checkArgument(endIndex <= text.length());
+ Preconditions.checkArgument(endIndex > startIndex);
+ }
+
+ /** Returns if the length of the text is within the range. */
+ static boolean checkTextLength(CharSequence text, int maxLength) {
+ int textLength = text.length();
+ return textLength >= 0 && textLength <= maxLength;
+ }
+
+ /**
+ * Returns the substring of {@code text} that contains at least text from index
+ * {@code start} <i>(inclusive)</i> to index {@code end} <i><(exclusive)/i> with the goal of
+ * returning text that is at least {@code minimumLength}. If {@code text} is not long
+ * enough, this will return {@code text}. This method returns text at word boundaries.
+ *
+ * @param text the source text
+ * @param start the start index of text that must be included
+ * @param end the end index of text that must be included
+ * @param minimumLength minimum length of text to return if {@code text} is long enough
+ */
+ public static String getSubString(
+ String text, int start, int end, int minimumLength) {
+ Preconditions.checkArgument(start >= 0);
+ Preconditions.checkArgument(end <= text.length());
+ Preconditions.checkArgument(start <= end);
+
+ if (text.length() < minimumLength) {
+ return text;
+ }
+
+ final int length = end - start;
+ if (length >= minimumLength) {
+ return text.substring(start, end);
+ }
+
+ final int offset = (minimumLength - length) / 2;
+ int iterStart = Math.max(0, Math.min(start - offset, text.length() - minimumLength));
+ int iterEnd = Math.min(text.length(), iterStart + minimumLength);
+
+ synchronized (WORD_ITERATOR) {
+ WORD_ITERATOR.setText(text);
+ iterStart = WORD_ITERATOR.isBoundary(iterStart)
+ ? iterStart : Math.max(0, WORD_ITERATOR.preceding(iterStart));
+ iterEnd = WORD_ITERATOR.isBoundary(iterEnd)
+ ? iterEnd : Math.max(iterEnd, WORD_ITERATOR.following(iterEnd));
+ WORD_ITERATOR.setText("");
+ return text.substring(iterStart, iterEnd);
+ }
+ }
+
+ /**
+ * Generates links using legacy {@link Linkify}.
+ */
+ public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) {
+ final String string = request.getText().toString();
+ final TextLinks.Builder links = new TextLinks.Builder(string);
+
+ final Collection<String> entities = request.getEntityConfig()
+ .resolveEntityListModifications(Collections.emptyList());
+ if (entities.contains(TextClassifier.TYPE_URL)) {
+ addLinks(links, string, TextClassifier.TYPE_URL);
+ }
+ if (entities.contains(TextClassifier.TYPE_PHONE)) {
+ addLinks(links, string, TextClassifier.TYPE_PHONE);
+ }
+ if (entities.contains(TextClassifier.TYPE_EMAIL)) {
+ addLinks(links, string, TextClassifier.TYPE_EMAIL);
+ }
+ // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
+ return links.build();
+ }
+
+ private static void addLinks(
+ TextLinks.Builder links, String string, @EntityType String entityType) {
+ final Spannable spannable = new SpannableString(string);
+ if (Linkify.addLinks(spannable, linkMask(entityType))) {
+ final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
+ for (URLSpan urlSpan : spans) {
+ links.addLink(
+ spannable.getSpanStart(urlSpan),
+ spannable.getSpanEnd(urlSpan),
+ entityScores(entityType),
+ urlSpan);
+ }
+ }
+ }
+
+ @LinkifyMask
+ private static int linkMask(@EntityType String entityType) {
+ switch (entityType) {
+ case TextClassifier.TYPE_URL:
+ return Linkify.WEB_URLS;
+ case TextClassifier.TYPE_PHONE:
+ return Linkify.PHONE_NUMBERS;
+ case TextClassifier.TYPE_EMAIL:
+ return Linkify.EMAIL_ADDRESSES;
+ default:
+ // NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
+ return 0;
+ }
+ }
+
+ private static Map<String, Float> entityScores(@EntityType String entityType) {
+ final Map<String, Float> scores = new ArrayMap<>();
+ scores.put(entityType, 1f);
+ return scores;
+ }
+
+ static void checkMainThread() {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ Log.w(LOG_TAG, "TextClassifier called on main thread");
+ }
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextClassifierEvent.java b/android/view/textclassifier/TextClassifierEvent.java
new file mode 100644
index 0000000..90667cf
--- /dev/null
+++ b/android/view/textclassifier/TextClassifierEvent.java
@@ -0,0 +1,1182 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.icu.util.ULocale;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class represents events that are sent by components to the {@link TextClassifier} to report
+ * something of note that relates to a feature powered by the TextClassifier. The TextClassifier may
+ * log these events or use them to improve future responses to queries.
+ * <p>
+ * Each category of events has its their own subclass. Events of each type have an associated
+ * set of related properties. You can find their specification in the subclasses.
+ */
+public abstract class TextClassifierEvent implements Parcelable {
+
+ private static final int PARCEL_TOKEN_TEXT_SELECTION_EVENT = 1;
+ private static final int PARCEL_TOKEN_TEXT_LINKIFY_EVENT = 2;
+ private static final int PARCEL_TOKEN_CONVERSATION_ACTION_EVENT = 3;
+ private static final int PARCEL_TOKEN_LANGUAGE_DETECTION_EVENT = 4;
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CATEGORY_SELECTION, CATEGORY_LINKIFY,
+ CATEGORY_CONVERSATION_ACTIONS, CATEGORY_LANGUAGE_DETECTION})
+ public @interface Category {
+ // For custom event categories, use range 1000+.
+ }
+
+ /**
+ * Smart selection
+ *
+ * @see TextSelectionEvent
+ */
+ public static final int CATEGORY_SELECTION = 1;
+ /**
+ * Linkify
+ *
+ * @see TextLinkifyEvent
+ */
+ public static final int CATEGORY_LINKIFY = 2;
+ /**
+ * Conversation actions
+ *
+ * @see ConversationActionsEvent
+ */
+ public static final int CATEGORY_CONVERSATION_ACTIONS = 3;
+ /**
+ * Language detection
+ *
+ * @see LanguageDetectionEvent
+ */
+ public static final int CATEGORY_LANGUAGE_DETECTION = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_SELECTION_STARTED, TYPE_SELECTION_MODIFIED,
+ TYPE_SMART_SELECTION_SINGLE, TYPE_SMART_SELECTION_MULTI, TYPE_AUTO_SELECTION,
+ TYPE_ACTIONS_SHOWN, TYPE_LINK_CLICKED, TYPE_OVERTYPE, TYPE_COPY_ACTION,
+ TYPE_PASTE_ACTION, TYPE_CUT_ACTION, TYPE_SHARE_ACTION, TYPE_SMART_ACTION,
+ TYPE_SELECTION_DRAG, TYPE_SELECTION_DESTROYED, TYPE_OTHER_ACTION, TYPE_SELECT_ALL,
+ TYPE_SELECTION_RESET, TYPE_MANUAL_REPLY, TYPE_ACTIONS_GENERATED, TYPE_LINKS_GENERATED,
+ TYPE_READ_CLIPBOARD})
+ public @interface Type {
+ // For custom event types, use range 1,000,000+.
+ }
+
+ // All these event type constants are required to match with those defined in
+ // textclassifier_enums.proto.
+ /** User started a new selection. */
+ public static final int TYPE_SELECTION_STARTED = 1;
+ /** User modified an existing selection. */
+ public static final int TYPE_SELECTION_MODIFIED = 2;
+ /** Smart selection triggered for a single token (word). */
+ public static final int TYPE_SMART_SELECTION_SINGLE = 3;
+ /** Smart selection triggered spanning multiple tokens (words). */
+ public static final int TYPE_SMART_SELECTION_MULTI = 4;
+ /** Something else other than user or the default TextClassifier triggered a selection. */
+ public static final int TYPE_AUTO_SELECTION = 5;
+ /** Smart actions shown to the user. */
+ public static final int TYPE_ACTIONS_SHOWN = 6;
+ /** User clicked a link. */
+ public static final int TYPE_LINK_CLICKED = 7;
+ /** User typed over the selection. */
+ public static final int TYPE_OVERTYPE = 8;
+ /** User clicked on Copy action. */
+ public static final int TYPE_COPY_ACTION = 9;
+ /** User clicked on Paste action. */
+ public static final int TYPE_PASTE_ACTION = 10;
+ /** User clicked on Cut action. */
+ public static final int TYPE_CUT_ACTION = 11;
+ /** User clicked on Share action. */
+ public static final int TYPE_SHARE_ACTION = 12;
+ /** User clicked on a Smart action. */
+ public static final int TYPE_SMART_ACTION = 13;
+ /** User dragged+dropped the selection. */
+ public static final int TYPE_SELECTION_DRAG = 14;
+ /** Selection is destroyed. */
+ public static final int TYPE_SELECTION_DESTROYED = 15;
+ /** User clicked on a custom action. */
+ public static final int TYPE_OTHER_ACTION = 16;
+ /** User clicked on Select All action */
+ public static final int TYPE_SELECT_ALL = 17;
+ /** User reset the smart selection. */
+ public static final int TYPE_SELECTION_RESET = 18;
+ /** User composed a reply. */
+ public static final int TYPE_MANUAL_REPLY = 19;
+ /** TextClassifier generated some actions */
+ public static final int TYPE_ACTIONS_GENERATED = 20;
+ /** Some text links were generated.*/
+ public static final int TYPE_LINKS_GENERATED = 21;
+ /**
+ * Read a clipboard.
+ * TODO: Make this public.
+ *
+ * @hide
+ */
+ public static final int TYPE_READ_CLIPBOARD = 22;
+
+ @Category
+ private final int mEventCategory;
+ @Type
+ private final int mEventType;
+ @Nullable
+ private final String[] mEntityTypes;
+ @Nullable
+ private TextClassificationContext mEventContext;
+ @Nullable
+ private final String mResultId;
+ private final int mEventIndex;
+ private final float[] mScores;
+ @Nullable
+ private final String mModelName;
+ private final int[] mActionIndices;
+ @Nullable
+ private final ULocale mLocale;
+ private final Bundle mExtras;
+
+ /**
+ * Session id holder to help with converting this event to the legacy SelectionEvent.
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Nullable
+ public TextClassificationSessionId mHiddenTempSessionId;
+
+ private TextClassifierEvent(Builder builder) {
+ mEventCategory = builder.mEventCategory;
+ mEventType = builder.mEventType;
+ mEntityTypes = builder.mEntityTypes;
+ mEventContext = builder.mEventContext;
+ mResultId = builder.mResultId;
+ mEventIndex = builder.mEventIndex;
+ mScores = builder.mScores;
+ mModelName = builder.mModelName;
+ mActionIndices = builder.mActionIndices;
+ mLocale = builder.mLocale;
+ mExtras = builder.mExtras == null ? Bundle.EMPTY : builder.mExtras;
+ }
+
+ private TextClassifierEvent(Parcel in) {
+ mEventCategory = in.readInt();
+ mEventType = in.readInt();
+ mEntityTypes = in.readStringArray();
+ mEventContext = in.readParcelable(null);
+ mResultId = in.readString();
+ mEventIndex = in.readInt();
+ int scoresLength = in.readInt();
+ mScores = new float[scoresLength];
+ in.readFloatArray(mScores);
+ mModelName = in.readString();
+ mActionIndices = in.createIntArray();
+ final String languageTag = in.readString();
+ mLocale = languageTag == null ? null : ULocale.forLanguageTag(languageTag);
+ mExtras = in.readBundle();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<TextClassifierEvent> CREATOR = new Creator<TextClassifierEvent>() {
+ @Override
+ public TextClassifierEvent createFromParcel(Parcel in) {
+ int token = in.readInt();
+ if (token == PARCEL_TOKEN_TEXT_SELECTION_EVENT) {
+ return new TextSelectionEvent(in);
+ }
+ if (token == PARCEL_TOKEN_TEXT_LINKIFY_EVENT) {
+ return new TextLinkifyEvent(in);
+ }
+ if (token == PARCEL_TOKEN_LANGUAGE_DETECTION_EVENT) {
+ return new LanguageDetectionEvent(in);
+ }
+ if (token == PARCEL_TOKEN_CONVERSATION_ACTION_EVENT) {
+ return new ConversationActionsEvent(in);
+ }
+ throw new IllegalStateException("Unexpected input event type token in parcel.");
+ }
+
+ @Override
+ public TextClassifierEvent[] newArray(int size) {
+ return new TextClassifierEvent[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(getParcelToken());
+ dest.writeInt(mEventCategory);
+ dest.writeInt(mEventType);
+ dest.writeStringArray(mEntityTypes);
+ dest.writeParcelable(mEventContext, flags);
+ dest.writeString(mResultId);
+ dest.writeInt(mEventIndex);
+ dest.writeInt(mScores.length);
+ dest.writeFloatArray(mScores);
+ dest.writeString(mModelName);
+ dest.writeIntArray(mActionIndices);
+ dest.writeString(mLocale == null ? null : mLocale.toLanguageTag());
+ dest.writeBundle(mExtras);
+ }
+
+ private int getParcelToken() {
+ if (this instanceof TextSelectionEvent) {
+ return PARCEL_TOKEN_TEXT_SELECTION_EVENT;
+ }
+ if (this instanceof TextLinkifyEvent) {
+ return PARCEL_TOKEN_TEXT_LINKIFY_EVENT;
+ }
+ if (this instanceof LanguageDetectionEvent) {
+ return PARCEL_TOKEN_LANGUAGE_DETECTION_EVENT;
+ }
+ if (this instanceof ConversationActionsEvent) {
+ return PARCEL_TOKEN_CONVERSATION_ACTION_EVENT;
+ }
+ throw new IllegalArgumentException("Unexpected type: " + this.getClass().getSimpleName());
+ }
+
+ /**
+ * Returns the event category. e.g. {@link #CATEGORY_SELECTION}.
+ */
+ @Category
+ public int getEventCategory() {
+ return mEventCategory;
+ }
+
+ /**
+ * Returns the event type. e.g. {@link #TYPE_SELECTION_STARTED}.
+ */
+ @Type
+ public int getEventType() {
+ return mEventType;
+ }
+
+ /**
+ * Returns an array of entity types. e.g. {@link TextClassifier#TYPE_ADDRESS}.
+ *
+ * @see Builder#setEntityTypes(String...) for supported types.
+ */
+ @NonNull
+ public String[] getEntityTypes() {
+ return mEntityTypes;
+ }
+
+ /**
+ * Returns the event context.
+ */
+ @Nullable
+ public TextClassificationContext getEventContext() {
+ return mEventContext;
+ }
+
+ /**
+ * Sets the event context.
+ * <p>
+ * Package-private for SystemTextClassifier's use.
+ */
+ void setEventContext(@Nullable TextClassificationContext eventContext) {
+ mEventContext = eventContext;
+ }
+
+ /**
+ * Returns the id of the text classifier result related to this event.
+ */
+ @Nullable
+ public String getResultId() {
+ return mResultId;
+ }
+
+ /**
+ * Returns the index of this event in the series of event it belongs to.
+ */
+ public int getEventIndex() {
+ return mEventIndex;
+ }
+
+ /**
+ * Returns the scores of the suggestions.
+ */
+ @NonNull
+ public float[] getScores() {
+ return mScores;
+ }
+
+ /**
+ * Returns the model name.
+ */
+ @Nullable
+ public String getModelName() {
+ return mModelName;
+ }
+
+ /**
+ * Returns the indices of the actions relating to this event.
+ * Actions are usually returned by the text classifier in priority order with the most
+ * preferred action at index 0. This list gives an indication of the position of the actions
+ * that are being reported.
+ *
+ * @see Builder#setActionIndices(int...)
+ */
+ @NonNull
+ public int[] getActionIndices() {
+ return mActionIndices;
+ }
+
+ /**
+ * Returns the detected locale.
+ */
+ @Nullable
+ public ULocale getLocale() {
+ return mLocale;
+ }
+
+ /**
+ * Returns a bundle containing non-structured extra information about this event.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder(128);
+ out.append(this.getClass().getSimpleName());
+ out.append("{");
+ out.append("mEventCategory=").append(mEventCategory);
+ out.append(", mEventType=").append(mEventType);
+ out.append(", mEntityTypes=").append(Arrays.toString(mEntityTypes));
+ out.append(", mEventContext=").append(mEventContext);
+ out.append(", mResultId=").append(mResultId);
+ out.append(", mEventIndex=").append(mEventIndex);
+ out.append(", mExtras=").append(mExtras);
+ out.append(", mScores=").append(Arrays.toString(mScores));
+ out.append(", mModelName=").append(mModelName);
+ out.append(", mActionIndices=").append(Arrays.toString(mActionIndices));
+ toString(out);
+ out.append("}");
+ return out.toString();
+ }
+
+ /**
+ * Overrides this to append extra fields to the output of {@link #toString()}.
+ * <p>
+ * Extra fields should be formatted like this: ", {field_name}={field_value}".
+ */
+ void toString(StringBuilder out) {}
+
+ /**
+ * Returns a {@link SelectionEvent} equivalent of this event; or {@code null} if it can not be
+ * converted to a {@link SelectionEvent}.
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Nullable
+ public final SelectionEvent toSelectionEvent() {
+ final int invocationMethod;
+ switch (getEventCategory()) {
+ case TextClassifierEvent.CATEGORY_SELECTION:
+ invocationMethod = SelectionEvent.INVOCATION_MANUAL;
+ break;
+ case TextClassifierEvent.CATEGORY_LINKIFY:
+ invocationMethod = SelectionEvent.INVOCATION_LINK;
+ break;
+ default:
+ // Cannot be converted to a SelectionEvent.
+ return null;
+ }
+
+ final String entityType = getEntityTypes().length > 0
+ ? getEntityTypes()[0] : TextClassifier.TYPE_UNKNOWN;
+ final SelectionEvent out = new SelectionEvent(
+ /* absoluteStart= */ 0,
+ /* absoluteEnd= */ 0,
+ /* eventType= */0,
+ entityType,
+ SelectionEvent.INVOCATION_UNKNOWN,
+ SelectionEvent.NO_SIGNATURE);
+ out.setInvocationMethod(invocationMethod);
+
+ final TextClassificationContext eventContext = getEventContext();
+ if (eventContext != null) {
+ out.setTextClassificationSessionContext(getEventContext());
+ }
+ out.setSessionId(mHiddenTempSessionId);
+ final String resultId = getResultId();
+ out.setResultId(resultId == null ? SelectionEvent.NO_SIGNATURE : resultId);
+ out.setEventIndex(getEventIndex());
+
+
+ final int eventType;
+ switch (getEventType()) {
+ case TextClassifierEvent.TYPE_SELECTION_STARTED:
+ eventType = SelectionEvent.EVENT_SELECTION_STARTED;
+ break;
+ case TextClassifierEvent.TYPE_SELECTION_MODIFIED:
+ eventType = SelectionEvent.EVENT_SELECTION_MODIFIED;
+ break;
+ case TextClassifierEvent.TYPE_SMART_SELECTION_SINGLE:
+ eventType = SelectionEvent.EVENT_SMART_SELECTION_SINGLE;
+ break;
+ case TextClassifierEvent.TYPE_SMART_SELECTION_MULTI:
+ eventType = SelectionEvent.EVENT_SMART_SELECTION_MULTI;
+ break;
+ case TextClassifierEvent.TYPE_AUTO_SELECTION:
+ eventType = SelectionEvent.EVENT_AUTO_SELECTION;
+ break;
+ case TextClassifierEvent.TYPE_OVERTYPE:
+ eventType = SelectionEvent.ACTION_OVERTYPE;
+ break;
+ case TextClassifierEvent.TYPE_COPY_ACTION:
+ eventType = SelectionEvent.ACTION_COPY;
+ break;
+ case TextClassifierEvent.TYPE_PASTE_ACTION:
+ eventType = SelectionEvent.ACTION_PASTE;
+ break;
+ case TextClassifierEvent.TYPE_CUT_ACTION:
+ eventType = SelectionEvent.ACTION_CUT;
+ break;
+ case TextClassifierEvent.TYPE_SHARE_ACTION:
+ eventType = SelectionEvent.ACTION_SHARE;
+ break;
+ case TextClassifierEvent.TYPE_SMART_ACTION:
+ eventType = SelectionEvent.ACTION_SMART_SHARE;
+ break;
+ case TextClassifierEvent.TYPE_SELECTION_DRAG:
+ eventType = SelectionEvent.ACTION_DRAG;
+ break;
+ case TextClassifierEvent.TYPE_SELECTION_DESTROYED:
+ eventType = SelectionEvent.ACTION_ABANDON;
+ break;
+ case TextClassifierEvent.TYPE_OTHER_ACTION:
+ eventType = SelectionEvent.ACTION_OTHER;
+ break;
+ case TextClassifierEvent.TYPE_SELECT_ALL:
+ eventType = SelectionEvent.ACTION_SELECT_ALL;
+ break;
+ case TextClassifierEvent.TYPE_SELECTION_RESET:
+ eventType = SelectionEvent.ACTION_RESET;
+ break;
+ default:
+ eventType = 0;
+ break;
+ }
+ out.setEventType(eventType);
+
+ if (this instanceof TextClassifierEvent.TextSelectionEvent) {
+ final TextClassifierEvent.TextSelectionEvent selEvent =
+ (TextClassifierEvent.TextSelectionEvent) this;
+ // TODO: Ideally, we should have these fields in events of type
+ // TextClassifierEvent.TextLinkifyEvent events too but we're now past the API deadline
+ // and will have to do with these fields being set only in TextSelectionEvent events.
+ // Fix this at the next API bump.
+ out.setStart(selEvent.getRelativeWordStartIndex());
+ out.setEnd(selEvent.getRelativeWordEndIndex());
+ out.setSmartStart(selEvent.getRelativeSuggestedWordStartIndex());
+ out.setSmartEnd(selEvent.getRelativeSuggestedWordEndIndex());
+ }
+
+ return out;
+ }
+
+ /**
+ * Builder to build a text classifier event.
+ *
+ * @param <T> The subclass to be built.
+ */
+ public abstract static class Builder<T extends Builder<T>> {
+
+ private final int mEventCategory;
+ private final int mEventType;
+ private String[] mEntityTypes = new String[0];
+ @Nullable
+ private TextClassificationContext mEventContext;
+ @Nullable
+ private String mResultId;
+ private int mEventIndex;
+ private float[] mScores = new float[0];
+ @Nullable
+ private String mModelName;
+ private int[] mActionIndices = new int[0];
+ @Nullable
+ private ULocale mLocale;
+ @Nullable
+ private Bundle mExtras;
+
+ /**
+ * Creates a builder for building {@link TextClassifierEvent}s.
+ *
+ * @param eventCategory The event category. e.g. {@link #CATEGORY_SELECTION}
+ * @param eventType The event type. e.g. {@link #TYPE_SELECTION_STARTED}
+ */
+ private Builder(@Category int eventCategory, @Type int eventType) {
+ mEventCategory = eventCategory;
+ mEventType = eventType;
+ }
+
+ /**
+ * Sets the entity types. e.g. {@link TextClassifier#TYPE_ADDRESS}.
+ * <p>
+ * Supported types:
+ * <p>See {@link TextClassifier.EntityType}
+ * <p>See {@link ConversationAction.ActionType}
+ * <p>See {@link ULocale#toLanguageTag()}
+ */
+ @NonNull
+ public T setEntityTypes(@NonNull String... entityTypes) {
+ Objects.requireNonNull(entityTypes);
+ mEntityTypes = new String[entityTypes.length];
+ System.arraycopy(entityTypes, 0, mEntityTypes, 0, entityTypes.length);
+ return self();
+ }
+
+ /**
+ * Sets the event context.
+ */
+ @NonNull
+ public T setEventContext(@Nullable TextClassificationContext eventContext) {
+ mEventContext = eventContext;
+ return self();
+ }
+
+ /**
+ * Sets the id of the text classifier result related to this event.
+ */
+ @NonNull
+ public T setResultId(@Nullable String resultId) {
+ mResultId = resultId;
+ return self();
+ }
+
+ /**
+ * Sets the index of this event in the series of events it belongs to.
+ */
+ @NonNull
+ public T setEventIndex(int eventIndex) {
+ mEventIndex = eventIndex;
+ return self();
+ }
+
+ /**
+ * Sets the scores of the suggestions.
+ */
+ @NonNull
+ public T setScores(@NonNull float... scores) {
+ Objects.requireNonNull(scores);
+ mScores = new float[scores.length];
+ System.arraycopy(scores, 0, mScores, 0, scores.length);
+ return self();
+ }
+
+ /**
+ * Sets the model name string.
+ */
+ @NonNull
+ public T setModelName(@Nullable String modelVersion) {
+ mModelName = modelVersion;
+ return self();
+ }
+
+ /**
+ * Sets the indices of the actions involved in this event. Actions are usually returned by
+ * the text classifier in priority order with the most preferred action at index 0.
+ * These indices give an indication of the position of the actions that are being reported.
+ * <p>
+ * E.g.
+ * <pre>
+ * // 3 smart actions are shown at index 0, 1, 2 respectively in response to a link click.
+ * new TextClassifierEvent.Builder(CATEGORY_LINKIFY, TYPE_ACTIONS_SHOWN)
+ * .setEventIndex(0, 1, 2)
+ * ...
+ * .build();
+ *
+ * ...
+ *
+ * // Smart action at index 1 is activated.
+ * new TextClassifierEvent.Builder(CATEGORY_LINKIFY, TYPE_SMART_ACTION)
+ * .setEventIndex(1)
+ * ...
+ * .build();
+ * </pre>
+ *
+ * @see TextClassification#getActions()
+ */
+ @NonNull
+ public T setActionIndices(@NonNull int... actionIndices) {
+ mActionIndices = new int[actionIndices.length];
+ System.arraycopy(actionIndices, 0, mActionIndices, 0, actionIndices.length);
+ return self();
+ }
+
+ /**
+ * Sets the detected locale.
+ */
+ @NonNull
+ public T setLocale(@Nullable ULocale locale) {
+ mLocale = locale;
+ return self();
+ }
+
+ /**
+ * Sets a bundle containing non-structured extra information about the event.
+ *
+ * <p><b>NOTE: </b>Prefer to set only immutable values on the bundle otherwise, avoid
+ * updating the internals of this bundle as it may have unexpected consequences on the
+ * clients of the built event object. For similar reasons, avoid depending on mutable
+ * objects in this bundle.
+ */
+ @NonNull
+ public T setExtras(@NonNull Bundle extras) {
+ mExtras = Objects.requireNonNull(extras);
+ return self();
+ }
+
+ abstract T self();
+ }
+
+ /**
+ * This class represents events that are related to the smart text selection feature.
+ * <p>
+ * <pre>
+ * // User started a selection. e.g. "York" in text "New York City, NY".
+ * new TextSelectionEvent.Builder(TYPE_SELECTION_STARTED)
+ * .setEventContext(classificationContext)
+ * .setEventIndex(0)
+ * .build();
+ *
+ * // System smart-selects a recognized entity. e.g. "New York City".
+ * new TextSelectionEvent.Builder(TYPE_SMART_SELECTION_MULTI)
+ * .setEventContext(classificationContext)
+ * .setResultId(textSelection.getId())
+ * .setRelativeWordStartIndex(-1) // Goes back one word to "New" from "York".
+ * .setRelativeWordEndIndex(2) // Goes forward 2 words from "York" to start of ",".
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setEventIndex(1)
+ * .build();
+ *
+ * // User resets the selection to the original selection. i.e. "York".
+ * new TextSelectionEvent.Builder(TYPE_SELECTION_RESET)
+ * .setEventContext(classificationContext)
+ * .setResultId(textSelection.getId())
+ * .setRelativeSuggestedWordStartIndex(-1) // Repeated from above.
+ * .setRelativeSuggestedWordEndIndex(2) // Repeated from above.
+ * .setRelativeWordStartIndex(0) // Original selection is always at (0, 1].
+ * .setRelativeWordEndIndex(1)
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setEventIndex(2)
+ * .build();
+ *
+ * // User modified the selection. e.g. "New".
+ * new TextSelectionEvent.Builder(TYPE_SELECTION_MODIFIED)
+ * .setEventContext(classificationContext)
+ * .setResultId(textSelection.getId())
+ * .setRelativeSuggestedWordStartIndex(-1) // Repeated from above.
+ * .setRelativeSuggestedWordEndIndex(2) // Repeated from above.
+ * .setRelativeWordStartIndex(-1) // Goes backward one word from "York" to
+ * "New".
+ * .setRelativeWordEndIndex(0) // Goes backward one word to exclude "York".
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setEventIndex(3)
+ * .build();
+ *
+ * // Smart (contextual) actions (at indices, 0, 1, 2) presented to the user.
+ * // e.g. "Map", "Ride share", "Explore".
+ * new TextSelectionEvent.Builder(TYPE_ACTIONS_SHOWN)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setActionIndices(0, 1, 2)
+ * .setEventIndex(4)
+ * .build();
+ *
+ * // User chooses the "Copy" action.
+ * new TextSelectionEvent.Builder(TYPE_COPY_ACTION)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setEventIndex(5)
+ * .build();
+ *
+ * // User chooses smart action at index 1. i.e. "Ride share".
+ * new TextSelectionEvent.Builder(TYPE_SMART_ACTION)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setActionIndices(1)
+ * .setEventIndex(5)
+ * .build();
+ *
+ * // Selection dismissed.
+ * new TextSelectionEvent.Builder(TYPE_SELECTION_DESTROYED)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setEventIndex(6)
+ * .build();
+ * </pre>
+ * <p>
+ */
+ public static final class TextSelectionEvent extends TextClassifierEvent implements Parcelable {
+
+ @NonNull
+ public static final Creator<TextSelectionEvent> CREATOR =
+ new Creator<TextSelectionEvent>() {
+ @Override
+ public TextSelectionEvent createFromParcel(Parcel in) {
+ in.readInt(); // skip token, we already know this is a TextSelectionEvent
+ return new TextSelectionEvent(in);
+ }
+
+ @Override
+ public TextSelectionEvent[] newArray(int size) {
+ return new TextSelectionEvent[size];
+ }
+ };
+
+ final int mRelativeWordStartIndex;
+ final int mRelativeWordEndIndex;
+ final int mRelativeSuggestedWordStartIndex;
+ final int mRelativeSuggestedWordEndIndex;
+
+ private TextSelectionEvent(TextSelectionEvent.Builder builder) {
+ super(builder);
+ mRelativeWordStartIndex = builder.mRelativeWordStartIndex;
+ mRelativeWordEndIndex = builder.mRelativeWordEndIndex;
+ mRelativeSuggestedWordStartIndex = builder.mRelativeSuggestedWordStartIndex;
+ mRelativeSuggestedWordEndIndex = builder.mRelativeSuggestedWordEndIndex;
+ }
+
+ private TextSelectionEvent(Parcel in) {
+ super(in);
+ mRelativeWordStartIndex = in.readInt();
+ mRelativeWordEndIndex = in.readInt();
+ mRelativeSuggestedWordStartIndex = in.readInt();
+ mRelativeSuggestedWordEndIndex = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mRelativeWordStartIndex);
+ dest.writeInt(mRelativeWordEndIndex);
+ dest.writeInt(mRelativeSuggestedWordStartIndex);
+ dest.writeInt(mRelativeSuggestedWordEndIndex);
+ }
+
+ /**
+ * Returns the relative word index of the start of the selection.
+ */
+ public int getRelativeWordStartIndex() {
+ return mRelativeWordStartIndex;
+ }
+
+ /**
+ * Returns the relative word (exclusive) index of the end of the selection.
+ */
+ public int getRelativeWordEndIndex() {
+ return mRelativeWordEndIndex;
+ }
+
+ /**
+ * Returns the relative word index of the start of the smart selection.
+ */
+ public int getRelativeSuggestedWordStartIndex() {
+ return mRelativeSuggestedWordStartIndex;
+ }
+
+ /**
+ * Returns the relative word (exclusive) index of the end of the
+ * smart selection.
+ */
+ public int getRelativeSuggestedWordEndIndex() {
+ return mRelativeSuggestedWordEndIndex;
+ }
+
+ @Override
+ void toString(StringBuilder out) {
+ out.append(", getRelativeWordStartIndex=").append(mRelativeWordStartIndex);
+ out.append(", getRelativeWordEndIndex=").append(mRelativeWordEndIndex);
+ out.append(", getRelativeSuggestedWordStartIndex=")
+ .append(mRelativeSuggestedWordStartIndex);
+ out.append(", getRelativeSuggestedWordEndIndex=")
+ .append(mRelativeSuggestedWordEndIndex);
+ }
+
+ /**
+ * Builder class for {@link TextSelectionEvent}.
+ */
+ public static final class Builder extends
+ TextClassifierEvent.Builder<TextSelectionEvent.Builder> {
+ int mRelativeWordStartIndex;
+ int mRelativeWordEndIndex;
+ int mRelativeSuggestedWordStartIndex;
+ int mRelativeSuggestedWordEndIndex;
+
+ /**
+ * Creates a builder for building {@link TextSelectionEvent}s.
+ *
+ * @param eventType The event type. e.g. {@link #TYPE_SELECTION_STARTED}
+ */
+ public Builder(@Type int eventType) {
+ super(CATEGORY_SELECTION, eventType);
+ }
+
+ /**
+ * Sets the relative word index of the start of the selection.
+ */
+ @NonNull
+ public Builder setRelativeWordStartIndex(int relativeWordStartIndex) {
+ mRelativeWordStartIndex = relativeWordStartIndex;
+ return this;
+ }
+
+ /**
+ * Sets the relative word (exclusive) index of the end of the
+ * selection.
+ */
+ @NonNull
+ public Builder setRelativeWordEndIndex(int relativeWordEndIndex) {
+ mRelativeWordEndIndex = relativeWordEndIndex;
+ return this;
+ }
+
+ /**
+ * Sets the relative word index of the start of the smart
+ * selection.
+ */
+ @NonNull
+ public Builder setRelativeSuggestedWordStartIndex(int relativeSuggestedWordStartIndex) {
+ mRelativeSuggestedWordStartIndex = relativeSuggestedWordStartIndex;
+ return this;
+ }
+
+ /**
+ * Sets the relative word (exclusive) index of the end of the
+ * smart selection.
+ */
+ @NonNull
+ public Builder setRelativeSuggestedWordEndIndex(int relativeSuggestedWordEndIndex) {
+ mRelativeSuggestedWordEndIndex = relativeSuggestedWordEndIndex;
+ return this;
+ }
+
+ @Override
+ TextSelectionEvent.Builder self() {
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link TextSelectionEvent}.
+ */
+ @NonNull
+ public TextSelectionEvent build() {
+ return new TextSelectionEvent(this);
+ }
+ }
+ }
+
+ /**
+ * This class represents events that are related to the smart linkify feature.
+ * <p>
+ * <pre>
+ * // User clicked on a link.
+ * new TextLinkifyEvent.Builder(TYPE_LINK_CLICKED)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setEventIndex(0)
+ * .build();
+ *
+ * // Smart (contextual) actions presented to the user in response to a link click.
+ * new TextLinkifyEvent.Builder(TYPE_ACTIONS_SHOWN)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setActionIndices(range(textClassification.getActions().size()))
+ * .setEventIndex(1)
+ * .build();
+ *
+ * // User chooses smart action at index 0.
+ * new TextLinkifyEvent.Builder(TYPE_SMART_ACTION)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(textClassification.getEntity(0))
+ * .setScore(textClassification.getConfidenceScore(entityType))
+ * .setActionIndices(0)
+ * .setEventIndex(2)
+ * .build();
+ * </pre>
+ */
+ public static final class TextLinkifyEvent extends TextClassifierEvent implements Parcelable {
+
+ @NonNull
+ public static final Creator<TextLinkifyEvent> CREATOR =
+ new Creator<TextLinkifyEvent>() {
+ @Override
+ public TextLinkifyEvent createFromParcel(Parcel in) {
+ in.readInt(); // skip token, we already know this is a TextLinkifyEvent
+ return new TextLinkifyEvent(in);
+ }
+
+ @Override
+ public TextLinkifyEvent[] newArray(int size) {
+ return new TextLinkifyEvent[size];
+ }
+ };
+
+ private TextLinkifyEvent(Parcel in) {
+ super(in);
+ }
+
+ private TextLinkifyEvent(TextLinkifyEvent.Builder builder) {
+ super(builder);
+ }
+
+ /**
+ * Builder class for {@link TextLinkifyEvent}.
+ */
+ public static final class Builder
+ extends TextClassifierEvent.Builder<TextLinkifyEvent.Builder> {
+ /**
+ * Creates a builder for building {@link TextLinkifyEvent}s.
+ *
+ * @param eventType The event type. e.g. {@link #TYPE_SMART_ACTION}
+ */
+ public Builder(@Type int eventType) {
+ super(TextClassifierEvent.CATEGORY_LINKIFY, eventType);
+ }
+
+ @Override
+ Builder self() {
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link TextLinkifyEvent}.
+ */
+ @NonNull
+ public TextLinkifyEvent build() {
+ return new TextLinkifyEvent(this);
+ }
+ }
+ }
+
+ /**
+ * This class represents events that are related to the language detection feature.
+ * <p>
+ * <pre>
+ * // Translate action shown for foreign text.
+ * new LanguageDetectionEvent.Builder(TYPE_ACTIONS_SHOWN)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(language)
+ * .setScore(score)
+ * .setActionIndices(textClassification.getActions().indexOf(translateAction))
+ * .setEventIndex(0)
+ * .build();
+ *
+ * // Translate action selected.
+ * new LanguageDetectionEvent.Builder(TYPE_SMART_ACTION)
+ * .setEventContext(classificationContext)
+ * .setResultId(textClassification.getId())
+ * .setEntityTypes(language)
+ * .setScore(score)
+ * .setActionIndices(textClassification.getActions().indexOf(translateAction))
+ * .setEventIndex(1)
+ * .build();
+ */
+ public static final class LanguageDetectionEvent extends TextClassifierEvent
+ implements Parcelable {
+
+ @NonNull
+ public static final Creator<LanguageDetectionEvent> CREATOR =
+ new Creator<LanguageDetectionEvent>() {
+ @Override
+ public LanguageDetectionEvent createFromParcel(Parcel in) {
+ // skip token, we already know this is a LanguageDetectionEvent.
+ in.readInt();
+ return new LanguageDetectionEvent(in);
+ }
+
+ @Override
+ public LanguageDetectionEvent[] newArray(int size) {
+ return new LanguageDetectionEvent[size];
+ }
+ };
+
+ private LanguageDetectionEvent(Parcel in) {
+ super(in);
+ }
+
+ private LanguageDetectionEvent(LanguageDetectionEvent.Builder builder) {
+ super(builder);
+ }
+
+ /**
+ * Builder class for {@link LanguageDetectionEvent}.
+ */
+ public static final class Builder
+ extends TextClassifierEvent.Builder<LanguageDetectionEvent.Builder> {
+
+ /**
+ * Creates a builder for building {@link TextSelectionEvent}s.
+ *
+ * @param eventType The event type. e.g. {@link #TYPE_SMART_ACTION}
+ */
+ public Builder(@Type int eventType) {
+ super(TextClassifierEvent.CATEGORY_LANGUAGE_DETECTION, eventType);
+ }
+
+ @Override
+ Builder self() {
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link LanguageDetectionEvent}.
+ */
+ @NonNull
+ public LanguageDetectionEvent build() {
+ return new LanguageDetectionEvent(this);
+ }
+ }
+ }
+
+ /**
+ * This class represents events that are related to the conversation actions feature.
+ * <p>
+ * <pre>
+ * // Conversation (contextual) actions/replies generated.
+ * new ConversationActionsEvent.Builder(TYPE_ACTIONS_GENERATED)
+ * .setEventContext(classificationContext)
+ * .setResultId(conversationActions.getId())
+ * .setEntityTypes(getTypes(conversationActions))
+ * .setActionIndices(range(conversationActions.getActions().size()))
+ * .setEventIndex(0)
+ * .build();
+ *
+ * // Conversation actions/replies presented to user.
+ * new ConversationActionsEvent.Builder(TYPE_ACTIONS_SHOWN)
+ * .setEventContext(classificationContext)
+ * .setResultId(conversationActions.getId())
+ * .setEntityTypes(getTypes(conversationActions))
+ * .setActionIndices(range(conversationActions.getActions().size()))
+ * .setEventIndex(1)
+ * .build();
+ *
+ * // User clicked the "Reply" button to compose their custom reply.
+ * new ConversationActionsEvent.Builder(TYPE_MANUAL_REPLY)
+ * .setEventContext(classificationContext)
+ * .setResultId(conversationActions.getId())
+ * .setEventIndex(2)
+ * .build();
+ *
+ * // User selected a smart (contextual) action/reply.
+ * new ConversationActionsEvent.Builder(TYPE_SMART_ACTION)
+ * .setEventContext(classificationContext)
+ * .setResultId(conversationActions.getId())
+ * .setEntityTypes(conversationActions.get(1).getType())
+ * .setScore(conversationAction.get(1).getConfidenceScore())
+ * .setActionIndices(1)
+ * .setEventIndex(2)
+ * .build();
+ * </pre>
+ */
+ public static final class ConversationActionsEvent extends TextClassifierEvent
+ implements Parcelable {
+
+ @NonNull
+ public static final Creator<ConversationActionsEvent> CREATOR =
+ new Creator<ConversationActionsEvent>() {
+ @Override
+ public ConversationActionsEvent createFromParcel(Parcel in) {
+ // skip token, we already know this is a ConversationActionsEvent.
+ in.readInt();
+ return new ConversationActionsEvent(in);
+ }
+
+ @Override
+ public ConversationActionsEvent[] newArray(int size) {
+ return new ConversationActionsEvent[size];
+ }
+ };
+
+ private ConversationActionsEvent(Parcel in) {
+ super(in);
+ }
+
+ private ConversationActionsEvent(ConversationActionsEvent.Builder builder) {
+ super(builder);
+ }
+
+ /**
+ * Builder class for {@link ConversationActionsEvent}.
+ */
+ public static final class Builder
+ extends TextClassifierEvent.Builder<ConversationActionsEvent.Builder> {
+ /**
+ * Creates a builder for building {@link TextSelectionEvent}s.
+ *
+ * @param eventType The event type. e.g. {@link #TYPE_SMART_ACTION}
+ */
+ public Builder(@Type int eventType) {
+ super(TextClassifierEvent.CATEGORY_CONVERSATION_ACTIONS, eventType);
+ }
+
+ @Override
+ Builder self() {
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link ConversationActionsEvent}.
+ */
+ @NonNull
+ public ConversationActionsEvent build() {
+ return new ConversationActionsEvent(this);
+ }
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextClassifierPerfTest.java b/android/view/textclassifier/TextClassifierPerfTest.java
new file mode 100644
index 0000000..324def8
--- /dev/null
+++ b/android/view/textclassifier/TextClassifierPerfTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view.textclassifier;
+
+import android.content.Context;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.service.textclassifier.TextClassifierService;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Collections;
+
+@LargeTest
+public class TextClassifierPerfTest {
+ private static final String TEXT = " Oh hi Mark, the number is (323) 654-6192.\n"
+ + "Anyway, I'll meet you at 1600 Pennsylvania Avenue NW.\n"
+ + "My flight is LX 38 and I'll arrive at 8:00pm.\n"
+ + "Also, check out www.google.com.\n";
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private TextClassifier mTextClassifier;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ mTextClassifier = TextClassifierService.getDefaultTextClassifierImplementation(context);
+ }
+
+ @Test
+ public void testClassifyText() {
+ TextClassification.Request request =
+ new TextClassification.Request.Builder(TEXT, 0, TEXT.length()).build();
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mTextClassifier.classifyText(request);
+ }
+ }
+
+ @Test
+ public void testSuggestSelection() {
+ // Trying to select the phone number.
+ TextSelection.Request request =
+ new TextSelection.Request.Builder(
+ TEXT,
+ /* startIndex= */ 28,
+ /* endIndex= */29)
+ .build();
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mTextClassifier.suggestSelection(request);
+ }
+ }
+
+ @Test
+ public void testGenerateLinks() {
+ TextLinks.Request request =
+ new TextLinks.Request.Builder(TEXT).build();
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mTextClassifier.generateLinks(request);
+ }
+ }
+
+ @Test
+ public void testSuggestConversationActions() {
+ ConversationActions.Message message =
+ new ConversationActions.Message.Builder(
+ ConversationActions.Message.PERSON_USER_OTHERS)
+ .setText(TEXT)
+ .build();
+ ConversationActions.Request request = new ConversationActions.Request.Builder(
+ Collections.singletonList(message))
+ .build();
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mTextClassifier.suggestConversationActions(request);
+ }
+ }
+
+ @Test
+ public void testDetectLanguage() {
+ TextLanguage.Request request = new TextLanguage.Request.Builder(TEXT).build();
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ mTextClassifier.detectLanguage(request);
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextLanguage.java b/android/view/textclassifier/TextLanguage.java
new file mode 100644
index 0000000..1e8253d
--- /dev/null
+++ b/android/view/textclassifier/TextLanguage.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.icu.util.ULocale;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Represents the result of language detection of a piece of text.
+ * <p>
+ * This contains a list of locales, each paired with a confidence score, sorted in decreasing
+ * order of those scores. E.g., for a given input text, the model may return
+ * {@code [<"en", 0.85>, <"fr", 0.15>]}. This sample result means the model reports that it is
+ * 85% likely that the entire text is in English and 15% likely that the entire text is in French,
+ * etc. It does not mean that 85% of the input is in English and 15% is in French.
+ */
+public final class TextLanguage implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<TextLanguage> CREATOR = new Creator<TextLanguage>() {
+ @Override
+ public TextLanguage createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public TextLanguage[] newArray(int size) {
+ return new TextLanguage[size];
+ }
+ };
+
+ static final TextLanguage EMPTY = new Builder().build();
+
+ @Nullable private final String mId;
+ private final EntityConfidence mEntityConfidence;
+ private final Bundle mBundle;
+
+ private TextLanguage(
+ @Nullable String id,
+ EntityConfidence entityConfidence,
+ Bundle bundle) {
+ mId = id;
+ mEntityConfidence = entityConfidence;
+ mBundle = bundle;
+ }
+
+ /**
+ * Returns the id, if one exists, for this object.
+ */
+ @Nullable
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the number of possible locales for the processed text.
+ */
+ @IntRange(from = 0)
+ public int getLocaleHypothesisCount() {
+ return mEntityConfidence.getEntities().size();
+ }
+
+ /**
+ * Returns the language locale at the specified index. Locales are ordered from high
+ * confidence to low confidence.
+ * <p>
+ * See {@link #getLocaleHypothesisCount()} for the number of locales available.
+ *
+ * @throws IndexOutOfBoundsException if the specified index is out of range.
+ */
+ @NonNull
+ public ULocale getLocale(int index) {
+ return ULocale.forLanguageTag(mEntityConfidence.getEntities().get(index));
+ }
+
+ /**
+ * Returns the confidence score for the specified language locale. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the locale was not found for
+ * the processed text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(@NonNull ULocale locale) {
+ return mEntityConfidence.getConfidenceScore(locale.toLanguageTag());
+ }
+
+ /**
+ * Returns a bundle containing non-structured extra information about this result. What is
+ * returned in the extras is specific to the {@link TextClassifier} implementation.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mBundle;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "TextLanguage {id=%s, locales=%s, bundle=%s}",
+ mId, mEntityConfidence, mBundle);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeBundle(mBundle);
+ }
+
+ private static TextLanguage readFromParcel(Parcel in) {
+ return new TextLanguage(
+ in.readString(),
+ EntityConfidence.CREATOR.createFromParcel(in),
+ in.readBundle());
+ }
+
+ /**
+ * Builder used to build TextLanguage objects.
+ */
+ public static final class Builder {
+
+ @Nullable private String mId;
+ private final Map<String, Float> mEntityConfidenceMap = new ArrayMap<>();
+ @Nullable private Bundle mBundle;
+
+ /**
+ * Sets a language locale for the processed text and assigns a confidence score. If the
+ * locale has already been set, this updates it.
+ *
+ * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
+ * 0 implies the locale does not exist for the processed text.
+ * Values greater than 1 are clamped to 1.
+ */
+ @NonNull
+ public Builder putLocale(
+ @NonNull ULocale locale,
+ @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
+ Preconditions.checkNotNull(locale);
+ mEntityConfidenceMap.put(locale.toLanguageTag(), confidenceScore);
+ return this;
+ }
+
+ /**
+ * Sets an optional id for the TextLanguage object.
+ */
+ @NonNull
+ public Builder setId(@Nullable String id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets a bundle containing non-structured extra information about the TextLanguage object.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle bundle) {
+ mBundle = Preconditions.checkNotNull(bundle);
+ return this;
+ }
+
+ /**
+ * Builds and returns a new TextLanguage object.
+ * <p>
+ * If necessary, this method will verify fields, clamp them, and make them immutable.
+ */
+ @NonNull
+ public TextLanguage build() {
+ mBundle = mBundle == null ? Bundle.EMPTY : mBundle;
+ return new TextLanguage(
+ mId,
+ new EntityConfidence(mEntityConfidenceMap),
+ mBundle);
+ }
+ }
+
+ /**
+ * A request object for detecting the language of a piece of text.
+ */
+ public static final class Request implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<Request> CREATOR = new Creator<Request>() {
+ @Override
+ public Request createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public Request[] newArray(int size) {
+ return new Request[size];
+ }
+ };
+
+ private final CharSequence mText;
+ private final Bundle mExtra;
+ @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ private Request(CharSequence text, Bundle bundle) {
+ mText = text;
+ mExtra = bundle;
+ }
+
+ /**
+ * Returns the text to process.
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the name of the package that sent this request.
+ * This returns null if no calling package name is set.
+ */
+ @Nullable
+ public String getCallingPackageName() {
+ return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setSystemTextClassifierMetadata(
+ @Nullable SystemTextClassifierMetadata systemTcMetadata) {
+ mSystemTcMetadata = systemTcMetadata;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns a bundle containing non-structured extra information about this request.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtra;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mText);
+ dest.writeBundle(mExtra);
+ dest.writeParcelable(mSystemTcMetadata, flags);
+ }
+
+ private static Request readFromParcel(Parcel in) {
+ final CharSequence text = in.readCharSequence();
+ final Bundle extra = in.readBundle();
+ final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null);
+
+ final Request request = new Request(text, extra);
+ request.setSystemTextClassifierMetadata(systemTcMetadata);
+ return request;
+ }
+
+ /**
+ * A builder for building TextLanguage requests.
+ */
+ public static final class Builder {
+
+ private final CharSequence mText;
+ @Nullable private Bundle mBundle;
+
+ /**
+ * Creates a builder to build TextLanguage requests.
+ *
+ * @param text the text to process.
+ */
+ public Builder(@NonNull CharSequence text) {
+ mText = Preconditions.checkNotNull(text);
+ }
+
+ /**
+ * Sets a bundle containing non-structured extra information about the request.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle bundle) {
+ mBundle = Preconditions.checkNotNull(bundle);
+ return this;
+ }
+
+ /**
+ * Builds and returns a new TextLanguage request object.
+ * <p>
+ * If necessary, this method will verify fields, clamp them, and make them immutable.
+ */
+ @NonNull
+ public Request build() {
+ return new Request(mText.toString(), mBundle == null ? Bundle.EMPTY : mBundle);
+ }
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
new file mode 100644
index 0000000..dea3a90
--- /dev/null
+++ b/android/view/textclassifier/TextLinks.java
@@ -0,0 +1,758 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Spannable;
+import android.text.method.MovementMethod;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.view.View;
+import android.view.textclassifier.TextClassifier.EntityConfig;
+import android.view.textclassifier.TextClassifier.EntityType;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A collection of links, representing subsequences of text and the entity types (phone number,
+ * address, url, etc) they may be.
+ */
+public final class TextLinks implements Parcelable {
+
+ /**
+ * Return status of an attempt to apply TextLinks to text.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATUS_LINKS_APPLIED, STATUS_NO_LINKS_FOUND, STATUS_NO_LINKS_APPLIED,
+ STATUS_DIFFERENT_TEXT, STATUS_UNSUPPORTED_CHARACTER})
+ public @interface Status {}
+
+ /** Links were successfully applied to the text. */
+ public static final int STATUS_LINKS_APPLIED = 0;
+
+ /** No links exist to apply to text. Links count is zero. */
+ public static final int STATUS_NO_LINKS_FOUND = 1;
+
+ /** No links applied to text. The links were filtered out. */
+ public static final int STATUS_NO_LINKS_APPLIED = 2;
+
+ /** The specified text does not match the text used to generate the links. */
+ public static final int STATUS_DIFFERENT_TEXT = 3;
+
+ /** The specified text contains unsupported characters. */
+ public static final int STATUS_UNSUPPORTED_CHARACTER = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({APPLY_STRATEGY_IGNORE, APPLY_STRATEGY_REPLACE})
+ public @interface ApplyStrategy {}
+
+ /**
+ * Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to
+ * be applied to. Do not apply the TextLinkSpan.
+ */
+ public static final int APPLY_STRATEGY_IGNORE = 0;
+
+ /**
+ * Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be
+ * applied to.
+ */
+ public static final int APPLY_STRATEGY_REPLACE = 1;
+
+ private final String mFullText;
+ private final List<TextLink> mLinks;
+ private final Bundle mExtras;
+
+ private TextLinks(String fullText, ArrayList<TextLink> links, Bundle extras) {
+ mFullText = fullText;
+ mLinks = Collections.unmodifiableList(links);
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the text that was used to generate these links.
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mFullText;
+ }
+
+ /**
+ * Returns an unmodifiable Collection of the links.
+ */
+ @NonNull
+ public Collection<TextLink> getLinks() {
+ return mLinks;
+ }
+
+ /**
+ * Returns the extended data.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Annotates the given text with the generated links. It will fail if the provided text doesn't
+ * match the original text used to create the TextLinks.
+ *
+ * <p><strong>NOTE: </strong>It may be necessary to set a LinkMovementMethod on the TextView
+ * widget to properly handle links. See {@link TextView#setMovementMethod(MovementMethod)}
+ *
+ * @param text the text to apply the links to. Must match the original text
+ * @param applyStrategy the apply strategy used to determine how to apply links to text.
+ * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE}
+ * @param spanFactory a custom span factory for converting TextLinks to TextLinkSpans.
+ * Set to {@code null} to use the default span factory.
+ *
+ * @return a status code indicating whether or not the links were successfully applied
+ * e.g. {@link #STATUS_LINKS_APPLIED}
+ */
+ @Status
+ public int apply(
+ @NonNull Spannable text,
+ @ApplyStrategy int applyStrategy,
+ @Nullable Function<TextLink, TextLinkSpan> spanFactory) {
+ Objects.requireNonNull(text);
+ return new TextLinksParams.Builder()
+ .setApplyStrategy(applyStrategy)
+ .setSpanFactory(spanFactory)
+ .build()
+ .apply(text, this);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "TextLinks{fullText=%s, links=%s}", mFullText, mLinks);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mFullText);
+ dest.writeTypedList(mLinks);
+ dest.writeBundle(mExtras);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextLinks> CREATOR =
+ new Parcelable.Creator<TextLinks>() {
+ @Override
+ public TextLinks createFromParcel(Parcel in) {
+ return new TextLinks(in);
+ }
+
+ @Override
+ public TextLinks[] newArray(int size) {
+ return new TextLinks[size];
+ }
+ };
+
+ private TextLinks(Parcel in) {
+ mFullText = in.readString();
+ mLinks = in.createTypedArrayList(TextLink.CREATOR);
+ mExtras = in.readBundle();
+ }
+
+ /**
+ * A link, identifying a substring of text and possible entity types for it.
+ */
+ public static final class TextLink implements Parcelable {
+ private final EntityConfidence mEntityScores;
+ private final int mStart;
+ private final int mEnd;
+ private final Bundle mExtras;
+ @Nullable private final URLSpan mUrlSpan;
+
+ /**
+ * Create a new TextLink.
+ *
+ * @param start The start index of the identified subsequence
+ * @param end The end index of the identified subsequence
+ * @param entityConfidence A mapping of entity type to confidence score
+ * @param extras A bundle containing custom data related to this TextLink
+ * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled
+ *
+ * @throws IllegalArgumentException if {@code entityConfidence} is null or empty
+ * @throws IllegalArgumentException if {@code start} is greater than {@code end}
+ */
+ private TextLink(int start, int end, @NonNull EntityConfidence entityConfidence,
+ @NonNull Bundle extras, @Nullable URLSpan urlSpan) {
+ Objects.requireNonNull(entityConfidence);
+ Preconditions.checkArgument(!entityConfidence.getEntities().isEmpty());
+ Preconditions.checkArgument(start <= end);
+ Objects.requireNonNull(extras);
+ mStart = start;
+ mEnd = end;
+ mEntityScores = entityConfidence;
+ mUrlSpan = urlSpan;
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the start index of this link in the original text.
+ *
+ * @return the start index
+ */
+ public int getStart() {
+ return mStart;
+ }
+
+ /**
+ * Returns the end index of this link in the original text.
+ *
+ * @return the end index
+ */
+ public int getEnd() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the number of entity types that have confidence scores.
+ *
+ * @return the entity count
+ */
+ public int getEntityCount() {
+ return mEntityScores.getEntities().size();
+ }
+
+ /**
+ * Returns the entity type at a given index. Entity types are sorted by confidence.
+ *
+ * @return the entity type at the provided index
+ */
+ @NonNull public @EntityType String getEntity(int index) {
+ return mEntityScores.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for a particular entity type.
+ *
+ * @param entityType the entity type
+ */
+ public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore(
+ @EntityType String entityType) {
+ return mEntityScores.getConfidenceScore(entityType);
+ }
+
+ /**
+ * Returns a bundle containing custom data related to this TextLink.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "TextLink{start=%s, end=%s, entityScores=%s, urlSpan=%s}",
+ mStart, mEnd, mEntityScores, mUrlSpan);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mEntityScores.writeToParcel(dest, flags);
+ dest.writeInt(mStart);
+ dest.writeInt(mEnd);
+ dest.writeBundle(mExtras);
+ }
+
+ private static TextLink readFromParcel(Parcel in) {
+ final EntityConfidence entityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ final int start = in.readInt();
+ final int end = in.readInt();
+ final Bundle extras = in.readBundle();
+ return new TextLink(start, end, entityConfidence, extras, null /* urlSpan */);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextLink> CREATOR =
+ new Parcelable.Creator<TextLink>() {
+ @Override
+ public TextLink createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public TextLink[] newArray(int size) {
+ return new TextLink[size];
+ }
+ };
+ }
+
+ /**
+ * A request object for generating TextLinks.
+ */
+ public static final class Request implements Parcelable {
+
+ private final CharSequence mText;
+ @Nullable private final LocaleList mDefaultLocales;
+ @Nullable private final EntityConfig mEntityConfig;
+ private final boolean mLegacyFallback;
+ private final Bundle mExtras;
+ @Nullable private final ZonedDateTime mReferenceTime;
+ @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ private Request(
+ CharSequence text,
+ LocaleList defaultLocales,
+ EntityConfig entityConfig,
+ boolean legacyFallback,
+ ZonedDateTime referenceTime,
+ Bundle extras) {
+ mText = text;
+ mDefaultLocales = defaultLocales;
+ mEntityConfig = entityConfig;
+ mLegacyFallback = legacyFallback;
+ mReferenceTime = referenceTime;
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the text to generate links for.
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns an ordered list of locale preferences that can be used to disambiguate the
+ * provided text.
+ */
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ /**
+ * Returns the config representing the set of entities to look for
+ *
+ * @see Builder#setEntityConfig(EntityConfig)
+ */
+ @Nullable
+ public EntityConfig getEntityConfig() {
+ return mEntityConfig;
+ }
+
+ /**
+ * Returns whether the TextClassifier can fallback to legacy links if smart linkify is
+ * disabled.
+ * <strong>Note: </strong>This is not parcelled.
+ * @hide
+ */
+ public boolean isLegacyFallback() {
+ return mLegacyFallback;
+ }
+
+ /**
+ * Returns reference time based on which relative dates (e.g. "tomorrow") should be
+ * interpreted.
+ */
+ @Nullable
+ public ZonedDateTime getReferenceTime() {
+ return mReferenceTime;
+ }
+
+ /**
+ * Returns the name of the package that sent this request.
+ * This returns {@code null} if no calling package name is set.
+ */
+ @Nullable
+ public String getCallingPackageName() {
+ return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setSystemTextClassifierMetadata(
+ @Nullable SystemTextClassifierMetadata systemTcMetadata) {
+ mSystemTcMetadata = systemTcMetadata;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns the extended data.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * A builder for building TextLinks requests.
+ */
+ public static final class Builder {
+
+ private final CharSequence mText;
+
+ @Nullable private LocaleList mDefaultLocales;
+ @Nullable private EntityConfig mEntityConfig;
+ private boolean mLegacyFallback = true; // Use legacy fall back by default.
+ @Nullable private Bundle mExtras;
+ @Nullable private ZonedDateTime mReferenceTime;
+
+ public Builder(@NonNull CharSequence text) {
+ mText = Objects.requireNonNull(text);
+ }
+
+ /**
+ * Sets ordered list of locale preferences that may be used to disambiguate the
+ * provided text.
+ *
+ * @param defaultLocales ordered list of locale preferences that may be used to
+ * disambiguate the provided text. If no locale preferences exist,
+ * set this to null or an empty locale list.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /**
+ * Sets the entity configuration to use. This determines what types of entities the
+ * TextClassifier will look for.
+ * Set to {@code null} for the default entity config and teh TextClassifier will
+ * automatically determine what links to generate.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setEntityConfig(@Nullable EntityConfig entityConfig) {
+ mEntityConfig = entityConfig;
+ return this;
+ }
+
+ /**
+ * Sets whether the TextClassifier can fallback to legacy links if smart linkify is
+ * disabled.
+ *
+ * <p><strong>Note: </strong>This is not parcelled.
+ *
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ public Builder setLegacyFallback(boolean legacyFallback) {
+ mLegacyFallback = legacyFallback;
+ return this;
+ }
+
+ /**
+ * Sets the extended data.
+ *
+ * @return this builder
+ */
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Sets the reference time based on which relative dates (e.g.
+ * "tomorrow") should be interpreted.
+ *
+ * @param referenceTime reference time based on which relative dates. This should
+ * usually be the time when the text was originally composed.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
+ mReferenceTime = referenceTime;
+ return this;
+ }
+
+ /**
+ * Builds and returns the request object.
+ */
+ @NonNull
+ public Request build() {
+ return new Request(
+ mText, mDefaultLocales, mEntityConfig,
+ mLegacyFallback, mReferenceTime,
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText.toString());
+ dest.writeParcelable(mDefaultLocales, flags);
+ dest.writeParcelable(mEntityConfig, flags);
+ dest.writeBundle(mExtras);
+ dest.writeString(mReferenceTime == null ? null : mReferenceTime.toString());
+ dest.writeParcelable(mSystemTcMetadata, flags);
+ }
+
+ private static Request readFromParcel(Parcel in) {
+ final String text = in.readString();
+ final LocaleList defaultLocales = in.readParcelable(null);
+ final EntityConfig entityConfig = in.readParcelable(null);
+ final Bundle extras = in.readBundle();
+ final String referenceTimeString = in.readString();
+ final ZonedDateTime referenceTime = referenceTimeString == null
+ ? null : ZonedDateTime.parse(referenceTimeString);
+ final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null);
+
+ final Request request = new Request(text, defaultLocales, entityConfig,
+ /* legacyFallback= */ true, referenceTime, extras);
+ request.setSystemTextClassifierMetadata(systemTcMetadata);
+ return request;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR =
+ new Parcelable.Creator<Request>() {
+ @Override
+ public Request createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public Request[] newArray(int size) {
+ return new Request[size];
+ }
+ };
+ }
+
+ /**
+ * A ClickableSpan for a TextLink.
+ *
+ * <p>Applies only to TextViews.
+ */
+ public static class TextLinkSpan extends ClickableSpan {
+
+ /**
+ * How the clickspan is triggered.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({INVOCATION_METHOD_UNSPECIFIED, INVOCATION_METHOD_TOUCH,
+ INVOCATION_METHOD_KEYBOARD})
+ public @interface InvocationMethod {}
+
+ /** @hide */
+ public static final int INVOCATION_METHOD_UNSPECIFIED = -1;
+ /** @hide */
+ public static final int INVOCATION_METHOD_TOUCH = 0;
+ /** @hide */
+ public static final int INVOCATION_METHOD_KEYBOARD = 1;
+
+ private final TextLink mTextLink;
+
+ public TextLinkSpan(@NonNull TextLink textLink) {
+ mTextLink = textLink;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ onClick(widget, INVOCATION_METHOD_UNSPECIFIED);
+ }
+
+ /** @hide */
+ public final void onClick(View widget, @InvocationMethod int invocationMethod) {
+ if (widget instanceof TextView) {
+ final TextView textView = (TextView) widget;
+ final Context context = textView.getContext();
+ if (TextClassificationManager.getSettings(context).isSmartLinkifyEnabled()) {
+ switch (invocationMethod) {
+ case INVOCATION_METHOD_TOUCH:
+ textView.requestActionMode(this);
+ break;
+ case INVOCATION_METHOD_KEYBOARD:// fall though
+ case INVOCATION_METHOD_UNSPECIFIED: // fall through
+ default:
+ textView.handleClick(this);
+ break;
+ }
+ } else {
+ if (mTextLink.mUrlSpan != null) {
+ mTextLink.mUrlSpan.onClick(textView);
+ } else {
+ textView.handleClick(this);
+ }
+ }
+ }
+ }
+
+ public final TextLink getTextLink() {
+ return mTextLink;
+ }
+
+ /** @hide */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ @Nullable
+ public final String getUrl() {
+ if (mTextLink.mUrlSpan != null) {
+ return mTextLink.mUrlSpan.getURL();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A builder to construct a TextLinks instance.
+ */
+ public static final class Builder {
+ private final String mFullText;
+ private final ArrayList<TextLink> mLinks;
+ private Bundle mExtras;
+
+ /**
+ * Create a new TextLinks.Builder.
+ *
+ * @param fullText The full text to annotate with links
+ */
+ public Builder(@NonNull String fullText) {
+ mFullText = Objects.requireNonNull(fullText);
+ mLinks = new ArrayList<>();
+ }
+
+ /**
+ * Adds a TextLink.
+ *
+ * @param start The start index of the identified subsequence
+ * @param end The end index of the identified subsequence
+ * @param entityScores A mapping of entity type to confidence score
+ *
+ * @throws IllegalArgumentException if entityScores is null or empty.
+ */
+ @NonNull
+ public Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores) {
+ return addLink(start, end, entityScores, Bundle.EMPTY, null);
+ }
+
+ /**
+ * Adds a TextLink.
+ *
+ * @see #addLink(int, int, Map)
+ * @param extras An optional bundle containing custom data related to this TextLink
+ */
+ @NonNull
+ public Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores,
+ @NonNull Bundle extras) {
+ return addLink(start, end, entityScores, extras, null);
+ }
+
+ /**
+ * Adds a TextLink.
+ *
+ * @see #addLink(int, int, Map)
+ * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled.
+ */
+ @NonNull
+ Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores,
+ @Nullable URLSpan urlSpan) {
+ return addLink(start, end, entityScores, Bundle.EMPTY, urlSpan);
+ }
+
+ private Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores,
+ @NonNull Bundle extras, @Nullable URLSpan urlSpan) {
+ mLinks.add(new TextLink(
+ start, end, new EntityConfidence(entityScores), extras, urlSpan));
+ return this;
+ }
+
+ /**
+ * Removes all {@link TextLink}s.
+ */
+ @NonNull
+ public Builder clearTextLinks() {
+ mLinks.clear();
+ return this;
+ }
+
+ /**
+ * Sets the extended data.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Constructs a TextLinks instance.
+ *
+ * @return the constructed TextLinks
+ */
+ @NonNull
+ public TextLinks build() {
+ return new TextLinks(mFullText, mLinks,
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextLinksParams.java b/android/view/textclassifier/TextLinksParams.java
new file mode 100644
index 0000000..f12b0d7
--- /dev/null
+++ b/android/view/textclassifier/TextLinksParams.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.Spannable;
+import android.text.style.ClickableSpan;
+import android.text.util.Linkify;
+import android.text.util.Linkify.LinkifyMask;
+import android.view.textclassifier.TextLinks.TextLink;
+import android.view.textclassifier.TextLinks.TextLinkSpan;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Parameters for generating and applying links.
+ * @hide
+ */
+public final class TextLinksParams {
+
+ /**
+ * A function to create spans from TextLinks.
+ */
+ private static final Function<TextLink, TextLinkSpan> DEFAULT_SPAN_FACTORY =
+ textLink -> new TextLinkSpan(textLink);
+
+ @TextLinks.ApplyStrategy
+ private final int mApplyStrategy;
+ private final Function<TextLink, TextLinkSpan> mSpanFactory;
+ private final TextClassifier.EntityConfig mEntityConfig;
+
+ private TextLinksParams(
+ @TextLinks.ApplyStrategy int applyStrategy,
+ Function<TextLink, TextLinkSpan> spanFactory) {
+ mApplyStrategy = applyStrategy;
+ mSpanFactory = spanFactory;
+ mEntityConfig = TextClassifier.EntityConfig.createWithHints(null);
+ }
+
+ /**
+ * Returns a new TextLinksParams object based on the specified link mask.
+ *
+ * @param mask the link mask
+ * e.g. {@link LinkifyMask#PHONE_NUMBERS} | {@link LinkifyMask#EMAIL_ADDRESSES}
+ * @hide
+ */
+ @NonNull
+ public static TextLinksParams fromLinkMask(@LinkifyMask int mask) {
+ final List<String> entitiesToFind = new ArrayList<>();
+ if ((mask & Linkify.WEB_URLS) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_URL);
+ }
+ if ((mask & Linkify.EMAIL_ADDRESSES) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_EMAIL);
+ }
+ if ((mask & Linkify.PHONE_NUMBERS) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_PHONE);
+ }
+ if ((mask & Linkify.MAP_ADDRESSES) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_ADDRESS);
+ }
+ return new TextLinksParams.Builder().setEntityConfig(
+ TextClassifier.EntityConfig.createWithExplicitEntityList(entitiesToFind))
+ .build();
+ }
+
+ /**
+ * Returns the entity config used to determine what entity types to generate.
+ */
+ @NonNull
+ public TextClassifier.EntityConfig getEntityConfig() {
+ return mEntityConfig;
+ }
+
+ /**
+ * Annotates the given text with the generated links. It will fail if the provided text doesn't
+ * match the original text used to crete the TextLinks.
+ *
+ * @param text the text to apply the links to. Must match the original text
+ * @param textLinks the links to apply to the text
+ *
+ * @return a status code indicating whether or not the links were successfully applied
+ * @hide
+ */
+ @TextLinks.Status
+ public int apply(@NonNull Spannable text, @NonNull TextLinks textLinks) {
+ Objects.requireNonNull(text);
+ Objects.requireNonNull(textLinks);
+
+ final String textString = text.toString();
+
+ if (Linkify.containsUnsupportedCharacters(textString)) {
+ // Do not apply links to text containing unsupported characters.
+ android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
+ return TextLinks.STATUS_UNSUPPORTED_CHARACTER;
+ }
+
+ if (!textString.startsWith(textLinks.getText().toString())) {
+ return TextLinks.STATUS_DIFFERENT_TEXT;
+ }
+ if (textLinks.getLinks().isEmpty()) {
+ return TextLinks.STATUS_NO_LINKS_FOUND;
+ }
+
+ int applyCount = 0;
+ for (TextLink link : textLinks.getLinks()) {
+ final TextLinkSpan span = mSpanFactory.apply(link);
+ if (span != null) {
+ final ClickableSpan[] existingSpans = text.getSpans(
+ link.getStart(), link.getEnd(), ClickableSpan.class);
+ if (existingSpans.length > 0) {
+ if (mApplyStrategy == TextLinks.APPLY_STRATEGY_REPLACE) {
+ for (ClickableSpan existingSpan : existingSpans) {
+ text.removeSpan(existingSpan);
+ }
+ text.setSpan(span, link.getStart(), link.getEnd(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ applyCount++;
+ }
+ } else {
+ text.setSpan(span, link.getStart(), link.getEnd(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ applyCount++;
+ }
+ }
+ }
+ if (applyCount == 0) {
+ return TextLinks.STATUS_NO_LINKS_APPLIED;
+ }
+ return TextLinks.STATUS_LINKS_APPLIED;
+ }
+
+ /**
+ * A builder for building TextLinksParams.
+ */
+ public static final class Builder {
+
+ @TextLinks.ApplyStrategy
+ private int mApplyStrategy = TextLinks.APPLY_STRATEGY_IGNORE;
+ private Function<TextLink, TextLinkSpan> mSpanFactory = DEFAULT_SPAN_FACTORY;
+
+ /**
+ * Sets the apply strategy used to determine how to apply links to text.
+ * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE}
+ *
+ * @return this builder
+ */
+ public Builder setApplyStrategy(@TextLinks.ApplyStrategy int applyStrategy) {
+ mApplyStrategy = checkApplyStrategy(applyStrategy);
+ return this;
+ }
+
+ /**
+ * Sets a custom span factory for converting TextLinks to TextLinkSpans.
+ * Set to {@code null} to use the default span factory.
+ *
+ * @return this builder
+ */
+ public Builder setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) {
+ mSpanFactory = spanFactory == null ? DEFAULT_SPAN_FACTORY : spanFactory;
+ return this;
+ }
+
+ /**
+ * Sets the entity configuration used to determine what entity types to generate.
+ * Set to {@code null} for the default entity config which will automatically determine
+ * what links to generate.
+ *
+ * @return this builder
+ */
+ public Builder setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) {
+ return this;
+ }
+
+ /**
+ * Builds and returns a TextLinksParams object.
+ */
+ public TextLinksParams build() {
+ return new TextLinksParams(mApplyStrategy, mSpanFactory);
+ }
+ }
+
+ /** @throws IllegalArgumentException if the value is invalid */
+ @TextLinks.ApplyStrategy
+ private static int checkApplyStrategy(int applyStrategy) {
+ if (applyStrategy != TextLinks.APPLY_STRATEGY_IGNORE
+ && applyStrategy != TextLinks.APPLY_STRATEGY_REPLACE) {
+ throw new IllegalArgumentException(
+ "Invalid apply strategy. See TextLinksParams.ApplyStrategy for options.");
+ }
+ return applyStrategy;
+ }
+}
+
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
new file mode 100644
index 0000000..c1913f6
--- /dev/null
+++ b/android/view/textclassifier/TextSelection.java
@@ -0,0 +1,553 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.SpannedString;
+import android.util.ArrayMap;
+import android.view.textclassifier.TextClassifier.EntityType;
+import android.view.textclassifier.TextClassifier.Utils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Information about where text selection should be.
+ */
+public final class TextSelection implements Parcelable {
+
+ private final int mStartIndex;
+ private final int mEndIndex;
+ private final EntityConfidence mEntityConfidence;
+ @Nullable private final String mId;
+ @Nullable
+ private final TextClassification mTextClassification;
+ private final Bundle mExtras;
+
+ private TextSelection(
+ int startIndex, int endIndex, Map<String, Float> entityConfidence, String id,
+ @Nullable TextClassification textClassification,
+ Bundle extras) {
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ mEntityConfidence = new EntityConfidence(entityConfidence);
+ mId = id;
+ mTextClassification = textClassification;
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the start index of the text selection.
+ */
+ public int getSelectionStartIndex() {
+ return mStartIndex;
+ }
+
+ /**
+ * Returns the end index of the text selection.
+ */
+ public int getSelectionEndIndex() {
+ return mEndIndex;
+ }
+
+ /**
+ * Returns the number of entities found in the classified text.
+ */
+ @IntRange(from = 0)
+ public int getEntityCount() {
+ return mEntityConfidence.getEntities().size();
+ }
+
+ /**
+ * Returns the entity at the specified index. Entities are ordered from high confidence
+ * to low confidence.
+ *
+ * @throws IndexOutOfBoundsException if the specified index is out of range.
+ * @see #getEntityCount() for the number of entities available.
+ */
+ @NonNull
+ @EntityType
+ public String getEntity(int index) {
+ return mEntityConfidence.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for the specified entity. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
+ * classified text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(@EntityType String entity) {
+ return mEntityConfidence.getConfidenceScore(entity);
+ }
+
+ /**
+ * Returns the id, if one exists, for this object.
+ */
+ @Nullable
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the text classification result of the suggested selection span. Enables the text
+ * classification by calling
+ * {@link TextSelection.Request.Builder#setIncludeTextClassification(boolean)}. If the text
+ * classifier does not support it, a {@code null} is returned.
+ *
+ * @see TextSelection.Request.Builder#setIncludeTextClassification(boolean)
+ */
+ @Nullable
+ public TextClassification getTextClassification() {
+ return mTextClassification;
+ }
+
+ /**
+ * Returns the extended data.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** @hide */
+ public TextSelection.Builder toBuilder() {
+ return new TextSelection.Builder(mStartIndex, mEndIndex)
+ .setId(mId)
+ .setEntityConfidence(mEntityConfidence)
+ .setTextClassification(mTextClassification)
+ .setExtras(mExtras);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "TextSelection {id=%s, startIndex=%d, endIndex=%d, entities=%s}",
+ mId, mStartIndex, mEndIndex, mEntityConfidence);
+ }
+
+ /**
+ * Builder used to build {@link TextSelection} objects.
+ */
+ public static final class Builder {
+
+ private final int mStartIndex;
+ private final int mEndIndex;
+ private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
+ @Nullable private String mId;
+ @Nullable
+ private TextClassification mTextClassification;
+ @Nullable
+ private Bundle mExtras;
+
+ /**
+ * Creates a builder used to build {@link TextSelection} objects.
+ *
+ * @param startIndex the start index of the text selection.
+ * @param endIndex the end index of the text selection. Must be greater than startIndex
+ */
+ public Builder(@IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex) {
+ Preconditions.checkArgument(startIndex >= 0);
+ Preconditions.checkArgument(endIndex > startIndex);
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ }
+
+ /**
+ * Sets an entity type for the classified text and assigns a confidence score.
+ *
+ * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
+ * 0 implies the entity does not exist for the classified text.
+ * Values greater than 1 are clamped to 1.
+ */
+ @NonNull
+ public Builder setEntityType(
+ @NonNull @EntityType String type,
+ @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
+ Objects.requireNonNull(type);
+ mEntityConfidence.put(type, confidenceScore);
+ return this;
+ }
+
+ Builder setEntityConfidence(EntityConfidence scores) {
+ mEntityConfidence.clear();
+ mEntityConfidence.putAll(scores.toMap());
+ return this;
+ }
+
+ /**
+ * Sets an id for the TextSelection object.
+ */
+ @NonNull
+ public Builder setId(@Nullable String id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the text classification result of the suggested selection. If
+ * {@link Request#shouldIncludeTextClassification()} is {@code true}, set this value.
+ * Otherwise this value may be set to null. The intention of this method is to avoid
+ * doing expensive work if the client is not interested in such result.
+ *
+ * @return this builder
+ * @see Request#shouldIncludeTextClassification()
+ */
+ @NonNull
+ public Builder setTextClassification(@Nullable TextClassification textClassification) {
+ mTextClassification = textClassification;
+ return this;
+ }
+
+ /**
+ * Sets the extended data.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds and returns {@link TextSelection} object.
+ */
+ @NonNull
+ public TextSelection build() {
+ return new TextSelection(
+ mStartIndex, mEndIndex, mEntityConfidence, mId,
+ mTextClassification, mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+
+ /**
+ * A request object for generating TextSelection.
+ */
+ public static final class Request implements Parcelable {
+
+ private final CharSequence mText;
+ private final int mStartIndex;
+ private final int mEndIndex;
+ @Nullable private final LocaleList mDefaultLocales;
+ private final boolean mDarkLaunchAllowed;
+ private final boolean mIncludeTextClassification;
+ private final Bundle mExtras;
+ @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
+
+ private Request(
+ CharSequence text,
+ int startIndex,
+ int endIndex,
+ LocaleList defaultLocales,
+ boolean darkLaunchAllowed,
+ boolean includeTextClassification,
+ Bundle extras) {
+ mText = text;
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ mDefaultLocales = defaultLocales;
+ mDarkLaunchAllowed = darkLaunchAllowed;
+ mIncludeTextClassification = includeTextClassification;
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the text providing context for the selected text (which is specified by the
+ * sub sequence starting at startIndex and ending at endIndex).
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns start index of the selected part of text.
+ */
+ @IntRange(from = 0)
+ public int getStartIndex() {
+ return mStartIndex;
+ }
+
+ /**
+ * Returns end index of the selected part of text.
+ */
+ @IntRange(from = 0)
+ public int getEndIndex() {
+ return mEndIndex;
+ }
+
+ /**
+ * Returns true if the TextClassifier should return selection suggestions when "dark
+ * launched". Otherwise, returns false.
+ *
+ * @hide
+ */
+ public boolean isDarkLaunchAllowed() {
+ return mDarkLaunchAllowed;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate the
+ * provided text.
+ */
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ /**
+ * Returns the name of the package that sent this request.
+ * This returns {@code null} if no calling package name is set.
+ */
+ @Nullable
+ public String getCallingPackageName() {
+ return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
+ }
+
+ /**
+ * Sets the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setSystemTextClassifierMetadata(
+ @Nullable SystemTextClassifierMetadata systemTcMetadata) {
+ mSystemTcMetadata = systemTcMetadata;
+ }
+
+ /**
+ * Returns the information about the {@link SystemTextClassifier} that sent this request.
+ *
+ * @hide
+ */
+ @Nullable
+ public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
+ return mSystemTcMetadata;
+ }
+
+ /**
+ * Returns true if the client wants the text classifier to classify the text as well and
+ * include a {@link TextClassification} object in the result.
+ */
+ public boolean shouldIncludeTextClassification() {
+ return mIncludeTextClassification;
+ }
+
+ /**
+ * Returns the extended data.
+ *
+ * <p><b>NOTE: </b>Do not modify this bundle.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * A builder for building TextSelection requests.
+ */
+ public static final class Builder {
+
+ private final CharSequence mText;
+ private final int mStartIndex;
+ private final int mEndIndex;
+ @Nullable private LocaleList mDefaultLocales;
+ private boolean mDarkLaunchAllowed;
+ private boolean mIncludeTextClassification;
+ private Bundle mExtras;
+
+ /**
+ * @param text text providing context for the selected text (which is specified by the
+ * sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
+ * @param startIndex start index of the selected part of text
+ * @param endIndex end index of the selected part of text
+ */
+ public Builder(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex) {
+ Utils.checkArgument(text, startIndex, endIndex);
+ mText = text;
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ }
+
+ /**
+ * @param defaultLocales ordered list of locale preferences that may be used to
+ * disambiguate the provided text. If no locale preferences exist, set this to null
+ * or an empty locale list.
+ *
+ * @return this builder.
+ */
+ @NonNull
+ public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /**
+ * @param allowed whether or not the TextClassifier should return selection suggestions
+ * when "dark launched". When a TextClassifier is dark launched, it can suggest
+ * selection changes that should not be used to actually change the user's
+ * selection. Instead, the suggested selection is logged, compared with the user's
+ * selection interaction, and used to generate quality metrics for the
+ * TextClassifier. Not parceled.
+ *
+ * @return this builder.
+ * @hide
+ */
+ @NonNull
+ public Builder setDarkLaunchAllowed(boolean allowed) {
+ mDarkLaunchAllowed = allowed;
+ return this;
+ }
+
+ /**
+ * @param includeTextClassification If true, suggests the TextClassifier to classify the
+ * text in the suggested selection span and include a TextClassification object in
+ * the result. The TextClassifier may not support this and in which case,
+ * {@link TextSelection#getTextClassification()} returns {@code null}.
+ *
+ * @return this builder.
+ * @see TextSelection#getTextClassification()
+ */
+ @NonNull
+ public Builder setIncludeTextClassification(boolean includeTextClassification) {
+ mIncludeTextClassification = includeTextClassification;
+ return this;
+ }
+
+ /**
+ * Sets the extended data.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds and returns the request object.
+ */
+ @NonNull
+ public Request build() {
+ return new Request(new SpannedString(mText), mStartIndex, mEndIndex,
+ mDefaultLocales, mDarkLaunchAllowed,
+ mIncludeTextClassification,
+ mExtras == null ? Bundle.EMPTY : mExtras);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mText);
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ dest.writeParcelable(mDefaultLocales, flags);
+ dest.writeBundle(mExtras);
+ dest.writeParcelable(mSystemTcMetadata, flags);
+ dest.writeBoolean(mIncludeTextClassification);
+ }
+
+ private static Request readFromParcel(Parcel in) {
+ final CharSequence text = in.readCharSequence();
+ final int startIndex = in.readInt();
+ final int endIndex = in.readInt();
+ final LocaleList defaultLocales = in.readParcelable(null);
+ final Bundle extras = in.readBundle();
+ final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null);
+ final boolean includeTextClassification = in.readBoolean();
+
+ final Request request = new Request(text, startIndex, endIndex, defaultLocales,
+ /* darkLaunchAllowed= */ false, includeTextClassification, extras);
+ request.setSystemTextClassifierMetadata(systemTcMetadata);
+ return request;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR =
+ new Parcelable.Creator<Request>() {
+ @Override
+ public Request createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public Request[] newArray(int size) {
+ return new Request[size];
+ }
+ };
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mId);
+ dest.writeBundle(mExtras);
+ dest.writeParcelable(mTextClassification, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextSelection> CREATOR =
+ new Parcelable.Creator<TextSelection>() {
+ @Override
+ public TextSelection createFromParcel(Parcel in) {
+ return new TextSelection(in);
+ }
+
+ @Override
+ public TextSelection[] newArray(int size) {
+ return new TextSelection[size];
+ }
+ };
+
+ private TextSelection(Parcel in) {
+ mStartIndex = in.readInt();
+ mEndIndex = in.readInt();
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mId = in.readString();
+ mExtras = in.readBundle();
+ mTextClassification = in.readParcelable(TextClassification.class.getClassLoader());
+ }
+}
diff --git a/android/view/textservice/SentenceSuggestionsInfo.java b/android/view/textservice/SentenceSuggestionsInfo.java
new file mode 100644
index 0000000..0d37632
--- /dev/null
+++ b/android/view/textservice/SentenceSuggestionsInfo.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * This class contains a metadata of suggestions returned from a text service
+ * (e.g. {@link android.service.textservice.SpellCheckerService}).
+ * The text service uses this class to return the suggestions
+ * for a sentence. See {@link SuggestionsInfo} which is used for suggestions for a word.
+ * This class extends the functionality of {@link SuggestionsInfo} as far as this class enables
+ * you to put multiple {@link SuggestionsInfo}s on a sentence with the offsets and the lengths
+ * of all {@link SuggestionsInfo}s.
+ */
+public final class SentenceSuggestionsInfo implements Parcelable {
+
+ private final SuggestionsInfo[] mSuggestionsInfos;
+ private final int[] mOffsets;
+ private final int[] mLengths;
+
+ /**
+ * Constructor.
+ * @param suggestionsInfos from the text service
+ * @param offsets the array of offsets of suggestions
+ * @param lengths the array of lengths of suggestions
+ */
+ public SentenceSuggestionsInfo(
+ SuggestionsInfo[] suggestionsInfos, int[] offsets, int[] lengths) {
+ if (suggestionsInfos == null || offsets == null || lengths == null) {
+ throw new NullPointerException();
+ }
+ if (suggestionsInfos.length != offsets.length || offsets.length != lengths.length) {
+ throw new IllegalArgumentException();
+ }
+ final int infoSize = suggestionsInfos.length;
+ mSuggestionsInfos = Arrays.copyOf(suggestionsInfos, infoSize);
+ mOffsets = Arrays.copyOf(offsets, infoSize);
+ mLengths = Arrays.copyOf(lengths, infoSize);
+ }
+
+ public SentenceSuggestionsInfo(Parcel source) {
+ final int infoSize = source.readInt();
+ mSuggestionsInfos = new SuggestionsInfo[infoSize];
+ source.readTypedArray(mSuggestionsInfos, SuggestionsInfo.CREATOR);
+ mOffsets = new int[mSuggestionsInfos.length];
+ source.readIntArray(mOffsets);
+ mLengths = new int[mSuggestionsInfos.length];
+ source.readIntArray(mLengths);
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ final int infoSize = mSuggestionsInfos.length;
+ dest.writeInt(infoSize);
+ dest.writeTypedArray(mSuggestionsInfos, 0);
+ dest.writeIntArray(mOffsets);
+ dest.writeIntArray(mLengths);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @return the count of {@link SuggestionsInfo}s this instance holds.
+ */
+ public int getSuggestionsCount() {
+ return mSuggestionsInfos.length;
+ }
+
+ /**
+ * @param i the id of {@link SuggestionsInfo}s this instance holds.
+ * @return a {@link SuggestionsInfo} at the specified id
+ */
+ public SuggestionsInfo getSuggestionsInfoAt(int i) {
+ if (i >= 0 && i < mSuggestionsInfos.length) {
+ return mSuggestionsInfos[i];
+ }
+ return null;
+ }
+
+ /**
+ * @param i the id of {@link SuggestionsInfo}s this instance holds
+ * @return the offset of the specified {@link SuggestionsInfo}
+ */
+ public int getOffsetAt(int i) {
+ if (i >= 0 && i < mOffsets.length) {
+ return mOffsets[i];
+ }
+ return -1;
+ }
+
+ /**
+ * @param i the id of {@link SuggestionsInfo}s this instance holds
+ * @return the length of the specified {@link SuggestionsInfo}
+ */
+ public int getLengthAt(int i) {
+ if (i >= 0 && i < mLengths.length) {
+ return mLengths[i];
+ }
+ return -1;
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<SentenceSuggestionsInfo> CREATOR
+ = new Parcelable.Creator<SentenceSuggestionsInfo>() {
+ @Override
+ public SentenceSuggestionsInfo createFromParcel(Parcel source) {
+ return new SentenceSuggestionsInfo(source);
+ }
+
+ @Override
+ public SentenceSuggestionsInfo[] newArray(int size) {
+ return new SentenceSuggestionsInfo[size];
+ }
+ };
+}
diff --git a/android/view/textservice/SpellCheckerInfo.java b/android/view/textservice/SpellCheckerInfo.java
new file mode 100644
index 0000000..13d44da
--- /dev/null
+++ b/android/view/textservice/SpellCheckerInfo.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.view.textservice;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.PrintWriterPrinter;
+import android.util.Slog;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * This class is used to specify meta information of a spell checker.
+ */
+public final class SpellCheckerInfo implements Parcelable {
+ private static final String TAG = SpellCheckerInfo.class.getSimpleName();
+ private final ResolveInfo mService;
+ private final String mId;
+ private final int mLabel;
+
+ /**
+ * The spell checker setting activity's name, used by the system settings to
+ * launch the setting activity.
+ */
+ private final String mSettingsActivityName;
+
+ /**
+ * The array of subtypes.
+ */
+ private final ArrayList<SpellCheckerSubtype> mSubtypes = new ArrayList<>();
+
+ /**
+ * Constructor.
+ * @hide
+ */
+ public SpellCheckerInfo(Context context, ResolveInfo service)
+ throws XmlPullParserException, IOException {
+ mService = service;
+ ServiceInfo si = service.serviceInfo;
+ mId = new ComponentName(si.packageName, si.name).flattenToShortString();
+
+ final PackageManager pm = context.getPackageManager();
+ int label = 0;
+ String settingsActivityComponent = null;
+
+ XmlResourceParser parser = null;
+ try {
+ parser = si.loadXmlMetaData(pm, SpellCheckerSession.SERVICE_META_DATA);
+ if (parser == null) {
+ throw new XmlPullParserException("No "
+ + SpellCheckerSession.SERVICE_META_DATA + " meta-data");
+ }
+
+ final Resources res = pm.getResourcesForApplication(si.applicationInfo);
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ final String nodeName = parser.getName();
+ if (!"spell-checker".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data does not start with spell-checker tag");
+ }
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.SpellChecker);
+ label = sa.getResourceId(com.android.internal.R.styleable.SpellChecker_label, 0);
+ settingsActivityComponent = sa.getString(
+ com.android.internal.R.styleable.SpellChecker_settingsActivity);
+ sa.recycle();
+
+ final int depth = parser.getDepth();
+ // Parse all subtypes
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.START_TAG) {
+ final String subtypeNodeName = parser.getName();
+ if (!"subtype".equals(subtypeNodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data in spell-checker does not start with subtype tag");
+ }
+ final TypedArray a = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.SpellChecker_Subtype);
+ SpellCheckerSubtype subtype = new SpellCheckerSubtype(
+ a.getResourceId(com.android.internal.R.styleable
+ .SpellChecker_Subtype_label, 0),
+ a.getString(com.android.internal.R.styleable
+ .SpellChecker_Subtype_subtypeLocale),
+ a.getString(com.android.internal.R.styleable
+ .SpellChecker_Subtype_languageTag),
+ a.getString(com.android.internal.R.styleable
+ .SpellChecker_Subtype_subtypeExtraValue),
+ a.getInt(com.android.internal.R.styleable
+ .SpellChecker_Subtype_subtypeId, 0));
+ mSubtypes.add(subtype);
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Caught exception: " + e);
+ throw new XmlPullParserException(
+ "Unable to create context for: " + si.packageName);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ mLabel = label;
+ mSettingsActivityName = settingsActivityComponent;
+ }
+
+ /**
+ * Constructor.
+ * @hide
+ */
+ public SpellCheckerInfo(Parcel source) {
+ mLabel = source.readInt();
+ mId = source.readString();
+ mSettingsActivityName = source.readString();
+ mService = ResolveInfo.CREATOR.createFromParcel(source);
+ source.readTypedList(mSubtypes, SpellCheckerSubtype.CREATOR);
+ }
+
+ /**
+ * Return a unique ID for this spell checker. The ID is generated from
+ * the package and class name implementing the method.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Return the component of the service that implements.
+ */
+ public ComponentName getComponent() {
+ return new ComponentName(
+ mService.serviceInfo.packageName, mService.serviceInfo.name);
+ }
+
+ /**
+ * Return the .apk package that implements this.
+ */
+ public String getPackageName() {
+ return mService.serviceInfo.packageName;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mLabel);
+ dest.writeString(mId);
+ dest.writeString(mSettingsActivityName);
+ mService.writeToParcel(dest, flags);
+ dest.writeTypedList(mSubtypes);
+ }
+
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<SpellCheckerInfo> CREATOR
+ = new Parcelable.Creator<SpellCheckerInfo>() {
+ @Override
+ public SpellCheckerInfo createFromParcel(Parcel source) {
+ return new SpellCheckerInfo(source);
+ }
+
+ @Override
+ public SpellCheckerInfo[] newArray(int size) {
+ return new SpellCheckerInfo[size];
+ }
+ };
+
+ /**
+ * Load the user-displayed label for this spell checker.
+ *
+ * @param pm Supply a PackageManager used to load the spell checker's resources.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ if (mLabel == 0 || pm == null) return "";
+ return pm.getText(getPackageName(), mLabel, mService.serviceInfo.applicationInfo);
+ }
+
+ /**
+ * Load the user-displayed icon for this spell checker.
+ *
+ * @param pm Supply a PackageManager used to load the spell checker's resources.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ return mService.loadIcon(pm);
+ }
+
+
+ /**
+ * Return the raw information about the Service implementing this
+ * spell checker. Do not modify the returned object.
+ */
+ public ServiceInfo getServiceInfo() {
+ return mService.serviceInfo;
+ }
+
+ /**
+ * Return the class name of an activity that provides a settings UI.
+ * You can launch this activity be starting it with
+ * an {@link android.content.Intent} whose action is MAIN and with an
+ * explicit {@link android.content.ComponentName}
+ * composed of {@link #getPackageName} and the class name returned here.
+ *
+ * <p>A null will be returned if there is no settings activity.
+ */
+ public String getSettingsActivity() {
+ return mSettingsActivityName;
+ }
+
+ /**
+ * Return the count of the subtypes.
+ */
+ public int getSubtypeCount() {
+ return mSubtypes.size();
+ }
+
+ /**
+ * Return the subtype at the specified index.
+ *
+ * @param index the index of the subtype to return.
+ */
+ public SpellCheckerSubtype getSubtypeAt(int index) {
+ return mSubtypes.get(index);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ */
+ public void dump(final PrintWriter pw, final String prefix) {
+ pw.println(prefix + "mId=" + mId);
+ pw.println(prefix + "mSettingsActivityName=" + mSettingsActivityName);
+ pw.println(prefix + "Service:");
+ mService.dump(new PrintWriterPrinter(pw), prefix + " ");
+ final int N = getSubtypeCount();
+ for (int i = 0; i < N; i++) {
+ final SpellCheckerSubtype st = getSubtypeAt(i);
+ pw.println(prefix + " " + "Subtype #" + i + ":");
+ pw.println(prefix + " " + "locale=" + st.getLocale()
+ + " languageTag=" + st.getLanguageTag());
+ pw.println(prefix + " " + "extraValue=" + st.getExtraValue());
+ }
+ }
+}
diff --git a/android/view/textservice/SpellCheckerSession.java b/android/view/textservice/SpellCheckerSession.java
new file mode 100644
index 0000000..6cfb938
--- /dev/null
+++ b/android/view/textservice/SpellCheckerSession.java
@@ -0,0 +1,748 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textservice;
+
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.textservice.ISpellCheckerSession;
+import com.android.internal.textservice.ISpellCheckerSessionListener;
+import com.android.internal.textservice.ITextServicesSessionListener;
+
+import dalvik.system.CloseGuard;
+
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+/**
+ * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
+ *
+ *
+ * <a name="Applications"></a>
+ * <h3>Applications</h3>
+ *
+ * <p>In most cases, applications that are using the standard
+ * {@link android.widget.TextView} or its subclasses will have little they need
+ * to do to work well with spell checker services. The main things you need to
+ * be aware of are:</p>
+ *
+ * <ul>
+ * <li> Properly set the {@link android.R.attr#inputType} in your editable
+ * text views, so that the spell checker will have enough context to help the
+ * user in editing text in them.
+ * </ul>
+ *
+ * <p>For the rare people amongst us writing client applications that use the spell checker service
+ * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or
+ * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker
+ * service by yourself.</p>
+ *
+ * <h3>Security</h3>
+ *
+ * <p>There are a lot of security issues associated with spell checkers,
+ * since they could monitor all the text being sent to them
+ * through, for instance, {@link android.widget.TextView}.
+ * The Android spell checker framework also allows
+ * arbitrary third party spell checkers, so care must be taken to restrict their
+ * selection and interactions.</p>
+ *
+ * <p>Here are some key points about the security architecture behind the
+ * spell checker framework:</p>
+ *
+ * <ul>
+ * <li>Only the system is allowed to directly access a spell checker framework's
+ * {@link android.service.textservice.SpellCheckerService} interface, via the
+ * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission. This is
+ * enforced in the system by not binding to a spell checker service that does
+ * not require this permission.
+ *
+ * <li>The user must explicitly enable a new spell checker in settings before
+ * they can be enabled, to confirm with the system that they know about it
+ * and want to make it available for use.
+ * </ul>
+ *
+ */
+public class SpellCheckerSession {
+ private static final String TAG = SpellCheckerSession.class.getSimpleName();
+ private static final boolean DBG = false;
+ /**
+ * Name under which a SpellChecker service component publishes information about itself.
+ * This meta-data must reference an XML resource.
+ **/
+ public static final String SERVICE_META_DATA = "android.view.textservice.scs";
+
+ private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
+ private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
+
+ private final InternalListener mInternalListener;
+ private final TextServicesManager mTextServicesManager;
+ private final SpellCheckerInfo mSpellCheckerInfo;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private final SpellCheckerSessionListener mSpellCheckerSessionListener;
+ private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
+ private final Executor mExecutor;
+
+ private final CloseGuard mGuard = CloseGuard.get();
+
+ /**
+ * Constructor
+ * @hide
+ */
+ public SpellCheckerSession(
+ SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener,
+ Executor executor) {
+ if (info == null || listener == null || tsm == null) {
+ throw new NullPointerException();
+ }
+ mSpellCheckerInfo = info;
+ mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(this);
+ mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
+ mTextServicesManager = tsm;
+ mSpellCheckerSessionListener = listener;
+ mExecutor = executor;
+
+ mGuard.open("finishSession");
+ }
+
+ /**
+ * @return true if the connection to a text service of this session is disconnected and not
+ * alive.
+ */
+ public boolean isSessionDisconnected() {
+ return mSpellCheckerSessionListenerImpl.isDisconnected();
+ }
+
+ /**
+ * Get the spell checker service info this spell checker session has.
+ * @return SpellCheckerInfo for the specified locale.
+ */
+ public SpellCheckerInfo getSpellChecker() {
+ return mSpellCheckerInfo;
+ }
+
+ /**
+ * Cancel pending and running spell check tasks
+ */
+ public void cancel() {
+ mSpellCheckerSessionListenerImpl.cancel();
+ }
+
+ /**
+ * Finish this session and allow TextServicesManagerService to disconnect the bound spell
+ * checker.
+ */
+ public void close() {
+ mGuard.close();
+ mSpellCheckerSessionListenerImpl.close();
+ mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
+ }
+
+ /**
+ * Get suggestions from the specified sentences
+ * @param textInfos an array of text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
+ */
+ public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
+ final InputMethodManager imm = mTextServicesManager.getInputMethodManager();
+ if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
+ handleOnGetSentenceSuggestionsMultiple(new SentenceSuggestionsInfo[0]);
+ return;
+ }
+ mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
+ textInfos, suggestionsLimit);
+ }
+
+ /**
+ * Get candidate strings for a substring of the specified text.
+ * @param textInfo text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
+ * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
+ */
+ @Deprecated
+ public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
+ getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
+ }
+
+ /**
+ * A batch process of getSuggestions
+ * @param textInfos an array of text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
+ * @param sequentialWords true if textInfos can be treated as sequential words.
+ * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
+ */
+ @Deprecated
+ public void getSuggestions(
+ TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
+ if (DBG) {
+ Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
+ }
+ final InputMethodManager imm = mTextServicesManager.getInputMethodManager();
+ if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
+ handleOnGetSuggestionsMultiple(new SuggestionsInfo[0]);
+ return;
+ }
+ mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
+ textInfos, suggestionsLimit, sequentialWords);
+ }
+
+ void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionsInfos) {
+ mExecutor.execute(() -> mSpellCheckerSessionListener.onGetSuggestions(suggestionsInfos));
+ }
+
+ void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionsInfos) {
+ mExecutor.execute(() ->
+ mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionsInfos));
+ }
+
+ private static final class SpellCheckerSessionListenerImpl
+ extends ISpellCheckerSessionListener.Stub {
+ private static final int TASK_CANCEL = 1;
+ private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
+ private static final int TASK_CLOSE = 3;
+ private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
+ private static String taskToString(int task) {
+ switch (task) {
+ case TASK_CANCEL:
+ return "TASK_CANCEL";
+ case TASK_GET_SUGGESTIONS_MULTIPLE:
+ return "TASK_GET_SUGGESTIONS_MULTIPLE";
+ case TASK_CLOSE:
+ return "TASK_CLOSE";
+ case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
+ return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE";
+ default:
+ return "Unexpected task=" + task;
+ }
+ }
+
+ private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>();
+ @GuardedBy("SpellCheckerSessionListenerImpl.this")
+ private SpellCheckerSession mSpellCheckerSession;
+
+ private static final int STATE_WAIT_CONNECTION = 0;
+ private static final int STATE_CONNECTED = 1;
+ private static final int STATE_CLOSED_AFTER_CONNECTION = 2;
+ private static final int STATE_CLOSED_BEFORE_CONNECTION = 3;
+ private static String stateToString(int state) {
+ switch (state) {
+ case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION";
+ case STATE_CONNECTED: return "STATE_CONNECTED";
+ case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION";
+ case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION";
+ default: return "Unexpected state=" + state;
+ }
+ }
+ private int mState = STATE_WAIT_CONNECTION;
+
+ private ISpellCheckerSession mISpellCheckerSession;
+ private HandlerThread mThread;
+ private Handler mAsyncHandler;
+
+ SpellCheckerSessionListenerImpl(SpellCheckerSession spellCheckerSession) {
+ mSpellCheckerSession = spellCheckerSession;
+ }
+
+ private static class SpellCheckerParams {
+ public final int mWhat;
+ public final TextInfo[] mTextInfos;
+ public final int mSuggestionsLimit;
+ public final boolean mSequentialWords;
+ public ISpellCheckerSession mSession;
+ public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
+ boolean sequentialWords) {
+ mWhat = what;
+ mTextInfos = textInfos;
+ mSuggestionsLimit = suggestionsLimit;
+ mSequentialWords = sequentialWords;
+ }
+ }
+
+ private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
+ boolean async) {
+ if (DBG) {
+ synchronized (this) {
+ Log.d(TAG, "entering processTask:"
+ + " session.hashCode()=#" + Integer.toHexString(session.hashCode())
+ + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async
+ + " mAsyncHandler=" + mAsyncHandler
+ + " mState=" + stateToString(mState));
+ }
+ }
+ if (async || mAsyncHandler == null) {
+ switch (scp.mWhat) {
+ case TASK_CANCEL:
+ try {
+ session.onCancel();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to cancel " + e);
+ }
+ break;
+ case TASK_GET_SUGGESTIONS_MULTIPLE:
+ try {
+ session.onGetSuggestionsMultiple(scp.mTextInfos,
+ scp.mSuggestionsLimit, scp.mSequentialWords);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get suggestions " + e);
+ }
+ break;
+ case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
+ try {
+ session.onGetSentenceSuggestionsMultiple(
+ scp.mTextInfos, scp.mSuggestionsLimit);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get suggestions " + e);
+ }
+ break;
+ case TASK_CLOSE:
+ try {
+ session.onClose();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to close " + e);
+ }
+ break;
+ }
+ } else {
+ // The interface is to a local object, so need to execute it
+ // asynchronously.
+ scp.mSession = session;
+ mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
+ }
+
+ if (scp.mWhat == TASK_CLOSE) {
+ // If we are closing, we want to clean up our state now even
+ // if it is pending as an async operation.
+ synchronized (this) {
+ processCloseLocked();
+ }
+ }
+ }
+
+ @GuardedBy("SpellCheckerSessionListenerImpl.this")
+ private void processCloseLocked() {
+ if (DBG) Log.d(TAG, "entering processCloseLocked:"
+ + " session" + (mISpellCheckerSession != null ? ".hashCode()=#"
+ + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null")
+ + " mState=" + stateToString(mState));
+ mISpellCheckerSession = null;
+ if (mThread != null) {
+ mThread.quit();
+ }
+ mSpellCheckerSession = null;
+ mPendingTasks.clear();
+ mThread = null;
+ mAsyncHandler = null;
+ switch (mState) {
+ case STATE_WAIT_CONNECTION:
+ mState = STATE_CLOSED_BEFORE_CONNECTION;
+ break;
+ case STATE_CONNECTED:
+ mState = STATE_CLOSED_AFTER_CONNECTION;
+ break;
+ default:
+ Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" +
+ stateToString(mState));
+ break;
+ }
+ }
+
+ public void onServiceConnected(ISpellCheckerSession session) {
+ synchronized (this) {
+ switch (mState) {
+ case STATE_WAIT_CONNECTION:
+ // OK, go ahead.
+ break;
+ case STATE_CLOSED_BEFORE_CONNECTION:
+ // This is possible, and not an error. The client no longer is interested
+ // in this connection. OK to ignore.
+ if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is"
+ + " already closed.");
+ return;
+ default:
+ Log.e(TAG, "ignoring onServiceConnected due to unexpected mState="
+ + stateToString(mState));
+ return;
+ }
+ if (session == null) {
+ Log.e(TAG, "ignoring onServiceConnected due to session=null");
+ return;
+ }
+ mISpellCheckerSession = session;
+ if (session.asBinder() instanceof Binder && mThread == null) {
+ if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected.");
+ // If this is a local object, we need to do our own threading
+ // to make sure we handle it asynchronously.
+ mThread = new HandlerThread("SpellCheckerSession",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ mThread.start();
+ mAsyncHandler = new Handler(mThread.getLooper()) {
+ @Override public void handleMessage(Message msg) {
+ SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
+ processTask(scp.mSession, scp, true);
+ }
+ };
+ }
+ mState = STATE_CONNECTED;
+ if (DBG) {
+ Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#"
+ + Integer.toHexString(mISpellCheckerSession.hashCode())
+ + " mPendingTasks.size()=" + mPendingTasks.size());
+ }
+ while (!mPendingTasks.isEmpty()) {
+ processTask(session, mPendingTasks.poll(), false);
+ }
+ }
+ }
+
+ public void cancel() {
+ processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
+ }
+
+ public void getSuggestionsMultiple(
+ TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
+ processOrEnqueueTask(
+ new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
+ suggestionsLimit, sequentialWords));
+ }
+
+ public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
+ processOrEnqueueTask(
+ new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
+ textInfos, suggestionsLimit, false));
+ }
+
+ public void close() {
+ processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
+ }
+
+ public boolean isDisconnected() {
+ synchronized (this) {
+ return mState != STATE_CONNECTED;
+ }
+ }
+
+ private void processOrEnqueueTask(SpellCheckerParams scp) {
+ ISpellCheckerSession session;
+ synchronized (this) {
+ if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION
+ || mState == STATE_CLOSED_BEFORE_CONNECTION)) {
+ // It is OK to call SpellCheckerSession#close() multiple times.
+ // Don't output confusing/misleading warning messages.
+ return;
+ }
+ if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
+ Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
+ + stateToString(mState)
+ + " scp.mWhat=" + taskToString(scp.mWhat));
+ return;
+ }
+
+ if (mState == STATE_WAIT_CONNECTION) {
+ // If we are still waiting for the connection. Need to pay special attention.
+ if (scp.mWhat == TASK_CLOSE) {
+ processCloseLocked();
+ return;
+ }
+ // Enqueue the task to task queue.
+ SpellCheckerParams closeTask = null;
+ if (scp.mWhat == TASK_CANCEL) {
+ if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask.");
+ while (!mPendingTasks.isEmpty()) {
+ final SpellCheckerParams tmp = mPendingTasks.poll();
+ if (tmp.mWhat == TASK_CLOSE) {
+ // Only one close task should be processed, while we need to remove
+ // all close tasks from the queue
+ closeTask = tmp;
+ }
+ }
+ }
+ mPendingTasks.offer(scp);
+ if (closeTask != null) {
+ mPendingTasks.offer(closeTask);
+ }
+ if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
+ + " connection is not established."
+ + " mPendingTasks.size()=" + mPendingTasks.size());
+ return;
+ }
+
+ session = mISpellCheckerSession;
+ }
+ // session must never be null here.
+ processTask(session, scp, false);
+ }
+
+ @BinderThread
+ @Override
+ public void onGetSuggestions(SuggestionsInfo[] results) {
+ SpellCheckerSession session = getSpellCheckerSession();
+ if (session != null) {
+ // Lock should not be held when calling callback, in order to avoid deadlock.
+ session.handleOnGetSuggestionsMultiple(results);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
+ SpellCheckerSession session = getSpellCheckerSession();
+ if (session != null) {
+ // Lock should not be held when calling callback, in order to avoid deadlock.
+ session.handleOnGetSentenceSuggestionsMultiple(results);
+ }
+ }
+
+ @Nullable
+ private SpellCheckerSession getSpellCheckerSession() {
+ synchronized (SpellCheckerSessionListenerImpl.this) {
+ return mSpellCheckerSession;
+ }
+ }
+ }
+
+ /** Parameters used to create a {@link SpellCheckerSession}. */
+ public static class SpellCheckerSessionParams {
+ @Nullable
+ private final Locale mLocale;
+ private final boolean mShouldReferToSpellCheckerLanguageSettings;
+ private final @SuggestionsInfo.ResultAttrs int mSupportedAttributes;
+ private final Bundle mExtras;
+
+ private SpellCheckerSessionParams(Locale locale,
+ boolean referToSpellCheckerLanguageSettings, int supportedAttributes,
+ Bundle extras) {
+ mLocale = locale;
+ mShouldReferToSpellCheckerLanguageSettings = referToSpellCheckerLanguageSettings;
+ mSupportedAttributes = supportedAttributes;
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the locale in which the spell checker should operate.
+ *
+ * @see android.service.textservice.SpellCheckerService.Session#getLocale()
+ */
+ @SuppressLint("UseIcu")
+ @Nullable
+ public Locale getLocale() {
+ return mLocale;
+ }
+
+ /**
+ * Returns true if the user's spell checker language settings should be used to determine
+ * the spell checker locale.
+ */
+ public boolean shouldReferToSpellCheckerLanguageSettings() {
+ return mShouldReferToSpellCheckerLanguageSettings;
+ }
+
+ /**
+ * Returns a bitmask of {@link SuggestionsInfo} attributes that the spell checker can set
+ * in {@link SuggestionsInfo} it returns.
+ *
+ * @see android.service.textservice.SpellCheckerService.Session#getSupportedAttributes()
+ */
+ public @SuggestionsInfo.ResultAttrs int getSupportedAttributes() {
+ return mSupportedAttributes;
+ }
+
+ /**
+ * Returns a bundle containing extra parameters for the spell checker.
+ *
+ * <p>This bundle can be used to pass implementation-specific parameters to the
+ * {@link android.service.textservice.SpellCheckerService} implementation.
+ *
+ * @see android.service.textservice.SpellCheckerService.Session#getBundle()
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** Builder of {@link SpellCheckerSessionParams}. */
+ public static final class Builder {
+ @Nullable
+ private Locale mLocale;
+ private boolean mShouldReferToSpellCheckerLanguageSettings = false;
+ private @SuggestionsInfo.ResultAttrs int mSupportedAttributes = 0;
+ private Bundle mExtras = Bundle.EMPTY;
+
+ /** Constructs a {@code Builder}. */
+ public Builder() {
+ }
+
+ /**
+ * Returns constructed {@link SpellCheckerSession} instance.
+ *
+ * <p>Before calling this method, either {@link #setLocale(Locale)} should be called
+ * with a non-null locale or
+ * {@link #setShouldReferToSpellCheckerLanguageSettings(boolean)} should be called with
+ * {@code true}.
+ */
+ @NonNull
+ public SpellCheckerSessionParams build() {
+ if (mLocale == null && !mShouldReferToSpellCheckerLanguageSettings) {
+ throw new IllegalArgumentException("mLocale should not be null if "
+ + " mShouldReferToSpellCheckerLanguageSettings is false.");
+ }
+ return new SpellCheckerSessionParams(mLocale,
+ mShouldReferToSpellCheckerLanguageSettings, mSupportedAttributes, mExtras);
+ }
+
+ /**
+ * Sets the locale in which the spell checker should operate.
+ *
+ * @see android.service.textservice.SpellCheckerService.Session#getLocale()
+ */
+ @NonNull
+ public Builder setLocale(@SuppressLint("UseIcu") @Nullable Locale locale) {
+ mLocale = locale;
+ return this;
+ }
+
+ /**
+ * Sets whether or not the user's spell checker language settings should be used to
+ * determine spell checker locale.
+ *
+ * <p>If {@code shouldReferToSpellCheckerLanguageSettings} is true, the exact way of
+ * determining spell checker locale differs based on {@code locale} specified in
+ * {@link #setLocale(Locale)}.
+ * If {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is
+ * null, the locale specified in Settings will be used. If
+ * {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is not
+ * null, {@link SpellCheckerSession} can be created only when the locale specified in
+ * Settings is the same as {@code locale}. Exceptionally, if
+ * {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is
+ * language only (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
+ * used.
+ *
+ * @see #setLocale(Locale)
+ */
+ @NonNull
+ public Builder setShouldReferToSpellCheckerLanguageSettings(
+ boolean shouldReferToSpellCheckerLanguageSettings) {
+ mShouldReferToSpellCheckerLanguageSettings =
+ shouldReferToSpellCheckerLanguageSettings;
+ return this;
+ }
+
+ /**
+ * Sets a bitmask of {@link SuggestionsInfo} attributes that the spell checker can set
+ * in {@link SuggestionsInfo} it returns.
+ *
+ * @see android.service.textservice.SpellCheckerService.Session#getSupportedAttributes()
+ */
+ @NonNull
+ public Builder setSupportedAttributes(
+ @SuggestionsInfo.ResultAttrs int supportedAttributes) {
+ mSupportedAttributes = supportedAttributes;
+ return this;
+ }
+
+ /**
+ * Sets a bundle containing extra parameters for the spell checker.
+ *
+ * <p>This bundle can be used to pass implementation-specific parameters to the
+ * {@link android.service.textservice.SpellCheckerService} implementation.
+ *
+ * @see android.service.textservice.SpellCheckerService.Session#getBundle()
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+ }
+ }
+
+ /**
+ * Callback for getting results from text services
+ */
+ public interface SpellCheckerSessionListener {
+ /**
+ * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
+ * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
+ * @param results an array of {@link SuggestionsInfo}s.
+ * These results are suggestions for {@link TextInfo}s queried by
+ * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
+ * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
+ */
+ public void onGetSuggestions(SuggestionsInfo[] results);
+ /**
+ * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
+ * @param results an array of {@link SentenceSuggestionsInfo}s.
+ * These results are suggestions for {@link TextInfo}s
+ * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
+ */
+ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
+ }
+
+ private static final class InternalListener extends ITextServicesSessionListener.Stub {
+ private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
+
+ public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
+ mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
+ }
+
+ @Override
+ public void onServiceConnected(ISpellCheckerSession session) {
+ mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ // Note that mGuard will be null if the constructor threw.
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ close();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public ITextServicesSessionListener getTextServicesSessionListener() {
+ return mInternalListener;
+ }
+
+ /**
+ * @hide
+ */
+ public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
+ return mSpellCheckerSessionListenerImpl;
+ }
+}
diff --git a/android/view/textservice/SpellCheckerSubtype.java b/android/view/textservice/SpellCheckerSubtype.java
new file mode 100644
index 0000000..38a140f
--- /dev/null
+++ b/android/view/textservice/SpellCheckerSubtype.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textservice;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.inputmethod.SubtypeLocaleUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * This class is used to specify meta information of a subtype contained in a spell checker.
+ * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings.
+ *
+ * @see SpellCheckerInfo
+ *
+ * @attr ref android.R.styleable#SpellChecker_Subtype_label
+ * @attr ref android.R.styleable#SpellChecker_Subtype_languageTag
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeLocale
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeExtraValue
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeId
+ */
+public final class SpellCheckerSubtype implements Parcelable {
+ private static final String TAG = SpellCheckerSubtype.class.getSimpleName();
+ private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
+ private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
+ /**
+ * @hide
+ */
+ @TestApi
+ public static final int SUBTYPE_ID_NONE = 0;
+ private static final String SUBTYPE_LANGUAGE_TAG_NONE = "";
+
+ private final int mSubtypeId;
+ private final int mSubtypeHashCode;
+ private final int mSubtypeNameResId;
+ private final String mSubtypeLocale;
+ private final String mSubtypeLanguageTag;
+ private final String mSubtypeExtraValue;
+ private HashMap<String, String> mExtraValueHashMapCache;
+
+ /**
+ * Constructor.
+ *
+ * <p>There is no public API that requires developers to instantiate custom
+ * {@link SpellCheckerSubtype} object. Hence so far there is no need to make this constructor
+ * available in public API.</p>
+ *
+ * @param nameId The name of the subtype
+ * @param locale The locale supported by the subtype
+ * @param languageTag The BCP-47 Language Tag associated with this subtype.
+ * @param extraValue The extra value of the subtype
+ * @param subtypeId The subtype ID that is supposed to be stable during package update.
+ *
+ * @hide
+ */
+ public SpellCheckerSubtype(int nameId, String locale, String languageTag, String extraValue,
+ int subtypeId) {
+ mSubtypeNameResId = nameId;
+ mSubtypeLocale = locale != null ? locale : "";
+ mSubtypeLanguageTag = languageTag != null ? languageTag : SUBTYPE_LANGUAGE_TAG_NONE;
+ mSubtypeExtraValue = extraValue != null ? extraValue : "";
+ mSubtypeId = subtypeId;
+ mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
+ mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+ }
+
+ /**
+ * Constructor.
+ * @param nameId The name of the subtype
+ * @param locale The locale supported by the subtype
+ * @param extraValue The extra value of the subtype
+ *
+ * @deprecated There is no public API that requires developers to directly instantiate custom
+ * {@link SpellCheckerSubtype} objects right now. Hence only the system is expected to be able
+ * to instantiate {@link SpellCheckerSubtype} object.
+ */
+ @Deprecated
+ public SpellCheckerSubtype(int nameId, String locale, String extraValue) {
+ this(nameId, locale, SUBTYPE_LANGUAGE_TAG_NONE, extraValue, SUBTYPE_ID_NONE);
+ }
+
+ SpellCheckerSubtype(Parcel source) {
+ String s;
+ mSubtypeNameResId = source.readInt();
+ s = source.readString();
+ mSubtypeLocale = s != null ? s : "";
+ s = source.readString();
+ mSubtypeLanguageTag = s != null ? s : "";
+ s = source.readString();
+ mSubtypeExtraValue = s != null ? s : "";
+ mSubtypeId = source.readInt();
+ mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
+ mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+ }
+
+ /**
+ * @return the name of the subtype
+ */
+ public int getNameResId() {
+ return mSubtypeNameResId;
+ }
+
+ /**
+ * @return the locale of the subtype
+ *
+ * @deprecated Use {@link #getLanguageTag()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public String getLocale() {
+ return mSubtypeLocale;
+ }
+
+ /**
+ * @return the BCP-47 Language Tag of the subtype. Returns an empty string when no Language Tag
+ * is specified.
+ *
+ * @see Locale#forLanguageTag(String)
+ */
+ @NonNull
+ public String getLanguageTag() {
+ return mSubtypeLanguageTag;
+ }
+
+ /**
+ * @return the extra value of the subtype
+ */
+ public String getExtraValue() {
+ return mSubtypeExtraValue;
+ }
+
+ private HashMap<String, String> getExtraValueHashMap() {
+ if (mExtraValueHashMapCache == null) {
+ mExtraValueHashMapCache = new HashMap<String, String>();
+ final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR);
+ final int N = pairs.length;
+ for (int i = 0; i < N; ++i) {
+ final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR);
+ if (pair.length == 1) {
+ mExtraValueHashMapCache.put(pair[0], null);
+ } else if (pair.length > 1) {
+ if (pair.length > 2) {
+ Slog.w(TAG, "ExtraValue has two or more '='s");
+ }
+ mExtraValueHashMapCache.put(pair[0], pair[1]);
+ }
+ }
+ }
+ return mExtraValueHashMapCache;
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key the key of extra value
+ * @return the subtype contains specified the extra value
+ */
+ public boolean containsExtraValueKey(String key) {
+ return getExtraValueHashMap().containsKey(key);
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key the key of extra value
+ * @return the value of the specified key
+ */
+ public String getExtraValueOf(String key) {
+ return getExtraValueHashMap().get(key);
+ }
+
+ @Override
+ public int hashCode() {
+ return mSubtypeHashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof SpellCheckerSubtype) {
+ SpellCheckerSubtype subtype = (SpellCheckerSubtype) o;
+ if (subtype.mSubtypeId != SUBTYPE_ID_NONE || mSubtypeId != SUBTYPE_ID_NONE) {
+ return (subtype.hashCode() == hashCode());
+ }
+ return (subtype.hashCode() == hashCode())
+ && (subtype.getNameResId() == getNameResId())
+ && (subtype.getLocale().equals(getLocale()))
+ && (subtype.getLanguageTag().equals(getLanguageTag()))
+ && (subtype.getExtraValue().equals(getExtraValue()));
+ }
+ return false;
+ }
+
+ /**
+ * @return {@link Locale} constructed from {@link #getLanguageTag()}. If the Language Tag is not
+ * specified, then try to construct from {@link #getLocale()}
+ *
+ * <p>TODO: Consider to make this a public API, or move this to support lib.</p>
+ * @hide
+ */
+ @Nullable
+ public Locale getLocaleObject() {
+ if (!TextUtils.isEmpty(mSubtypeLanguageTag)) {
+ return Locale.forLanguageTag(mSubtypeLanguageTag);
+ }
+ return SubtypeLocaleUtils.constructLocaleFromString(mSubtypeLocale);
+ }
+
+ /**
+ * @param context Context will be used for getting Locale and PackageManager.
+ * @param packageName The package name of the spell checker
+ * @param appInfo The application info of the spell checker
+ * @return a display name for this subtype. The string resource of the label (mSubtypeNameResId)
+ * can have only one %s in it. If there is, the %s part will be replaced with the locale's
+ * display name by the formatter. If there is not, this method simply returns the string
+ * specified by mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the
+ * framework to generate an appropriate display name.
+ */
+ public CharSequence getDisplayName(
+ Context context, String packageName, ApplicationInfo appInfo) {
+ final Locale locale = getLocaleObject();
+ final String localeStr = locale != null ? locale.getDisplayName() : mSubtypeLocale;
+ if (mSubtypeNameResId == 0) {
+ return localeStr;
+ }
+ final CharSequence subtypeName = context.getPackageManager().getText(
+ packageName, mSubtypeNameResId, appInfo);
+ if (!TextUtils.isEmpty(subtypeName)) {
+ return String.format(subtypeName.toString(), localeStr);
+ } else {
+ return localeStr;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeInt(mSubtypeNameResId);
+ dest.writeString(mSubtypeLocale);
+ dest.writeString(mSubtypeLanguageTag);
+ dest.writeString(mSubtypeExtraValue);
+ dest.writeInt(mSubtypeId);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<SpellCheckerSubtype> CREATOR
+ = new Parcelable.Creator<SpellCheckerSubtype>() {
+ @Override
+ public SpellCheckerSubtype createFromParcel(Parcel source) {
+ return new SpellCheckerSubtype(source);
+ }
+
+ @Override
+ public SpellCheckerSubtype[] newArray(int size) {
+ return new SpellCheckerSubtype[size];
+ }
+ };
+
+ private static int hashCodeInternal(String locale, String extraValue) {
+ return Arrays.hashCode(new Object[] {locale, extraValue});
+ }
+
+ /**
+ * Sort the list of subtypes
+ * @param context Context will be used for getting localized strings
+ * @param flags Flags for the sort order
+ * @param sci SpellCheckerInfo of which subtypes are subject to be sorted
+ * @param subtypeList List which will be sorted
+ * @return Sorted list of subtypes
+ * @hide
+ */
+ public static List<SpellCheckerSubtype> sort(Context context, int flags, SpellCheckerInfo sci,
+ List<SpellCheckerSubtype> subtypeList) {
+ if (sci == null) return subtypeList;
+ final HashSet<SpellCheckerSubtype> subtypesSet = new HashSet<SpellCheckerSubtype>(
+ subtypeList);
+ final ArrayList<SpellCheckerSubtype> sortedList = new ArrayList<SpellCheckerSubtype>();
+ int N = sci.getSubtypeCount();
+ for (int i = 0; i < N; ++i) {
+ SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
+ if (subtypesSet.contains(subtype)) {
+ sortedList.add(subtype);
+ subtypesSet.remove(subtype);
+ }
+ }
+ // If subtypes in subtypesSet remain, that means these subtypes are not
+ // contained in sci, so the remaining subtypes will be appended.
+ for (SpellCheckerSubtype subtype: subtypesSet) {
+ sortedList.add(subtype);
+ }
+ return sortedList;
+ }
+}
diff --git a/android/view/textservice/SuggestionsInfo.java b/android/view/textservice/SuggestionsInfo.java
new file mode 100644
index 0000000..775a6bd
--- /dev/null
+++ b/android/view/textservice/SuggestionsInfo.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.view.textservice;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class contains a metadata of suggestions from the text service
+ */
+public final class SuggestionsInfo implements Parcelable {
+ private static final String[] EMPTY = ArrayUtils.emptyArray(String.class);
+
+ /**
+ * An internal annotation to indicate that one ore more combinations of
+ * <code>RESULT_ATTR_</code> flags are expected.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "RESULT_ATTR_" }, value = {
+ RESULT_ATTR_IN_THE_DICTIONARY,
+ RESULT_ATTR_LOOKS_LIKE_TYPO,
+ RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS,
+ RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR,
+ RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS,
+ })
+ public @interface ResultAttrs {}
+
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the requested word was found
+ * in the dictionary in the text service.
+ */
+ public static final int RESULT_ATTR_IN_THE_DICTIONARY = 0x0001;
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the text service thinks the requested
+ * word looks like a typo.
+ */
+ public static final int RESULT_ATTR_LOOKS_LIKE_TYPO = 0x0002;
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the text service thinks
+ * the result suggestions include highly recommended ones.
+ */
+ public static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = 0x0004;
+
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the text service thinks the requested
+ * sentence contains a grammar error.
+ */
+ public static final int RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR = 0x0008;
+
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the text service has an alternative way to
+ * show UI for the list of correction suggestions to the user. When this flag is set, the
+ * receiver of the result suggestions should mark the erroneous part of the text with a text
+ * signifier (for example, underline), but should not show any UI for the list of correction
+ * suggestions to the user (for example, in a popup window).
+ */
+ public static final int RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS = 0x0010;
+
+ private final @ResultAttrs int mSuggestionsAttributes;
+ private final String[] mSuggestions;
+ private final boolean mSuggestionsAvailable;
+ private int mCookie;
+ private int mSequence;
+
+ /**
+ * Constructor.
+ * @param suggestionsAttributes from the text service
+ * @param suggestions from the text service
+ */
+ public SuggestionsInfo(int suggestionsAttributes, String[] suggestions) {
+ this(suggestionsAttributes, suggestions, 0, 0);
+ }
+
+ /**
+ * Constructor.
+ * @param suggestionsAttributes from the text service
+ * @param suggestions from the text service
+ * @param cookie the cookie of the input TextInfo
+ * @param sequence the cookie of the input TextInfo
+ */
+ public SuggestionsInfo(@ResultAttrs int suggestionsAttributes, String[] suggestions, int cookie,
+ int sequence) {
+ if (suggestions == null) {
+ mSuggestions = EMPTY;
+ mSuggestionsAvailable = false;
+ } else {
+ mSuggestions = suggestions;
+ mSuggestionsAvailable = true;
+ }
+ mSuggestionsAttributes = suggestionsAttributes;
+ mCookie = cookie;
+ mSequence = sequence;
+ }
+
+ public SuggestionsInfo(Parcel source) {
+ mSuggestionsAttributes = source.readInt();
+ mSuggestions = source.readStringArray();
+ mCookie = source.readInt();
+ mSequence = source.readInt();
+ mSuggestionsAvailable = source.readInt() == 1;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSuggestionsAttributes);
+ dest.writeStringArray(mSuggestions);
+ dest.writeInt(mCookie);
+ dest.writeInt(mSequence);
+ dest.writeInt(mSuggestionsAvailable ? 1 : 0);
+ }
+
+ /**
+ * Set the cookie and the sequence of SuggestionsInfo which are set to TextInfo from a client
+ * application
+ * @param cookie the cookie of an input TextInfo
+ * @param sequence the cookie of an input TextInfo
+ */
+ public void setCookieAndSequence(int cookie, int sequence) {
+ mCookie = cookie;
+ mSequence = sequence;
+ }
+
+ /**
+ * @return the cookie which may be set by a client application
+ */
+ public int getCookie() {
+ return mCookie;
+ }
+
+ /**
+ * @return the sequence which may be set by a client application
+ */
+ public int getSequence() {
+ return mSequence;
+ }
+
+ /**
+ * @return the attributes of suggestions. This includes whether the spell checker has the word
+ * in its dictionary or not and whether the spell checker has confident suggestions for the
+ * word or not.
+ */
+ public @ResultAttrs int getSuggestionsAttributes() {
+ return mSuggestionsAttributes;
+ }
+
+ /**
+ * @return the count of the suggestions. If there's no suggestions at all, this method returns
+ * -1. Even if this method returns 0, it doesn't necessarily mean that there are no suggestions
+ * for the requested word. For instance, the caller could have been asked to limit the maximum
+ * number of suggestions returned.
+ */
+ public int getSuggestionsCount() {
+ if (!mSuggestionsAvailable) {
+ return -1;
+ }
+ return mSuggestions.length;
+ }
+
+ /**
+ * @param i the id of suggestions
+ * @return the suggestion at the specified id
+ */
+ public String getSuggestionAt(int i) {
+ return mSuggestions[i];
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<SuggestionsInfo> CREATOR
+ = new Parcelable.Creator<SuggestionsInfo>() {
+ @Override
+ public SuggestionsInfo createFromParcel(Parcel source) {
+ return new SuggestionsInfo(source);
+ }
+
+ @Override
+ public SuggestionsInfo[] newArray(int size) {
+ return new SuggestionsInfo[size];
+ }
+ };
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/textservice/TextInfo.java b/android/view/textservice/TextInfo.java
new file mode 100644
index 0000000..571bdf3
--- /dev/null
+++ b/android/view/textservice/TextInfo.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.view.textservice;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.ParcelableSpan;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.SpellCheckSpan;
+
+/**
+ * This class contains a metadata of the input of TextService
+ */
+public final class TextInfo implements Parcelable {
+ private final CharSequence mCharSequence;
+ private final int mCookie;
+ private final int mSequenceNumber;
+
+ private static final int DEFAULT_COOKIE = 0;
+ private static final int DEFAULT_SEQUENCE_NUMBER = 0;
+
+ /**
+ * Constructor.
+ * @param text the text which will be input to TextService
+ */
+ public TextInfo(String text) {
+ this(text, 0, getStringLengthOrZero(text), DEFAULT_COOKIE, DEFAULT_SEQUENCE_NUMBER);
+ }
+
+ /**
+ * Constructor.
+ * @param text the text which will be input to TextService
+ * @param cookie the cookie for this TextInfo
+ * @param sequenceNumber the sequence number for this TextInfo
+ */
+ public TextInfo(String text, int cookie, int sequenceNumber) {
+ this(text, 0, getStringLengthOrZero(text), cookie, sequenceNumber);
+ }
+
+ private static int getStringLengthOrZero(final String text) {
+ return TextUtils.isEmpty(text) ? 0 : text.length();
+ }
+
+ /**
+ * Constructor.
+ * @param charSequence the text which will be input to TextService. Attached spans that
+ * implement {@link ParcelableSpan} will also be marshaled alongside with the text.
+ * @param start the beginning of the range of text (inclusive).
+ * @param end the end of the range of text (exclusive).
+ * @param cookie the cookie for this TextInfo
+ * @param sequenceNumber the sequence number for this TextInfo
+ */
+ public TextInfo(CharSequence charSequence, int start, int end, int cookie, int sequenceNumber) {
+ if (TextUtils.isEmpty(charSequence)) {
+ throw new IllegalArgumentException("charSequence is empty");
+ }
+ // Create a snapshot of the text including spans in case they are updated outside later.
+ final SpannableStringBuilder spannableString =
+ new SpannableStringBuilder(charSequence, start, end);
+ // SpellCheckSpan is for internal use. We do not want to marshal this for TextService.
+ final SpellCheckSpan[] spans = spannableString.getSpans(0, spannableString.length(),
+ SpellCheckSpan.class);
+ for (int i = 0; i < spans.length; ++i) {
+ spannableString.removeSpan(spans[i]);
+ }
+
+ mCharSequence = spannableString;
+ mCookie = cookie;
+ mSequenceNumber = sequenceNumber;
+ }
+
+ public TextInfo(Parcel source) {
+ mCharSequence = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mCookie = source.readInt();
+ mSequenceNumber = source.readInt();
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ TextUtils.writeToParcel(mCharSequence, dest, flags);
+ dest.writeInt(mCookie);
+ dest.writeInt(mSequenceNumber);
+ }
+
+ /**
+ * @return the text which is an input of a text service
+ */
+ public String getText() {
+ if (mCharSequence == null) {
+ return null;
+ }
+ return mCharSequence.toString();
+ }
+
+ /**
+ * @return the charSequence which is an input of a text service. This may have some parcelable
+ * spans.
+ */
+ public CharSequence getCharSequence() {
+ return mCharSequence;
+ }
+
+ /**
+ * @return the cookie of TextInfo
+ */
+ public int getCookie() {
+ return mCookie;
+ }
+
+ /**
+ * @return the sequence of TextInfo
+ */
+ public int getSequence() {
+ return mSequenceNumber;
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<TextInfo> CREATOR
+ = new Parcelable.Creator<TextInfo>() {
+ @Override
+ public TextInfo createFromParcel(Parcel source) {
+ return new TextInfo(source);
+ }
+
+ @Override
+ public TextInfo[] newArray(int size) {
+ return new TextInfo[size];
+ }
+ };
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java
new file mode 100644
index 0000000..8e1f218
--- /dev/null
+++ b/android/view/textservice/TextServicesManager.java
@@ -0,0 +1,72 @@
+/*
+ * 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.view.textservice;
+
+import android.os.Bundle;
+import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
+
+import java.util.Locale;
+
+/**
+ * A stub class of TextServicesManager for Layout-Lib.
+ */
+public final class TextServicesManager {
+ private static final TextServicesManager sInstance = new TextServicesManager();
+ private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0];
+
+ /**
+ * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
+ * @hide
+ */
+ public static TextServicesManager getInstance() {
+ return sInstance;
+ }
+
+ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
+ SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ public SpellCheckerInfo[] getEnabledSpellCheckers() {
+ return EMPTY_SPELL_CHECKER_INFO;
+ }
+
+ /**
+ * @hide
+ */
+ public SpellCheckerInfo getCurrentSpellChecker() {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
+ boolean allowImplicitlySelectedSubtype) {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isSpellCheckerEnabled() {
+ return false;
+ }
+}
diff --git a/android/view/translation/TranslationCapability.java b/android/view/translation/TranslationCapability.java
new file mode 100644
index 0000000..65b749a
--- /dev/null
+++ b/android/view/translation/TranslationCapability.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Capability class holding information for a pair of {@link TranslationSpec}s.
+ *
+ * <p>Holds information and limitations on how to create a {@link TranslationContext} which can
+ * be used by
+ * {@link TranslationManager#createOnDeviceTranslator(TranslationContext, Executor, Consumer)}.
+ */
+@DataClass(genHiddenConstDefs = true, genToString = true, genConstructor = false)
+public final class TranslationCapability implements Parcelable {
+
+ /**
+ * TODO: fill in javadoc
+ */
+ public static final @ModelState int STATE_AVAILABLE_TO_DOWNLOAD = 1;
+ /**
+ * TODO: fill in javadoc
+ */
+ public static final @ModelState int STATE_DOWNLOADING = 2;
+ /**
+ * TODO: fill in javadoc
+ */
+ public static final @ModelState int STATE_ON_DEVICE = 3;
+ /**
+ * The translation service does not support translation between the source and target specs.
+ *
+ * <p>Note: This state is not returned from calling
+ * {@link TranslationManager#getOnDeviceTranslationCapabilities}. This state will only appear as
+ * part of capability updates from
+ * {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener} if existing support
+ * was dropped.</p>
+ */
+ public static final @ModelState int STATE_NOT_AVAILABLE = 4;
+ /**
+ * The translation between the source and target specs were removed from the system, but is
+ * still available to be downloaded again.
+ *
+ * @hide
+ */
+ public static final @ModelState int STATE_REMOVED_AND_AVAILABLE = 1000;
+
+ /**
+ * The state of translation readiness between {@code mSourceSpec} and {@code mTargetSpec}.
+ */
+ private final @ModelState int mState;
+
+ /**
+ * {@link TranslationSpec} describing the source data specs for this
+ * capability.
+ */
+ @NonNull
+ private final TranslationSpec mSourceSpec;
+
+ /**
+ * {@link TranslationSpec} describing the target data specs for this
+ * capability.
+ */
+ @NonNull
+ private final TranslationSpec mTargetSpec;
+
+ /**
+ * Whether ui translation for the source-target {@link TranslationSpec}s is enabled.
+ *
+ * <p>Translation service will still support translation requests for this capability.</p>
+ */
+ private final boolean mUiTranslationEnabled;
+
+ /**
+ * Translation flags for settings that are supported by the
+ * {@link android.service.translation.TranslationService} between the {@link TranslationSpec}s
+ * provided in this capability.
+ */
+ private final @TranslationContext.TranslationFlag int mSupportedTranslationFlags;
+
+ /**
+ * Constructor for creating a {@link TranslationCapability}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public TranslationCapability(@ModelState int state, @NonNull TranslationSpec sourceSpec,
+ @NonNull TranslationSpec targetSpec, boolean uiTranslationEnabled,
+ @TranslationContext.TranslationFlag int supportedTranslationFlags) {
+ Objects.requireNonNull(sourceSpec, "sourceSpec should not be null");
+ Objects.requireNonNull(targetSpec, "targetSpec should not be null");
+
+ this.mState = state;
+ this.mSourceSpec = sourceSpec;
+ this.mTargetSpec = targetSpec;
+ this.mUiTranslationEnabled = uiTranslationEnabled;
+ this.mSupportedTranslationFlags = supportedTranslationFlags;
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationCapability.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @IntDef(prefix = "STATE_", value = {
+ STATE_AVAILABLE_TO_DOWNLOAD,
+ STATE_DOWNLOADING,
+ STATE_ON_DEVICE,
+ STATE_NOT_AVAILABLE,
+ STATE_REMOVED_AND_AVAILABLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface ModelState {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String modelStateToString(@ModelState int value) {
+ switch (value) {
+ case STATE_AVAILABLE_TO_DOWNLOAD:
+ return "STATE_AVAILABLE_TO_DOWNLOAD";
+ case STATE_DOWNLOADING:
+ return "STATE_DOWNLOADING";
+ case STATE_ON_DEVICE:
+ return "STATE_ON_DEVICE";
+ case STATE_NOT_AVAILABLE:
+ return "STATE_NOT_AVAILABLE";
+ case STATE_REMOVED_AND_AVAILABLE:
+ return "STATE_REMOVED_AND_AVAILABLE";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ /**
+ * The state of translation readiness between {@code mSourceSpec} and {@code mTargetSpec}.
+ */
+ @DataClass.Generated.Member
+ public @ModelState int getState() {
+ return mState;
+ }
+
+ /**
+ * {@link TranslationSpec} describing the source data specs for this
+ * capability.
+ */
+ @DataClass.Generated.Member
+ public @NonNull TranslationSpec getSourceSpec() {
+ return mSourceSpec;
+ }
+
+ /**
+ * {@link TranslationSpec} describing the target data specs for this
+ * capability.
+ */
+ @DataClass.Generated.Member
+ public @NonNull TranslationSpec getTargetSpec() {
+ return mTargetSpec;
+ }
+
+ /**
+ * Whether ui translation for the source-target {@link TranslationSpec}s is enabled.
+ *
+ * <p>Translation service will still support translation requests for this capability.</p>
+ */
+ @DataClass.Generated.Member
+ public boolean isUiTranslationEnabled() {
+ return mUiTranslationEnabled;
+ }
+
+ /**
+ * Translation flags for settings that are supported by the
+ * {@link android.service.translation.TranslationService} between the {@link TranslationSpec}s
+ * provided in this capability.
+ */
+ @DataClass.Generated.Member
+ public @TranslationContext.TranslationFlag int getSupportedTranslationFlags() {
+ return mSupportedTranslationFlags;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationCapability { " +
+ "state = " + modelStateToString(mState) + ", " +
+ "sourceSpec = " + mSourceSpec + ", " +
+ "targetSpec = " + mTargetSpec + ", " +
+ "uiTranslationEnabled = " + mUiTranslationEnabled + ", " +
+ "supportedTranslationFlags = " + mSupportedTranslationFlags +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mUiTranslationEnabled) flg |= 0x8;
+ dest.writeByte(flg);
+ dest.writeInt(mState);
+ dest.writeTypedObject(mSourceSpec, flags);
+ dest.writeTypedObject(mTargetSpec, flags);
+ dest.writeInt(mSupportedTranslationFlags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationCapability(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean uiTranslationEnabled = (flg & 0x8) != 0;
+ int state = in.readInt();
+ TranslationSpec sourceSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR);
+ TranslationSpec targetSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR);
+ int supportedTranslationFlags = in.readInt();
+
+ this.mState = state;
+
+ if (!(mState == STATE_AVAILABLE_TO_DOWNLOAD)
+ && !(mState == STATE_DOWNLOADING)
+ && !(mState == STATE_ON_DEVICE)
+ && !(mState == STATE_NOT_AVAILABLE)
+ && !(mState == STATE_REMOVED_AND_AVAILABLE)) {
+ throw new java.lang.IllegalArgumentException(
+ "state was " + mState + " but must be one of: "
+ + "STATE_AVAILABLE_TO_DOWNLOAD(" + STATE_AVAILABLE_TO_DOWNLOAD + "), "
+ + "STATE_DOWNLOADING(" + STATE_DOWNLOADING + "), "
+ + "STATE_ON_DEVICE(" + STATE_ON_DEVICE + "), "
+ + "STATE_NOT_AVAILABLE(" + STATE_NOT_AVAILABLE + "), "
+ + "STATE_REMOVED_AND_AVAILABLE(" + STATE_REMOVED_AND_AVAILABLE + ")");
+ }
+
+ this.mSourceSpec = sourceSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSourceSpec);
+ this.mTargetSpec = targetSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTargetSpec);
+ this.mUiTranslationEnabled = uiTranslationEnabled;
+ this.mSupportedTranslationFlags = supportedTranslationFlags;
+ com.android.internal.util.AnnotationValidations.validate(
+ TranslationContext.TranslationFlag.class, null, mSupportedTranslationFlags);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationCapability> CREATOR
+ = new Parcelable.Creator<TranslationCapability>() {
+ @Override
+ public TranslationCapability[] newArray(int size) {
+ return new TranslationCapability[size];
+ }
+
+ @Override
+ public TranslationCapability createFromParcel(@NonNull android.os.Parcel in) {
+ return new TranslationCapability(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1624307114468L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationCapability.java",
+ inputSignatures = "public static final @android.view.translation.TranslationCapability.ModelState int STATE_AVAILABLE_TO_DOWNLOAD\npublic static final @android.view.translation.TranslationCapability.ModelState int STATE_DOWNLOADING\npublic static final @android.view.translation.TranslationCapability.ModelState int STATE_ON_DEVICE\npublic static final @android.view.translation.TranslationCapability.ModelState int STATE_NOT_AVAILABLE\npublic static final @android.view.translation.TranslationCapability.ModelState int STATE_REMOVED_AND_AVAILABLE\nprivate final @android.view.translation.TranslationCapability.ModelState int mState\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mSourceSpec\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mTargetSpec\nprivate final boolean mUiTranslationEnabled\nprivate final @android.view.translation.TranslationContext.TranslationFlag int mSupportedTranslationFlags\nclass TranslationCapability extends java.lang.Object implements [android.os.Parcelable]\[email protected](genHiddenConstDefs=true, genToString=true, genConstructor=false)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/TranslationContext.java b/android/view/translation/TranslationContext.java
new file mode 100644
index 0000000..210fe32
--- /dev/null
+++ b/android/view/translation/TranslationContext.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Info class holding information for {@link Translator}s and used by
+ * {@link TranslationManager#createOnDeviceTranslator(TranslationContext, Executor, Consumer)}.
+ */
+@DataClass(genHiddenConstDefs = true, genToString = true, genBuilder = true)
+public final class TranslationContext implements Parcelable {
+
+ /**
+ * This context will perform translations in low latency mode.
+ */
+ public static final @TranslationFlag int FLAG_LOW_LATENCY = 0x1;
+ /**
+ * This context will enable the {@link Translator} to return transliteration results.
+ */
+ public static final @TranslationFlag int FLAG_TRANSLITERATION = 0x2;
+ /**
+ * This context will enable the {@link Translator} to return dictionary definitions.
+ */
+ public static final @TranslationFlag int FLAG_DEFINITIONS = 0x4;
+
+ /**
+ * {@link TranslationSpec} describing the source data to be translated.
+ */
+ @NonNull
+ private final TranslationSpec mSourceSpec;
+
+ /**
+ * {@link TranslationSpec} describing the target translated data.
+ */
+ @NonNull
+ private final TranslationSpec mTargetSpec;
+
+ /**
+ * Translation flags to be used by the {@link Translator}
+ */
+ private final @TranslationFlag int mTranslationFlags;
+
+ private static int defaultTranslationFlags() {
+ return 0;
+ }
+
+ @DataClass.Suppress({"setSourceSpec", "setTargetSpec"})
+ abstract static class BaseBuilder {
+
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationContext.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @android.annotation.IntDef(flag = true, prefix = "FLAG_", value = {
+ FLAG_LOW_LATENCY,
+ FLAG_TRANSLITERATION,
+ FLAG_DEFINITIONS
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface TranslationFlag {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String translationFlagToString(@TranslationFlag int value) {
+ return com.android.internal.util.BitUtils.flagsToString(
+ value, TranslationContext::singleTranslationFlagToString);
+ }
+
+ @DataClass.Generated.Member
+ static String singleTranslationFlagToString(@TranslationFlag int value) {
+ switch (value) {
+ case FLAG_LOW_LATENCY:
+ return "FLAG_LOW_LATENCY";
+ case FLAG_TRANSLITERATION:
+ return "FLAG_TRANSLITERATION";
+ case FLAG_DEFINITIONS:
+ return "FLAG_DEFINITIONS";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ @DataClass.Generated.Member
+ /* package-private */ TranslationContext(
+ @NonNull TranslationSpec sourceSpec,
+ @NonNull TranslationSpec targetSpec,
+ @TranslationFlag int translationFlags) {
+ this.mSourceSpec = sourceSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSourceSpec);
+ this.mTargetSpec = targetSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTargetSpec);
+ this.mTranslationFlags = translationFlags;
+
+ com.android.internal.util.Preconditions.checkFlagsArgument(
+ mTranslationFlags,
+ FLAG_LOW_LATENCY
+ | FLAG_TRANSLITERATION
+ | FLAG_DEFINITIONS);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * {@link TranslationSpec} describing the source data to be translated.
+ */
+ @DataClass.Generated.Member
+ public @NonNull TranslationSpec getSourceSpec() {
+ return mSourceSpec;
+ }
+
+ /**
+ * {@link TranslationSpec} describing the target translated data.
+ */
+ @DataClass.Generated.Member
+ public @NonNull TranslationSpec getTargetSpec() {
+ return mTargetSpec;
+ }
+
+ /**
+ * Translation flags to be used by the {@link Translator}
+ */
+ @DataClass.Generated.Member
+ public @TranslationFlag int getTranslationFlags() {
+ return mTranslationFlags;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationContext { " +
+ "sourceSpec = " + mSourceSpec + ", " +
+ "targetSpec = " + mTargetSpec + ", " +
+ "translationFlags = " + translationFlagToString(mTranslationFlags) +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedObject(mSourceSpec, flags);
+ dest.writeTypedObject(mTargetSpec, flags);
+ dest.writeInt(mTranslationFlags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationContext(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ TranslationSpec sourceSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR);
+ TranslationSpec targetSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR);
+ int translationFlags = in.readInt();
+
+ this.mSourceSpec = sourceSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSourceSpec);
+ this.mTargetSpec = targetSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTargetSpec);
+ this.mTranslationFlags = translationFlags;
+
+ com.android.internal.util.Preconditions.checkFlagsArgument(
+ mTranslationFlags,
+ FLAG_LOW_LATENCY
+ | FLAG_TRANSLITERATION
+ | FLAG_DEFINITIONS);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationContext> CREATOR
+ = new Parcelable.Creator<TranslationContext>() {
+ @Override
+ public TranslationContext[] newArray(int size) {
+ return new TranslationContext[size];
+ }
+
+ @Override
+ public TranslationContext createFromParcel(@NonNull android.os.Parcel in) {
+ return new TranslationContext(in);
+ }
+ };
+
+ /**
+ * A builder for {@link TranslationContext}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @NonNull TranslationSpec mSourceSpec;
+ private @NonNull TranslationSpec mTargetSpec;
+ private @TranslationFlag int mTranslationFlags;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param sourceSpec
+ * {@link TranslationSpec} describing the source data to be translated.
+ * @param targetSpec
+ * {@link TranslationSpec} describing the target translated data.
+ */
+ public Builder(
+ @NonNull TranslationSpec sourceSpec,
+ @NonNull TranslationSpec targetSpec) {
+ mSourceSpec = sourceSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSourceSpec);
+ mTargetSpec = targetSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTargetSpec);
+ }
+
+ /**
+ * Translation flags to be used by the {@link Translator}
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setTranslationFlags(@TranslationFlag int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mTranslationFlags = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull TranslationContext build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mTranslationFlags = defaultTranslationFlags();
+ }
+ TranslationContext o = new TranslationContext(
+ mSourceSpec,
+ mTargetSpec,
+ mTranslationFlags);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x8) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1621545292157L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationContext.java",
+ inputSignatures = "public static final @android.view.translation.TranslationContext.TranslationFlag int FLAG_LOW_LATENCY\npublic static final @android.view.translation.TranslationContext.TranslationFlag int FLAG_TRANSLITERATION\npublic static final @android.view.translation.TranslationContext.TranslationFlag int FLAG_DEFINITIONS\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mSourceSpec\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mTargetSpec\nprivate final @android.view.translation.TranslationContext.TranslationFlag int mTranslationFlags\nprivate static int defaultTranslationFlags()\nclass TranslationContext extends java.lang.Object implements [android.os.Parcelable]\nclass BaseBuilder extends java.lang.Object implements []\[email protected](genHiddenConstDefs=true, genToString=true, genBuilder=true)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/TranslationManager.java b/android/view/translation/TranslationManager.java
new file mode 100644
index 0000000..54c455c
--- /dev/null
+++ b/android/view/translation/TranslationManager.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.annotation.WorkerThread;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.SynchronousResultReceiver;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.SyncResultReceiver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+/**
+ * The {@link TranslationManager} class provides ways for apps to integrate and use the
+ * translation framework.
+ *
+ * <p>The TranslationManager manages {@link Translator}s and help bridge client calls to
+ * the server {@link android.service.translation.TranslationService} </p>
+ */
+@SystemService(Context.TRANSLATION_MANAGER_SERVICE)
+public final class TranslationManager {
+
+ private static final String TAG = "TranslationManager";
+
+ /**
+ * Timeout for calls to system_server, default 1 minute.
+ */
+ static final int SYNC_CALLS_TIMEOUT_MS = 60_000;
+ /**
+ * The result code from result receiver success.
+ * @hide
+ */
+ public static final int STATUS_SYNC_CALL_SUCCESS = 1;
+ /**
+ * The result code from result receiver fail.
+ * @hide
+ */
+ public static final int STATUS_SYNC_CALL_FAIL = 2;
+
+ /**
+ * Name of the extra used to pass the translation capabilities.
+ * @hide
+ */
+ public static final String EXTRA_CAPABILITIES = "translation_capabilities";
+
+ @GuardedBy("mLock")
+ private final ArrayMap<Pair<Integer, Integer>, ArrayList<PendingIntent>>
+ mTranslationCapabilityUpdateListeners = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ private final Map<Consumer<TranslationCapability>, IRemoteCallback> mCapabilityCallbacks =
+ new ArrayMap<>();
+
+ private static final Random ID_GENERATOR = new Random();
+ private final Object mLock = new Object();
+
+ @NonNull
+ private final Context mContext;
+
+ private final ITranslationManager mService;
+
+ @NonNull
+ @GuardedBy("mLock")
+ private final SparseArray<Translator> mTranslators = new SparseArray<>();
+
+ @NonNull
+ @GuardedBy("mLock")
+ private final ArrayMap<TranslationContext, Integer> mTranslatorIds =
+ new ArrayMap<>();
+
+ @NonNull
+ private final Handler mHandler;
+
+ private static final AtomicInteger sAvailableRequestId = new AtomicInteger(1);
+
+ /**
+ * @hide
+ */
+ public TranslationManager(@NonNull Context context, ITranslationManager service) {
+ mContext = Objects.requireNonNull(context, "context cannot be null");
+ mService = service;
+
+ mHandler = Handler.createAsync(Looper.getMainLooper());
+ }
+
+ /**
+ * Creates an on-device Translator for natural language translation.
+ *
+ * @param translationContext {@link TranslationContext} containing the specs for creating the
+ * Translator.
+ * @param executor Executor to run callback operations
+ * @param callback {@link Consumer} to receive the translator. A {@code null} value is returned
+ * if the service could not create the translator.
+ */
+ public void createOnDeviceTranslator(@NonNull TranslationContext translationContext,
+ @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Translator> callback) {
+ Objects.requireNonNull(translationContext, "translationContext cannot be null");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+
+ synchronized (mLock) {
+ // TODO(b/176464808): Disallow multiple Translator now, it will throw
+ // IllegalStateException. Need to discuss if we can allow multiple Translators.
+ if (mTranslatorIds.containsKey(translationContext)) {
+ executor.execute(() -> callback.accept(
+ mTranslators.get(mTranslatorIds.get(translationContext))));
+ return;
+ }
+
+ int translatorId;
+ do {
+ translatorId = Math.abs(ID_GENERATOR.nextInt());
+ } while (translatorId == 0 || mTranslators.indexOfKey(translatorId) >= 0);
+ final int tId = translatorId;
+
+ new Translator(mContext, translationContext, translatorId, this, mHandler, mService,
+ new Consumer<Translator>() {
+ @Override
+ public void accept(Translator translator) {
+ if (translator == null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> callback.accept(null));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return;
+ }
+
+ synchronized (mLock) {
+ mTranslators.put(tId, translator);
+ mTranslatorIds.put(translationContext, tId);
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> callback.accept(translator));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Creates an on-device Translator for natural language translation.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * @removed use {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)}
+ * instead.
+ *
+ * @param translationContext {@link TranslationContext} containing the specs for creating the
+ * Translator.
+ */
+ @Deprecated
+ @Nullable
+ @WorkerThread
+ public Translator createOnDeviceTranslator(@NonNull TranslationContext translationContext) {
+ Objects.requireNonNull(translationContext, "translationContext cannot be null");
+
+ synchronized (mLock) {
+ // TODO(b/176464808): Disallow multiple Translator now, it will throw
+ // IllegalStateException. Need to discuss if we can allow multiple Translators.
+ if (mTranslatorIds.containsKey(translationContext)) {
+ return mTranslators.get(mTranslatorIds.get(translationContext));
+ }
+
+ int translatorId;
+ do {
+ translatorId = Math.abs(ID_GENERATOR.nextInt());
+ } while (translatorId == 0 || mTranslators.indexOfKey(translatorId) >= 0);
+
+ final Translator newTranslator = new Translator(mContext, translationContext,
+ translatorId, this, mHandler, mService);
+ // Start the Translator session and wait for the result
+ newTranslator.start();
+ try {
+ if (!newTranslator.isSessionCreated()) {
+ return null;
+ }
+ mTranslators.put(translatorId, newTranslator);
+ mTranslatorIds.put(translationContext, translatorId);
+ return newTranslator;
+ } catch (Translator.ServiceBinderReceiver.TimeoutException e) {
+ // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor
+ // public and use it.
+ Log.e(TAG, "Timed out getting create session: " + e);
+ return null;
+ }
+ }
+ }
+
+ /** @removed Use {@link #createOnDeviceTranslator(TranslationContext)} */
+ @Deprecated
+ @Nullable
+ @WorkerThread
+ public Translator createTranslator(@NonNull TranslationContext translationContext) {
+ return createOnDeviceTranslator(translationContext);
+ }
+
+ /**
+ * Returns a set of {@link TranslationCapability}s describing the capabilities for on-device
+ * {@link Translator}s.
+ *
+ * <p>These translation capabilities contains a source and target {@link TranslationSpec}
+ * representing the data expected for both ends of translation process. The capabilities
+ * provides the information and limitations for generating a {@link TranslationContext}.
+ * The context object can then be used by
+ * {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} to obtain a
+ * {@link Translator} for translations.</p>
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
+ * @param sourceFormat data format for the input data to be translated.
+ * @param targetFormat data format for the expected translated output data.
+ * @return A set of {@link TranslationCapability}s.
+ */
+ @NonNull
+ @WorkerThread
+ public Set<TranslationCapability> getOnDeviceTranslationCapabilities(
+ @TranslationSpec.DataFormat int sourceFormat,
+ @TranslationSpec.DataFormat int targetFormat) {
+ try {
+ final SynchronousResultReceiver receiver = new SynchronousResultReceiver();
+ mService.onTranslationCapabilitiesRequest(sourceFormat, targetFormat, receiver,
+ mContext.getUserId());
+ final SynchronousResultReceiver.Result result =
+ receiver.awaitResult(SYNC_CALLS_TIMEOUT_MS);
+ if (result.resultCode != STATUS_SYNC_CALL_SUCCESS) {
+ return Collections.emptySet();
+ }
+ Parcelable[] parcelables = result.bundle.getParcelableArray(EXTRA_CAPABILITIES);
+ ArraySet<TranslationCapability> capabilities = new ArraySet();
+ for (Parcelable obj : parcelables) {
+ if (obj instanceof TranslationCapability) {
+ capabilities.add((TranslationCapability) obj);
+ }
+ }
+ return capabilities;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (TimeoutException e) {
+ Log.e(TAG, "Timed out getting supported translation capabilities: " + e);
+ return Collections.emptySet();
+ }
+ }
+
+ /** @removed Use {@link #getOnDeviceTranslationCapabilities(int, int)} */
+ @Deprecated
+ @NonNull
+ @WorkerThread
+ public Set<TranslationCapability> getTranslationCapabilities(
+ @TranslationSpec.DataFormat int sourceFormat,
+ @TranslationSpec.DataFormat int targetFormat) {
+ return getOnDeviceTranslationCapabilities(sourceFormat, targetFormat);
+ }
+
+ /**
+ * Adds a {@link TranslationCapability} Consumer to listen for updates on states of on-device
+ * {@link TranslationCapability}s.
+ *
+ * @param capabilityListener a {@link TranslationCapability} Consumer to receive the updated
+ * {@link TranslationCapability} from the on-device translation service.
+ */
+ public void addOnDeviceTranslationCapabilityUpdateListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<TranslationCapability> capabilityListener) {
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(capabilityListener, "capability listener should not be null");
+
+ synchronized (mLock) {
+ if (mCapabilityCallbacks.containsKey(capabilityListener)) {
+ Log.w(TAG, "addOnDeviceTranslationCapabilityUpdateListener: the listener for "
+ + capabilityListener + " already registered; ignoring.");
+ return;
+ }
+ final IRemoteCallback remoteCallback = new TranslationCapabilityRemoteCallback(executor,
+ capabilityListener);
+ try {
+ mService.registerTranslationCapabilityCallback(remoteCallback,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCapabilityCallbacks.put(capabilityListener, remoteCallback);
+ }
+ }
+
+
+ /**
+ * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener(
+ * java.util.concurrent.Executor, java.util.function.Consumer)}
+ */
+ @Deprecated
+ public void addOnDeviceTranslationCapabilityUpdateListener(
+ @TranslationSpec.DataFormat int sourceFormat,
+ @TranslationSpec.DataFormat int targetFormat,
+ @NonNull PendingIntent pendingIntent) {
+ Objects.requireNonNull(pendingIntent, "pending intent should not be null");
+
+ synchronized (mLock) {
+ final Pair<Integer, Integer> formatPair = new Pair<>(sourceFormat, targetFormat);
+ mTranslationCapabilityUpdateListeners.computeIfAbsent(formatPair,
+ (formats) -> new ArrayList<>()).add(pendingIntent);
+ }
+ }
+
+ /**
+ * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener(
+ * java.util.concurrent.Executor, java.util.function.Consumer)}
+ */
+ @Deprecated
+ public void addTranslationCapabilityUpdateListener(
+ @TranslationSpec.DataFormat int sourceFormat,
+ @TranslationSpec.DataFormat int targetFormat,
+ @NonNull PendingIntent pendingIntent) {
+ addOnDeviceTranslationCapabilityUpdateListener(sourceFormat, targetFormat, pendingIntent);
+ }
+
+ /**
+ * Removes a {@link TranslationCapability} Consumer to listen for updates on states of
+ * on-device {@link TranslationCapability}s.
+ *
+ * @param capabilityListener the {@link TranslationCapability} Consumer to unregister
+ */
+ public void removeOnDeviceTranslationCapabilityUpdateListener(
+ @NonNull Consumer<TranslationCapability> capabilityListener) {
+ Objects.requireNonNull(capabilityListener, "capability callback should not be null");
+
+ synchronized (mLock) {
+ final IRemoteCallback remoteCallback = mCapabilityCallbacks.get(capabilityListener);
+ if (remoteCallback == null) {
+ Log.w(TAG, "removeOnDeviceTranslationCapabilityUpdateListener: the capability "
+ + "listener not found; ignoring.");
+ return;
+ }
+ try {
+ mService.unregisterTranslationCapabilityCallback(remoteCallback,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCapabilityCallbacks.remove(capabilityListener);
+ }
+ }
+
+ /**
+ * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener(
+ * java.util.function.Consumer)}.
+ */
+ @Deprecated
+ public void removeOnDeviceTranslationCapabilityUpdateListener(
+ @TranslationSpec.DataFormat int sourceFormat,
+ @TranslationSpec.DataFormat int targetFormat,
+ @NonNull PendingIntent pendingIntent) {
+ Objects.requireNonNull(pendingIntent, "pending intent should not be null");
+
+ synchronized (mLock) {
+ final Pair<Integer, Integer> formatPair = new Pair<>(sourceFormat, targetFormat);
+ if (mTranslationCapabilityUpdateListeners.containsKey(formatPair)) {
+ final ArrayList<PendingIntent> intents =
+ mTranslationCapabilityUpdateListeners.get(formatPair);
+ if (intents.contains(pendingIntent)) {
+ intents.remove(pendingIntent);
+ } else {
+ Log.w(TAG, "pending intent=" + pendingIntent + " does not exist in "
+ + "mTranslationCapabilityUpdateListeners");
+ }
+ } else {
+ Log.w(TAG, "format pair=" + formatPair + " does not exist in "
+ + "mTranslationCapabilityUpdateListeners");
+ }
+ }
+ }
+
+ /**
+ * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener(
+ * java.util.function.Consumer)}.
+ */
+ @Deprecated
+ public void removeTranslationCapabilityUpdateListener(
+ @TranslationSpec.DataFormat int sourceFormat,
+ @TranslationSpec.DataFormat int targetFormat,
+ @NonNull PendingIntent pendingIntent) {
+ removeOnDeviceTranslationCapabilityUpdateListener(
+ sourceFormat, targetFormat, pendingIntent);
+ }
+
+ /**
+ * Returns an immutable PendingIntent which can be used to launch an activity to view/edit
+ * on-device translation settings.
+ *
+ * @return An immutable PendingIntent or {@code null} if one of reason met:
+ * <ul>
+ * <li>Device manufacturer (OEM) does not provide TranslationService.</li>
+ * <li>The TranslationService doesn't provide the Settings.</li>
+ * </ul>
+ **/
+ @Nullable
+ public PendingIntent getOnDeviceTranslationSettingsActivityIntent() {
+ final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.getServiceSettingsActivity(resultReceiver, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ try {
+ return resultReceiver.getParcelableResult();
+ } catch (SyncResultReceiver.TimeoutException e) {
+ Log.e(TAG, "Fail to get translation service settings activity.");
+ return null;
+ }
+ }
+
+ /** @removed Use {@link #getOnDeviceTranslationSettingsActivityIntent()} */
+ @Deprecated
+ @Nullable
+ public PendingIntent getTranslationSettingsActivityIntent() {
+ return getOnDeviceTranslationSettingsActivityIntent();
+ }
+
+ void removeTranslator(int id) {
+ synchronized (mLock) {
+ mTranslators.remove(id);
+ for (int i = 0; i < mTranslatorIds.size(); i++) {
+ if (mTranslatorIds.valueAt(i) == id) {
+ mTranslatorIds.removeAt(i);
+ break;
+ }
+ }
+ }
+ }
+
+ AtomicInteger getAvailableRequestId() {
+ synchronized (mLock) {
+ return sAvailableRequestId;
+ }
+ }
+
+ private static class TranslationCapabilityRemoteCallback extends
+ IRemoteCallback.Stub {
+ private final Executor mExecutor;
+ private final Consumer<TranslationCapability> mListener;
+
+ TranslationCapabilityRemoteCallback(Executor executor,
+ Consumer<TranslationCapability> listener) {
+ mExecutor = executor;
+ mListener = listener;
+ }
+
+ @Override
+ public void sendResult(Bundle bundle) {
+ Binder.withCleanCallingIdentity(
+ () -> mExecutor.execute(() -> onTranslationCapabilityUpdate(bundle)));
+ }
+
+ private void onTranslationCapabilityUpdate(Bundle bundle) {
+ TranslationCapability capability =
+ (TranslationCapability) bundle.getParcelable(EXTRA_CAPABILITIES);
+ mListener.accept(capability);
+ }
+ }
+}
diff --git a/android/view/translation/TranslationRequest.java b/android/view/translation/TranslationRequest.java
new file mode 100644
index 0000000..df4836e
--- /dev/null
+++ b/android/view/translation/TranslationRequest.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Translation request sent to the {@link android.service.translation.TranslationService} by the
+ * {@link android.view.translation.Translator} which contains the text to be translated.
+ */
+@DataClass(genToString = true, genHiddenConstDefs = true, genBuilder = true)
+public final class TranslationRequest implements Parcelable {
+
+ /**
+ * Indicates this request wants to receive the standard translation result.
+ */
+ public static final @RequestFlags int FLAG_TRANSLATION_RESULT = 0x1;
+ /**
+ * Indicates this request wants to receive the dictionary result.
+ * TODO: describe the structure of the result.
+ */
+ public static final @RequestFlags int FLAG_DICTIONARY_RESULT = 0x2;
+ /**
+ * Indicates this request wants to receive the transliteration result.
+ * TODO: describe the structure of the result.
+ */
+ public static final @RequestFlags int FLAG_TRANSLITERATION_RESULT = 0x4;
+ /**
+ * Indicates this request is willing to accept partial responses.
+ *
+ * <p>The partial responses can be accessed by
+ * {@link TranslationResponse#getTranslationResponseValues()} or
+ * {@link TranslationResponse#getViewTranslationResponses()}. These responses will each contain
+ * only a subset of the corresponding translated values.
+ *
+ * <p>The are no guarantees to the number of translated values or the order in which these
+ * values are returned in the {@link TranslationResponse}.
+ *
+ * <p>This flag denotes the client can expect multiple partial responses, but there may not
+ * necessarily be multiple responses.</p>
+ */
+ public static final @RequestFlags int FLAG_PARTIAL_RESPONSES = 0x8;
+
+ /**
+ * Request flags. {@link #FLAG_TRANSLATION_RESULT} by default.
+ */
+ private final @RequestFlags int mFlags;
+
+ /**
+ * List of {@link TranslationRequestValue}s to be translated. The index of entries in this list
+ * will be their respective key in the {@link android.util.SparseArray} returned by calling
+ * {@link TranslationResponse#getTranslationResponseValues()}.
+ */
+ @NonNull
+ @DataClass.PluralOf("translationRequestValue")
+ private final List<TranslationRequestValue> mTranslationRequestValues;
+
+ /**
+ * List of {@link ViewTranslationRequest}s to be translated. The index of entries in this list
+ * will be their respective key in the {@link android.util.SparseArray} returned by calling
+ * {@link TranslationResponse#getViewTranslationResponses()}.
+ */
+ @NonNull
+ @DataClass.PluralOf("viewTranslationRequest")
+ private final List<ViewTranslationRequest> mViewTranslationRequests;
+
+ private static int defaultFlags() {
+ return FLAG_TRANSLATION_RESULT;
+ }
+
+ private static List<TranslationRequestValue> defaultTranslationRequestValues() {
+ return Collections.emptyList();
+ }
+
+ private static List<ViewTranslationRequest> defaultViewTranslationRequests() {
+ return Collections.emptyList();
+ }
+
+ abstract static class BaseBuilder {
+ /**
+ * @removed use {@link Builder#setTranslationRequestValues(List)}.
+ */
+ @Deprecated
+ public abstract Builder addTranslationRequestValue(
+ @NonNull TranslationRequestValue value);
+
+ /**
+ * @removed use {@link Builder#setViewTranslationRequests(List)}.
+ */
+ @Deprecated
+ public abstract Builder addViewTranslationRequest(
+ @NonNull ViewTranslationRequest value);
+
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @android.annotation.IntDef(flag = true, prefix = "FLAG_", value = {
+ FLAG_TRANSLATION_RESULT,
+ FLAG_DICTIONARY_RESULT,
+ FLAG_TRANSLITERATION_RESULT,
+ FLAG_PARTIAL_RESPONSES
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface RequestFlags {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String requestFlagsToString(@RequestFlags int value) {
+ return com.android.internal.util.BitUtils.flagsToString(
+ value, TranslationRequest::singleRequestFlagsToString);
+ }
+
+ @DataClass.Generated.Member
+ static String singleRequestFlagsToString(@RequestFlags int value) {
+ switch (value) {
+ case FLAG_TRANSLATION_RESULT:
+ return "FLAG_TRANSLATION_RESULT";
+ case FLAG_DICTIONARY_RESULT:
+ return "FLAG_DICTIONARY_RESULT";
+ case FLAG_TRANSLITERATION_RESULT:
+ return "FLAG_TRANSLITERATION_RESULT";
+ case FLAG_PARTIAL_RESPONSES:
+ return "FLAG_PARTIAL_RESPONSES";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ @DataClass.Generated.Member
+ /* package-private */ TranslationRequest(
+ @RequestFlags int flags,
+ @NonNull List<TranslationRequestValue> translationRequestValues,
+ @NonNull List<ViewTranslationRequest> viewTranslationRequests) {
+ this.mFlags = flags;
+
+ com.android.internal.util.Preconditions.checkFlagsArgument(
+ mFlags,
+ FLAG_TRANSLATION_RESULT
+ | FLAG_DICTIONARY_RESULT
+ | FLAG_TRANSLITERATION_RESULT
+ | FLAG_PARTIAL_RESPONSES);
+ this.mTranslationRequestValues = translationRequestValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationRequestValues);
+ this.mViewTranslationRequests = viewTranslationRequests;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mViewTranslationRequests);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * Request flags. {@link #FLAG_TRANSLATION_RESULT} by default.
+ */
+ @DataClass.Generated.Member
+ public @RequestFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * List of {@link TranslationRequestValue}s to be translated. The index of entries in this list
+ * will be their respective key in the {@link android.util.SparseArray} returned by calling
+ * {@link TranslationResponse#getTranslationResponseValues()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull List<TranslationRequestValue> getTranslationRequestValues() {
+ return mTranslationRequestValues;
+ }
+
+ /**
+ * List of {@link ViewTranslationRequest}s to be translated. The index of entries in this list
+ * will be their respective key in the {@link android.util.SparseArray} returned by calling
+ * {@link TranslationResponse#getViewTranslationResponses()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull List<ViewTranslationRequest> getViewTranslationRequests() {
+ return mViewTranslationRequests;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationRequest { " +
+ "flags = " + requestFlagsToString(mFlags) + ", " +
+ "translationRequestValues = " + mTranslationRequestValues + ", " +
+ "viewTranslationRequests = " + mViewTranslationRequests +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeInt(mFlags);
+ dest.writeParcelableList(mTranslationRequestValues, flags);
+ dest.writeParcelableList(mViewTranslationRequests, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ int flags = in.readInt();
+ List<TranslationRequestValue> translationRequestValues = new ArrayList<>();
+ in.readParcelableList(translationRequestValues, TranslationRequestValue.class.getClassLoader());
+ List<ViewTranslationRequest> viewTranslationRequests = new ArrayList<>();
+ in.readParcelableList(viewTranslationRequests, ViewTranslationRequest.class.getClassLoader());
+
+ this.mFlags = flags;
+
+ com.android.internal.util.Preconditions.checkFlagsArgument(
+ mFlags,
+ FLAG_TRANSLATION_RESULT
+ | FLAG_DICTIONARY_RESULT
+ | FLAG_TRANSLITERATION_RESULT
+ | FLAG_PARTIAL_RESPONSES);
+ this.mTranslationRequestValues = translationRequestValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationRequestValues);
+ this.mViewTranslationRequests = viewTranslationRequests;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mViewTranslationRequests);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationRequest> CREATOR
+ = new Parcelable.Creator<TranslationRequest>() {
+ @Override
+ public TranslationRequest[] newArray(int size) {
+ return new TranslationRequest[size];
+ }
+
+ @Override
+ public TranslationRequest createFromParcel(@NonNull Parcel in) {
+ return new TranslationRequest(in);
+ }
+ };
+
+ /**
+ * A builder for {@link TranslationRequest}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @RequestFlags int mFlags;
+ private @NonNull List<TranslationRequestValue> mTranslationRequestValues;
+ private @NonNull List<ViewTranslationRequest> mViewTranslationRequests;
+
+ private long mBuilderFieldsSet = 0L;
+
+ public Builder() {
+ }
+
+ /**
+ * Request flags. {@link #FLAG_TRANSLATION_RESULT} by default.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setFlags(@RequestFlags int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mFlags = value;
+ return this;
+ }
+
+ /**
+ * List of {@link TranslationRequestValue}s to be translated. The index of entries in this list
+ * will be their respective key in the {@link android.util.SparseArray} returned by calling
+ * {@link TranslationResponse#getTranslationResponseValues()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setTranslationRequestValues(@NonNull List<TranslationRequestValue> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mTranslationRequestValues = value;
+ return this;
+ }
+
+ /** @see #setTranslationRequestValues
+ * @removed
+ */
+ @DataClass.Generated.Member
+ @Override
+ @Deprecated
+ public @NonNull Builder addTranslationRequestValue(@NonNull TranslationRequestValue value) {
+ if (mTranslationRequestValues == null) setTranslationRequestValues(new ArrayList<>());
+ mTranslationRequestValues.add(value);
+ return this;
+ }
+
+ /**
+ * List of {@link ViewTranslationRequest}s to be translated. The index of entries in this list
+ * will be their respective key in the {@link android.util.SparseArray} returned by calling
+ * {@link TranslationResponse#getViewTranslationResponses()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setViewTranslationRequests(@NonNull List<ViewTranslationRequest> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mViewTranslationRequests = value;
+ return this;
+ }
+
+ /** @see #setViewTranslationRequests
+ * @removed
+ */
+ @DataClass.Generated.Member
+ @Override
+ @Deprecated
+ public @NonNull Builder addViewTranslationRequest(@NonNull ViewTranslationRequest value) {
+ if (mViewTranslationRequests == null) setViewTranslationRequests(new ArrayList<>());
+ mViewTranslationRequests.add(value);
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull TranslationRequest build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ mFlags = defaultFlags();
+ }
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mTranslationRequestValues = defaultTranslationRequestValues();
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mViewTranslationRequests = defaultViewTranslationRequests();
+ }
+ TranslationRequest o = new TranslationRequest(
+ mFlags,
+ mTranslationRequestValues,
+ mViewTranslationRequests);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x8) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1620429997487L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationRequest.java",
+ inputSignatures = "public static final @android.view.translation.TranslationRequest.RequestFlags int FLAG_TRANSLATION_RESULT\npublic static final @android.view.translation.TranslationRequest.RequestFlags int FLAG_DICTIONARY_RESULT\npublic static final @android.view.translation.TranslationRequest.RequestFlags int FLAG_TRANSLITERATION_RESULT\npublic static final @android.view.translation.TranslationRequest.RequestFlags int FLAG_PARTIAL_RESPONSES\nprivate final @android.view.translation.TranslationRequest.RequestFlags int mFlags\nprivate final @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"translationRequestValue\") java.util.List<android.view.translation.TranslationRequestValue> mTranslationRequestValues\nprivate final @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"viewTranslationRequest\") java.util.List<android.view.translation.ViewTranslationRequest> mViewTranslationRequests\nprivate static int defaultFlags()\nprivate static java.util.List<android.view.translation.TranslationRequestValue> defaultTranslationRequestValues()\nprivate static java.util.List<android.view.translation.ViewTranslationRequest> defaultViewTranslationRequests()\nclass TranslationRequest extends java.lang.Object implements [android.os.Parcelable]\npublic abstract @java.lang.Deprecated android.view.translation.TranslationRequest.Builder addTranslationRequestValue(android.view.translation.TranslationRequestValue)\npublic abstract @java.lang.Deprecated android.view.translation.TranslationRequest.Builder addViewTranslationRequest(android.view.translation.ViewTranslationRequest)\nclass BaseBuilder extends java.lang.Object implements []\[email protected](genToString=true, genHiddenConstDefs=true, genBuilder=true)\npublic abstract @java.lang.Deprecated android.view.translation.TranslationRequest.Builder addTranslationRequestValue(android.view.translation.TranslationRequestValue)\npublic abstract @java.lang.Deprecated android.view.translation.TranslationRequest.Builder addViewTranslationRequest(android.view.translation.ViewTranslationRequest)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/TranslationRequestValue.java b/android/view/translation/TranslationRequestValue.java
new file mode 100644
index 0000000..5178cb1
--- /dev/null
+++ b/android/view/translation/TranslationRequestValue.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Objects;
+
+/**
+ * A value to be translated via {@link android.view.translation.Translator}.
+ */
+@DataClass(genHiddenConstructor = true, genToString = true, genEqualsHashCode = true)
+public final class TranslationRequestValue implements Parcelable {
+
+ @Nullable
+ private final CharSequence mText;
+
+ /**
+ * Creates a {@link TranslationRequestValue} with a {@link CharSequence} value;
+ *
+ * @param text the text to be translated.
+ */
+ @NonNull
+ public static TranslationRequestValue forText(@NonNull CharSequence text) {
+ Objects.requireNonNull(text, "text should not be null");
+ return new TranslationRequestValue(text);
+ }
+
+ /**
+ * @return the text value as a {@link CharSequence} or {@code null} if the value is not of type
+ * text.
+ */
+ @Nullable
+ public CharSequence getText() {
+ return mText;
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationRequestValue.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new TranslationRequestValue.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public TranslationRequestValue(
+ @Nullable CharSequence text) {
+ this.mText = text;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationRequestValue { " +
+ "text = " + mText +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(TranslationRequestValue other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ TranslationRequestValue that = (TranslationRequestValue) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && Objects.equals(mText, that.mText);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + Objects.hashCode(mText);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mText != null) flg |= 0x1;
+ dest.writeByte(flg);
+ if (mText != null) dest.writeCharSequence(mText);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationRequestValue(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ CharSequence text = (flg & 0x1) == 0 ? null : (CharSequence) in.readCharSequence();
+
+ this.mText = text;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationRequestValue> CREATOR
+ = new Parcelable.Creator<TranslationRequestValue>() {
+ @Override
+ public TranslationRequestValue[] newArray(int size) {
+ return new TranslationRequestValue[size];
+ }
+
+ @Override
+ public TranslationRequestValue createFromParcel(@NonNull Parcel in) {
+ return new TranslationRequestValue(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1620259864154L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationRequestValue.java",
+ inputSignatures = "private final @android.annotation.Nullable java.lang.CharSequence mText\npublic static @android.annotation.NonNull android.view.translation.TranslationRequestValue forText(java.lang.CharSequence)\npublic @android.annotation.Nullable java.lang.CharSequence getText()\nclass TranslationRequestValue extends java.lang.Object implements [android.os.Parcelable]\[email protected](genHiddenConstructor=true, genToString=true, genEqualsHashCode=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/TranslationResponse.java b/android/view/translation/TranslationResponse.java
new file mode 100644
index 0000000..b77f2e2
--- /dev/null
+++ b/android/view/translation/TranslationResponse.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.translation.TranslationService;
+import android.util.SparseArray;
+
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Response from the {@link TranslationService}, which contains the translated result.
+ */
+@DataClass(genBuilder = true, genToString = true, genHiddenConstDefs = true)
+public final class TranslationResponse implements Parcelable {
+
+ /**
+ * The {@link TranslationService} was successful in translating.
+ */
+ public static final int TRANSLATION_STATUS_SUCCESS = 0;
+ /**
+ * The {@link TranslationService} returned unknown translation result.
+ */
+ public static final int TRANSLATION_STATUS_UNKNOWN_ERROR = 1;
+ /**
+ * The languages of the request is not available to be translated.
+ */
+ public static final int TRANSLATION_STATUS_CONTEXT_UNSUPPORTED = 2;
+
+ /**
+ * The translation result status code.
+ */
+ private final @TranslationStatus int mTranslationStatus;
+
+ /**
+ * List of translated {@link TranslationResponseValue}s. The key of entries in this list
+ * will be their respective index in {@link TranslationRequest#getTranslationRequestValues()}.
+ */
+ @NonNull
+ private final SparseArray<TranslationResponseValue> mTranslationResponseValues;
+
+ /**
+ * List of translated {@link ViewTranslationResponse}s. The key of entries in this list
+ * will be their respective index in {@link TranslationRequest#getViewTranslationRequests()}.
+ */
+ @NonNull
+ private final SparseArray<ViewTranslationResponse> mViewTranslationResponses;
+
+ /**
+ * Whether this response contains complete translated values, or is the final response in a
+ * series of partial responses.
+ *
+ * <p>This is {@code true} by default.</p>
+ */
+ private final boolean mFinalResponse;
+
+ abstract static class BaseBuilder {
+
+ /**
+ * @removed Use {@link Builder#Builder(int)}.
+ * @hide
+ */
+ @Deprecated
+ public abstract Builder setTranslationStatus(@TranslationStatus int value);
+
+ /**
+ * Adds {@link TranslationResponseValue} to be translated. The input
+ * TranslationResponseValue format should match those provided by the
+ * {@link android.view.translation.Translator}'s targetSpec.
+ *
+ * @param value the translated value.
+ * @return this Builder.
+ */
+ @NonNull
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ public Builder setTranslationResponseValue(int index,
+ @NonNull TranslationResponseValue value) {
+ Objects.requireNonNull(value, "value should not be null");
+ final Builder builder = (Builder) this;
+
+ if (builder.mTranslationResponseValues == null) {
+ builder.setTranslationResponseValues(new SparseArray<>());
+ }
+ builder.mTranslationResponseValues.put(index, value);
+ return builder;
+ }
+
+ /**
+ * Sets the list of {@link ViewTranslationResponse} to be translated. The input
+ * ViewTranslationResponse contains {@link TranslationResponseValue}s whose format should
+ * match those provided by the {@link android.view.translation.Translator}'s targetSpec.
+ *
+ * @param response the translated response.
+ * @return this Builder.
+ */
+ @NonNull
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ public Builder setViewTranslationResponse(int index,
+ @NonNull ViewTranslationResponse response) {
+ Objects.requireNonNull(response, "value should not be null");
+ final Builder builder = (Builder) this;
+
+ if (builder.mViewTranslationResponses == null) {
+ builder.setViewTranslationResponses(new SparseArray<>());
+ }
+ builder.mViewTranslationResponses.put(index, response);
+ return builder;
+ }
+ }
+
+ private static SparseArray<TranslationResponseValue> defaultTranslationResponseValues() {
+ return new SparseArray<>();
+ }
+
+ private static SparseArray<ViewTranslationResponse> defaultViewTranslationResponses() {
+ return new SparseArray<>();
+ }
+
+ private static boolean defaultFinalResponse() {
+ return true;
+ }
+
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @IntDef(prefix = "TRANSLATION_STATUS_", value = {
+ TRANSLATION_STATUS_SUCCESS,
+ TRANSLATION_STATUS_UNKNOWN_ERROR,
+ TRANSLATION_STATUS_CONTEXT_UNSUPPORTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface TranslationStatus {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String translationStatusToString(@TranslationStatus int value) {
+ switch (value) {
+ case TRANSLATION_STATUS_SUCCESS:
+ return "TRANSLATION_STATUS_SUCCESS";
+ case TRANSLATION_STATUS_UNKNOWN_ERROR:
+ return "TRANSLATION_STATUS_UNKNOWN_ERROR";
+ case TRANSLATION_STATUS_CONTEXT_UNSUPPORTED:
+ return "TRANSLATION_STATUS_CONTEXT_UNSUPPORTED";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ @DataClass.Generated.Member
+ /* package-private */ TranslationResponse(
+ @TranslationStatus int translationStatus,
+ @NonNull SparseArray<TranslationResponseValue> translationResponseValues,
+ @NonNull SparseArray<ViewTranslationResponse> viewTranslationResponses,
+ boolean finalResponse) {
+ this.mTranslationStatus = translationStatus;
+
+ if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS)
+ && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR)
+ && !(mTranslationStatus == TRANSLATION_STATUS_CONTEXT_UNSUPPORTED)) {
+ throw new java.lang.IllegalArgumentException(
+ "translationStatus was " + mTranslationStatus + " but must be one of: "
+ + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), "
+ + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), "
+ + "TRANSLATION_STATUS_CONTEXT_UNSUPPORTED(" + TRANSLATION_STATUS_CONTEXT_UNSUPPORTED + ")");
+ }
+
+ this.mTranslationResponseValues = translationResponseValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationResponseValues);
+ this.mViewTranslationResponses = viewTranslationResponses;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mViewTranslationResponses);
+ this.mFinalResponse = finalResponse;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The translation result status code.
+ */
+ @DataClass.Generated.Member
+ public @TranslationStatus int getTranslationStatus() {
+ return mTranslationStatus;
+ }
+
+ /**
+ * List of translated {@link TranslationResponseValue}s. The key of entries in this list
+ * will be their respective index in {@link TranslationRequest#getTranslationRequestValues()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull SparseArray<TranslationResponseValue> getTranslationResponseValues() {
+ return mTranslationResponseValues;
+ }
+
+ /**
+ * List of translated {@link ViewTranslationResponse}s. The key of entries in this list
+ * will be their respective index in {@link TranslationRequest#getViewTranslationRequests()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull SparseArray<ViewTranslationResponse> getViewTranslationResponses() {
+ return mViewTranslationResponses;
+ }
+
+ /**
+ * Whether this response contains complete translated values, or is the final response in a
+ * series of partial responses.
+ *
+ * <p>This is {@code true} by default.</p>
+ */
+ @DataClass.Generated.Member
+ public boolean isFinalResponse() {
+ return mFinalResponse;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationResponse { " +
+ "translationStatus = " + translationStatusToString(mTranslationStatus) + ", " +
+ "translationResponseValues = " + mTranslationResponseValues + ", " +
+ "viewTranslationResponses = " + mViewTranslationResponses + ", " +
+ "finalResponse = " + mFinalResponse +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mFinalResponse) flg |= 0x8;
+ dest.writeByte(flg);
+ dest.writeInt(mTranslationStatus);
+ dest.writeSparseArray(mTranslationResponseValues);
+ dest.writeSparseArray(mViewTranslationResponses);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationResponse(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean finalResponse = (flg & 0x8) != 0;
+ int translationStatus = in.readInt();
+ SparseArray<TranslationResponseValue> translationResponseValues = (SparseArray) in.readSparseArray(TranslationResponseValue.class.getClassLoader());
+ SparseArray<ViewTranslationResponse> viewTranslationResponses = (SparseArray) in.readSparseArray(ViewTranslationResponse.class.getClassLoader());
+
+ this.mTranslationStatus = translationStatus;
+
+ if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS)
+ && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR)
+ && !(mTranslationStatus == TRANSLATION_STATUS_CONTEXT_UNSUPPORTED)) {
+ throw new java.lang.IllegalArgumentException(
+ "translationStatus was " + mTranslationStatus + " but must be one of: "
+ + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), "
+ + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), "
+ + "TRANSLATION_STATUS_CONTEXT_UNSUPPORTED(" + TRANSLATION_STATUS_CONTEXT_UNSUPPORTED + ")");
+ }
+
+ this.mTranslationResponseValues = translationResponseValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationResponseValues);
+ this.mViewTranslationResponses = viewTranslationResponses;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mViewTranslationResponses);
+ this.mFinalResponse = finalResponse;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationResponse> CREATOR
+ = new Parcelable.Creator<TranslationResponse>() {
+ @Override
+ public TranslationResponse[] newArray(int size) {
+ return new TranslationResponse[size];
+ }
+
+ @Override
+ public TranslationResponse createFromParcel(@NonNull Parcel in) {
+ return new TranslationResponse(in);
+ }
+ };
+
+ /**
+ * A builder for {@link TranslationResponse}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @TranslationStatus int mTranslationStatus;
+ private @NonNull SparseArray<TranslationResponseValue> mTranslationResponseValues;
+ private @NonNull SparseArray<ViewTranslationResponse> mViewTranslationResponses;
+ private boolean mFinalResponse;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param translationStatus
+ * The translation result status code.
+ */
+ public Builder(
+ @TranslationStatus int translationStatus) {
+ mTranslationStatus = translationStatus;
+
+ if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS)
+ && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR)
+ && !(mTranslationStatus == TRANSLATION_STATUS_CONTEXT_UNSUPPORTED)) {
+ throw new java.lang.IllegalArgumentException(
+ "translationStatus was " + mTranslationStatus + " but must be one of: "
+ + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), "
+ + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), "
+ + "TRANSLATION_STATUS_CONTEXT_UNSUPPORTED(" + TRANSLATION_STATUS_CONTEXT_UNSUPPORTED + ")");
+ }
+
+ }
+
+ /**
+ * The translation result status code.
+ * @removed
+ */
+ @DataClass.Generated.Member
+ @Override
+ @Deprecated
+ public @NonNull Builder setTranslationStatus(@TranslationStatus int value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mTranslationStatus = value;
+ return this;
+ }
+
+ /**
+ * List of translated {@link TranslationResponseValue}s. The key of entries in this list
+ * will be their respective index in {@link TranslationRequest#getTranslationRequestValues()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setTranslationResponseValues(@NonNull SparseArray<TranslationResponseValue> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mTranslationResponseValues = value;
+ return this;
+ }
+
+ /**
+ * List of translated {@link ViewTranslationResponse}s. The key of entries in this list
+ * will be their respective index in {@link TranslationRequest#getViewTranslationRequests()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setViewTranslationResponses(@NonNull SparseArray<ViewTranslationResponse> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mViewTranslationResponses = value;
+ return this;
+ }
+
+ /**
+ * Whether this response contains complete translated values, or is the final response in a
+ * series of partial responses.
+ *
+ * <p>This is {@code true} by default.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setFinalResponse(boolean value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8;
+ mFinalResponse = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull TranslationResponse build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x10; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mTranslationResponseValues = defaultTranslationResponseValues();
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mViewTranslationResponses = defaultViewTranslationResponses();
+ }
+ if ((mBuilderFieldsSet & 0x8) == 0) {
+ mFinalResponse = defaultFinalResponse();
+ }
+ TranslationResponse o = new TranslationResponse(
+ mTranslationStatus,
+ mTranslationResponseValues,
+ mViewTranslationResponses,
+ mFinalResponse);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x10) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1621972659130L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationResponse.java",
+ inputSignatures = "public static final int TRANSLATION_STATUS_SUCCESS\npublic static final int TRANSLATION_STATUS_UNKNOWN_ERROR\npublic static final int TRANSLATION_STATUS_CONTEXT_UNSUPPORTED\nprivate final @android.view.translation.TranslationResponse.TranslationStatus int mTranslationStatus\nprivate final @android.annotation.NonNull android.util.SparseArray<android.view.translation.TranslationResponseValue> mTranslationResponseValues\nprivate final @android.annotation.NonNull android.util.SparseArray<android.view.translation.ViewTranslationResponse> mViewTranslationResponses\nprivate final boolean mFinalResponse\nprivate static android.util.SparseArray<android.view.translation.TranslationResponseValue> defaultTranslationResponseValues()\nprivate static android.util.SparseArray<android.view.translation.ViewTranslationResponse> defaultViewTranslationResponses()\nprivate static boolean defaultFinalResponse()\nclass TranslationResponse extends java.lang.Object implements [android.os.Parcelable]\npublic abstract @java.lang.Deprecated android.view.translation.TranslationResponse.Builder setTranslationStatus(int)\npublic @android.annotation.NonNull @java.lang.SuppressWarnings android.view.translation.TranslationResponse.Builder setTranslationResponseValue(int,android.view.translation.TranslationResponseValue)\npublic @android.annotation.NonNull @java.lang.SuppressWarnings android.view.translation.TranslationResponse.Builder setViewTranslationResponse(int,android.view.translation.ViewTranslationResponse)\nclass BaseBuilder extends java.lang.Object implements []\[email protected](genBuilder=true, genToString=true, genHiddenConstDefs=true)\npublic abstract @java.lang.Deprecated android.view.translation.TranslationResponse.Builder setTranslationStatus(int)\npublic @android.annotation.NonNull @java.lang.SuppressWarnings android.view.translation.TranslationResponse.Builder setTranslationResponseValue(int,android.view.translation.TranslationResponseValue)\npublic @android.annotation.NonNull @java.lang.SuppressWarnings android.view.translation.TranslationResponse.Builder setViewTranslationResponse(int,android.view.translation.ViewTranslationResponse)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/TranslationResponseValue.java b/android/view/translation/TranslationResponseValue.java
new file mode 100644
index 0000000..a24dbc3
--- /dev/null
+++ b/android/view/translation/TranslationResponseValue.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Objects;
+
+/**
+ * A translated response value from {@link android.service.translation.TranslationService}.
+ */
+@DataClass(genBuilder = true, genToString = true, genEqualsHashCode = true,
+ genHiddenConstDefs = true)
+public final class TranslationResponseValue implements Parcelable {
+
+ /**
+ * This value was successfully translated.
+ */
+ public static final int STATUS_SUCCESS = 0;
+ /**
+ * This value was not successfully translated. No value can be obtained with {@link #getText()}.
+ */
+ public static final int STATUS_ERROR = 1;
+
+ /**
+ * Name in the result of {@link #getExtras()} to pass dictionary definitions of the text
+ * categorized by parts of speech.
+ *
+ * <p>The dictionary definitions consists of groups of terms keyed by their corresponding parts
+ * of speech. This map-like structure is stored in a {@link Bundle}. The individual parts of
+ * speech can be traversed by {@link Bundle#keySet()} and used to get the corresponding list
+ * of terms as {@link CharSequence}s.
+ *
+ * <ul>
+ * <li>"noun" -> ["def1", "def2", ...]</li>
+ * <li>"verb" -> ["def3", "def4", ...]</li>
+ * <li>...</li>
+ * </ul>
+ *
+ * The set of parts of speech can then be used by
+ * {@link Bundle#getCharSequenceArrayList(String)} to get the list of terms.
+ *
+ * <b>Example</b>:
+ *
+ * {@code for (String partOfSpeech : extras.getBundle(EXTRA_DEFINITIONS).keySet()) {
+ * ArrayList<CharSequence> terms =
+ * extras.getBundle(EXTRA_DEFINITIONS).getCharSequenceArrayList(partOfSpeech);
+ * ...
+ * }}</p>
+ */
+ public static final String EXTRA_DEFINITIONS = "android.view.translation.extra.DEFINITIONS";
+
+ /**
+ * The status code of this {@link TranslationResponseValue}.
+ *
+ * <p>If the status code is {@link #STATUS_ERROR}, no values are attached, and all getters will
+ * return {@code null}.
+ */
+ private final @Status int mStatusCode;
+
+ /**
+ * The translated text result.
+ */
+ @Nullable
+ private final CharSequence mText;
+
+ /**
+ * Extra results associated with the translated text.
+ *
+ * <p>The bundle includes {@link #EXTRA_DEFINITIONS}, obtained by {@link Bundle#getBundle}.
+ * </p>
+ */
+ @NonNull
+ private final Bundle mExtras;
+
+ /**
+ * The transliteration result of the translated text.
+ * TODO: Describe the result structure.
+ */
+ @Nullable
+ private final CharSequence mTransliteration;
+
+ /**
+ * Creates a {@link TranslationResponseValue} with the {@link #STATUS_ERROR} result;
+ */
+ @NonNull
+ public static TranslationResponseValue forError() {
+ return new TranslationResponseValue(STATUS_ERROR, null, Bundle.EMPTY, null);
+ }
+
+ private static CharSequence defaultText() {
+ return null;
+ }
+
+ private static Bundle defaultExtras() {
+ return Bundle.EMPTY;
+ }
+
+ private boolean extrasEquals(Bundle other) {
+ // TODO: Also compare the contents.
+ return Objects.equals(mExtras, other) || (mExtras.isEmpty() && other.isEmpty());
+ }
+
+ private static CharSequence defaultTransliteration() {
+ return null;
+ }
+
+ @DataClass.Suppress("setStatusCode")
+ abstract static class BaseBuilder {
+
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationResponseValue.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @android.annotation.IntDef(prefix = "STATUS_", value = {
+ STATUS_SUCCESS,
+ STATUS_ERROR
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface Status {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String statusToString(@Status int value) {
+ switch (value) {
+ case STATUS_SUCCESS:
+ return "STATUS_SUCCESS";
+ case STATUS_ERROR:
+ return "STATUS_ERROR";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ @DataClass.Generated.Member
+ /* package-private */ TranslationResponseValue(
+ @Status int statusCode,
+ @Nullable CharSequence text,
+ @NonNull Bundle extras,
+ @Nullable CharSequence transliteration) {
+ this.mStatusCode = statusCode;
+
+ if (!(mStatusCode == STATUS_SUCCESS)
+ && !(mStatusCode == STATUS_ERROR)) {
+ throw new java.lang.IllegalArgumentException(
+ "statusCode was " + mStatusCode + " but must be one of: "
+ + "STATUS_SUCCESS(" + STATUS_SUCCESS + "), "
+ + "STATUS_ERROR(" + STATUS_ERROR + ")");
+ }
+
+ this.mText = text;
+ this.mExtras = extras;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mExtras);
+ this.mTransliteration = transliteration;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The status code of this {@link TranslationResponseValue}.
+ *
+ * <p>If the status code is {@link #STATUS_ERROR}, no values are attached, and all getters will
+ * return {@code null}.
+ */
+ @DataClass.Generated.Member
+ public @Status int getStatusCode() {
+ return mStatusCode;
+ }
+
+ /**
+ * The translated text result.
+ */
+ @DataClass.Generated.Member
+ public @Nullable CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Extra results associated with the translated text.
+ *
+ * <p>The bundle includes {@link #EXTRA_DEFINITIONS}, obtained by {@link Bundle#getBundle}.
+ * </p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * The transliteration result of the translated text.
+ * TODO: Describe the result structure.
+ */
+ @DataClass.Generated.Member
+ public @Nullable CharSequence getTransliteration() {
+ return mTransliteration;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationResponseValue { " +
+ "statusCode = " + statusToString(mStatusCode) + ", " +
+ "text = " + mText + ", " +
+ "extras = " + mExtras + ", " +
+ "transliteration = " + mTransliteration +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(TranslationResponseValue other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ TranslationResponseValue that = (TranslationResponseValue) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && mStatusCode == that.mStatusCode
+ && Objects.equals(mText, that.mText)
+ && extrasEquals(that.mExtras)
+ && Objects.equals(mTransliteration, that.mTransliteration);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + mStatusCode;
+ _hash = 31 * _hash + Objects.hashCode(mText);
+ _hash = 31 * _hash + Objects.hashCode(mExtras);
+ _hash = 31 * _hash + Objects.hashCode(mTransliteration);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mText != null) flg |= 0x2;
+ if (mTransliteration != null) flg |= 0x8;
+ dest.writeByte(flg);
+ dest.writeInt(mStatusCode);
+ if (mText != null) dest.writeCharSequence(mText);
+ dest.writeBundle(mExtras);
+ if (mTransliteration != null) dest.writeCharSequence(mTransliteration);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationResponseValue(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ int statusCode = in.readInt();
+ CharSequence text = (flg & 0x2) == 0 ? null : (CharSequence) in.readCharSequence();
+ Bundle extras = in.readBundle();
+ CharSequence transliteration = (flg & 0x8) == 0 ? null : (CharSequence) in.readCharSequence();
+
+ this.mStatusCode = statusCode;
+
+ if (!(mStatusCode == STATUS_SUCCESS)
+ && !(mStatusCode == STATUS_ERROR)) {
+ throw new java.lang.IllegalArgumentException(
+ "statusCode was " + mStatusCode + " but must be one of: "
+ + "STATUS_SUCCESS(" + STATUS_SUCCESS + "), "
+ + "STATUS_ERROR(" + STATUS_ERROR + ")");
+ }
+
+ this.mText = text;
+ this.mExtras = extras;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mExtras);
+ this.mTransliteration = transliteration;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationResponseValue> CREATOR
+ = new Parcelable.Creator<TranslationResponseValue>() {
+ @Override
+ public TranslationResponseValue[] newArray(int size) {
+ return new TranslationResponseValue[size];
+ }
+
+ @Override
+ public TranslationResponseValue createFromParcel(@NonNull Parcel in) {
+ return new TranslationResponseValue(in);
+ }
+ };
+
+ /**
+ * A builder for {@link TranslationResponseValue}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @Status int mStatusCode;
+ private @Nullable CharSequence mText;
+ private @NonNull Bundle mExtras;
+ private @Nullable CharSequence mTransliteration;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param statusCode
+ * The status code of this {@link TranslationResponseValue}.
+ *
+ * <p>If the status code is {@link #STATUS_ERROR}, no values are attached, and all getters will
+ * return {@code null}.
+ */
+ public Builder(
+ @Status int statusCode) {
+ mStatusCode = statusCode;
+
+ if (!(mStatusCode == STATUS_SUCCESS)
+ && !(mStatusCode == STATUS_ERROR)) {
+ throw new java.lang.IllegalArgumentException(
+ "statusCode was " + mStatusCode + " but must be one of: "
+ + "STATUS_SUCCESS(" + STATUS_SUCCESS + "), "
+ + "STATUS_ERROR(" + STATUS_ERROR + ")");
+ }
+
+ }
+
+ /**
+ * The translated text result.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setText(@NonNull CharSequence value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mText = value;
+ return this;
+ }
+
+ /**
+ * Extra results associated with the translated text.
+ *
+ * <p>The bundle includes {@link #EXTRA_DEFINITIONS}, obtained by {@link Bundle#getBundle}.
+ * </p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setExtras(@NonNull Bundle value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mExtras = value;
+ return this;
+ }
+
+ /**
+ * The transliteration result of the translated text.
+ * TODO: Describe the result structure.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setTransliteration(@NonNull CharSequence value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8;
+ mTransliteration = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull TranslationResponseValue build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x10; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mText = defaultText();
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mExtras = defaultExtras();
+ }
+ if ((mBuilderFieldsSet & 0x8) == 0) {
+ mTransliteration = defaultTransliteration();
+ }
+ TranslationResponseValue o = new TranslationResponseValue(
+ mStatusCode,
+ mText,
+ mExtras,
+ mTransliteration);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x10) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1622133051937L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationResponseValue.java",
+ inputSignatures = "public static final int STATUS_SUCCESS\npublic static final int STATUS_ERROR\npublic static final java.lang.String EXTRA_DEFINITIONS\nprivate final @android.view.translation.TranslationResponseValue.Status int mStatusCode\nprivate final @android.annotation.Nullable java.lang.CharSequence mText\nprivate final @android.annotation.NonNull android.os.Bundle mExtras\nprivate final @android.annotation.Nullable java.lang.CharSequence mTransliteration\npublic static @android.annotation.NonNull android.view.translation.TranslationResponseValue forError()\nprivate static java.lang.CharSequence defaultText()\nprivate static android.os.Bundle defaultExtras()\nprivate boolean extrasEquals(android.os.Bundle)\nprivate static java.lang.CharSequence defaultTransliteration()\nclass TranslationResponseValue extends java.lang.Object implements [android.os.Parcelable]\nclass BaseBuilder extends java.lang.Object implements []\[email protected](genBuilder=true, genToString=true, genEqualsHashCode=true, genHiddenConstDefs=true)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/TranslationSpec.java b/android/view/translation/TranslationSpec.java
new file mode 100644
index 0000000..efc3d8b
--- /dev/null
+++ b/android/view/translation/TranslationSpec.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.icu.util.ULocale;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Objects;
+
+/**
+ * Specs and additional info for the translation data.
+ *
+ * <p>This spec help specify information such as the language/locale for the translation, as well
+ * as the data format for the translation (text, audio, etc.)</p>
+ */
+@DataClass(genEqualsHashCode = true, genHiddenConstDefs = true, genToString = true,
+ genConstructor = false)
+public final class TranslationSpec implements Parcelable {
+
+ /** Data format for translation is text. */
+ public static final int DATA_FORMAT_TEXT = 1;
+
+ /** @hide */
+ @android.annotation.IntDef(prefix = "DATA_FORMAT_", value = {
+ DATA_FORMAT_TEXT
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface DataFormat {}
+
+ /**
+ * @removed use {@code mLocale} instead.
+ */
+ @Deprecated
+ private final @NonNull String mLanguage;
+
+ /**
+ * {@link ULocale} representing locale information of this spec.
+ */
+ private final @NonNull ULocale mLocale;
+
+ private final @DataFormat int mDataFormat;
+
+ void parcelLocale(Parcel dest, int flags) {
+ dest.writeSerializable(mLocale);
+ }
+
+ static ULocale unparcelLocale(Parcel in) {
+ return (ULocale) in.readSerializable();
+ }
+
+ /**
+ * @removed use {@link #TranslationSpec(ULocale, int)} instead.
+ */
+ @Deprecated
+ public TranslationSpec(@NonNull String language, @DataFormat int dataFormat) {
+ mLanguage = language;
+ mDataFormat = dataFormat;
+ mLocale = new ULocale.Builder().setLanguage(language).build();
+ }
+
+ /**
+ * Constructs a translation spec with the given locale and data format.
+ *
+ * @param locale locale of the associated translation data.
+ * @param dataFormat data format of the associated translation data.
+ */
+ public TranslationSpec(@NonNull ULocale locale, @DataFormat int dataFormat) {
+ Objects.requireNonNull(locale);
+ mLanguage = locale.getLanguage();
+ mLocale = locale;
+ mDataFormat = dataFormat;
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationSpec.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * @removed use {@code mLocale} instead.
+ */
+ @DataClass.Generated.Member
+ public @Deprecated @NonNull String getLanguage() {
+ return mLanguage;
+ }
+
+ /**
+ * {@link ULocale} representing locale information of this spec.
+ */
+ @DataClass.Generated.Member
+ public @NonNull ULocale getLocale() {
+ return mLocale;
+ }
+
+ @DataClass.Generated.Member
+ public @DataFormat int getDataFormat() {
+ return mDataFormat;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "TranslationSpec { " +
+ "language = " + mLanguage + ", " +
+ "locale = " + mLocale + ", " +
+ "dataFormat = " + mDataFormat +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@android.annotation.Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(TranslationSpec other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ TranslationSpec that = (TranslationSpec) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && Objects.equals(mLanguage, that.mLanguage)
+ && Objects.equals(mLocale, that.mLocale)
+ && mDataFormat == that.mDataFormat;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + Objects.hashCode(mLanguage);
+ _hash = 31 * _hash + Objects.hashCode(mLocale);
+ _hash = 31 * _hash + mDataFormat;
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeString(mLanguage);
+ parcelLocale(dest, flags);
+ dest.writeInt(mDataFormat);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ TranslationSpec(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ String language = in.readString();
+ ULocale locale = unparcelLocale(in);
+ int dataFormat = in.readInt();
+
+ this.mLanguage = language;
+ com.android.internal.util.AnnotationValidations.validate(
+ Deprecated.class, null, mLanguage);
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mLanguage);
+ this.mLocale = locale;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mLocale);
+ this.mDataFormat = dataFormat;
+ com.android.internal.util.AnnotationValidations.validate(
+ DataFormat.class, null, mDataFormat);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<TranslationSpec> CREATOR
+ = new Parcelable.Creator<TranslationSpec>() {
+ @Override
+ public TranslationSpec[] newArray(int size) {
+ return new TranslationSpec[size];
+ }
+
+ @Override
+ public TranslationSpec createFromParcel(@NonNull Parcel in) {
+ return new TranslationSpec(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1620089623334L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/TranslationSpec.java",
+ inputSignatures = "public static final int DATA_FORMAT_TEXT\nprivate final @java.lang.Deprecated @android.annotation.NonNull java.lang.String mLanguage\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.view.translation.TranslationSpec.DataFormat int mDataFormat\n void parcelLocale(android.os.Parcel,int)\nstatic android.icu.util.ULocale unparcelLocale(android.os.Parcel)\nclass TranslationSpec extends java.lang.Object implements [android.os.Parcelable]\[email protected](genEqualsHashCode=true, genHiddenConstDefs=true, genToString=true, genConstructor=false)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/Translator.java b/android/view/translation/Translator.java
new file mode 100644
index 0000000..606f39d
--- /dev/null
+++ b/android/view/translation/Translator.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
+import static android.view.translation.TranslationManager.SYNC_CALLS_TIMEOUT_MS;
+import static android.view.translation.UiTranslationController.DEBUG;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
+import android.service.translation.ITranslationCallback;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.IResultReceiver;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * The {@link Translator} for translation, defined by a {@link TranslationContext}.
+ */
+@SuppressLint("NotCloseable")
+public class Translator {
+
+ private static final String TAG = "Translator";
+
+ // TODO: make this configurable and cross the Translation component
+ private static boolean sDEBUG = false;
+
+ private final Object mLock = new Object();
+
+ private int mId;
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final TranslationContext mTranslationContext;
+
+ @NonNull
+ private final TranslationManager mManager;
+
+ @NonNull
+ private final Handler mHandler;
+
+ /**
+ * Interface to the system_server binder object.
+ */
+ private ITranslationManager mSystemServerBinder;
+
+ /**
+ * Direct interface to the TranslationService binder object.
+ */
+ @Nullable
+ private ITranslationDirectManager mDirectServiceBinder;
+
+ @NonNull
+ private final ServiceBinderReceiver mServiceBinderReceiver;
+
+ @GuardedBy("mLock")
+ private boolean mDestroyed;
+
+ /**
+ * Name of the {@link IResultReceiver} extra used to pass the binder interface to Translator.
+ * @hide
+ */
+ public static final String EXTRA_SERVICE_BINDER = "binder";
+ /**
+ * Name of the extra used to pass the session id to Translator.
+ * @hide
+ */
+ public static final String EXTRA_SESSION_ID = "sessionId";
+
+ static class ServiceBinderReceiver extends IResultReceiver.Stub {
+ // TODO: refactor how translator is instantiated after removing deprecated createTranslator.
+ private final Translator mTranslator;
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private int mSessionId;
+
+ private Consumer<Translator> mCallback;
+
+ ServiceBinderReceiver(Translator translator, Consumer<Translator> callback) {
+ mTranslator = translator;
+ mCallback = callback;
+ }
+
+ ServiceBinderReceiver(Translator translator) {
+ mTranslator = translator;
+ }
+
+ int getSessionStateResult() throws TimeoutException {
+ try {
+ if (!mLatch.await(SYNC_CALLS_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ throw new TimeoutException(
+ "Session not created in " + SYNC_CALLS_TIMEOUT_MS + "ms");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new TimeoutException("Session not created because interrupted");
+ }
+ return mSessionId;
+ }
+
+ @Override
+ public void send(int resultCode, Bundle resultData) {
+ if (resultCode == STATUS_SYNC_CALL_FAIL) {
+ mLatch.countDown();
+ if (mCallback != null) {
+ mCallback.accept(null);
+ }
+ return;
+ }
+ final IBinder binder;
+ if (resultData != null) {
+ mSessionId = resultData.getInt(EXTRA_SESSION_ID);
+ binder = resultData.getBinder(EXTRA_SERVICE_BINDER);
+ if (binder == null) {
+ Log.wtf(TAG, "No " + EXTRA_SERVICE_BINDER + " extra result");
+ return;
+ }
+ } else {
+ binder = null;
+ }
+ mTranslator.setServiceBinder(binder);
+ mLatch.countDown();
+ if (mCallback != null) {
+ mCallback.accept(mTranslator);
+ }
+ }
+
+ // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor public
+ // and use it.
+ static final class TimeoutException extends Exception {
+ private TimeoutException(String msg) {
+ super(msg);
+ }
+ }
+ }
+
+ /**
+ * Create the Translator.
+ *
+ * @hide
+ */
+ public Translator(@NonNull Context context,
+ @NonNull TranslationContext translationContext, int sessionId,
+ @NonNull TranslationManager translationManager, @NonNull Handler handler,
+ @Nullable ITranslationManager systemServerBinder,
+ @NonNull Consumer<Translator> callback) {
+ mContext = context;
+ mTranslationContext = translationContext;
+ mId = sessionId;
+ mManager = translationManager;
+ mHandler = handler;
+ mSystemServerBinder = systemServerBinder;
+ mServiceBinderReceiver = new ServiceBinderReceiver(this, callback);
+
+ try {
+ mSystemServerBinder.onSessionCreated(mTranslationContext, mId,
+ mServiceBinderReceiver, mContext.getUserId());
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling startSession(): " + e);
+ }
+ }
+
+ /**
+ * Create the Translator.
+ *
+ * @hide
+ */
+ public Translator(@NonNull Context context,
+ @NonNull TranslationContext translationContext, int sessionId,
+ @NonNull TranslationManager translationManager, @NonNull Handler handler,
+ @Nullable ITranslationManager systemServerBinder) {
+ mContext = context;
+ mTranslationContext = translationContext;
+ mId = sessionId;
+ mManager = translationManager;
+ mHandler = handler;
+ mSystemServerBinder = systemServerBinder;
+ mServiceBinderReceiver = new ServiceBinderReceiver(this);
+ }
+
+ /**
+ * Starts this Translator session.
+ */
+ void start() {
+ try {
+ mSystemServerBinder.onSessionCreated(mTranslationContext, mId,
+ mServiceBinderReceiver, mContext.getUserId());
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling startSession(): " + e);
+ }
+ }
+
+ /**
+ * Wait this Translator session created.
+ *
+ * @return {@code true} if the session is created successfully.
+ */
+ boolean isSessionCreated() throws ServiceBinderReceiver.TimeoutException {
+ int receivedId = mServiceBinderReceiver.getSessionStateResult();
+ return receivedId > 0;
+ }
+
+ private int getNextRequestId() {
+ // Get from manager to keep the request id unique to different Translators
+ return mManager.getAvailableRequestId().getAndIncrement();
+ }
+
+ private void setServiceBinder(@Nullable IBinder binder) {
+ synchronized (mLock) {
+ if (mDirectServiceBinder != null) {
+ return;
+ }
+ if (binder != null) {
+ mDirectServiceBinder = ITranslationDirectManager.Stub.asInterface(binder);
+ }
+ }
+ }
+
+ /** @hide */
+ public TranslationContext getTranslationContext() {
+ return mTranslationContext;
+ }
+
+ /** @hide */
+ public int getTranslatorId() {
+ return mId;
+ }
+
+ /** @hide */
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("translationContext: "); pw.println(mTranslationContext);
+ }
+
+ /**
+ * Requests a translation for the provided {@link TranslationRequest} using the Translator's
+ * source spec and destination spec.
+ *
+ * @param request {@link TranslationRequest} request to be translate.
+ *
+ * @throws IllegalStateException if this Translator session was destroyed when called.
+ *
+ * @removed use {@link #translate(TranslationRequest, CancellationSignal,
+ * Executor, Consumer)} instead.
+ */
+ @Deprecated
+ @Nullable
+ public void translate(@NonNull TranslationRequest request,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<TranslationResponse> callback) {
+ Objects.requireNonNull(request, "Translation request cannot be null");
+ Objects.requireNonNull(executor, "Executor cannot be null");
+ Objects.requireNonNull(callback, "Callback cannot be null");
+
+ if (isDestroyed()) {
+ // TODO(b/176464808): Disallow multiple Translator now, it will throw
+ // IllegalStateException. Need to discuss if we can allow multiple Translators.
+ throw new IllegalStateException(
+ "This translator has been destroyed");
+ }
+
+ final ITranslationCallback responseCallback =
+ new TranslationResponseCallbackImpl(callback, executor);
+ try {
+ mDirectServiceBinder.onTranslationRequest(request, mId,
+ CancellationSignal.createTransport(), responseCallback);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling requestTranslate(): " + e);
+ }
+ }
+
+ /**
+ * Requests a translation for the provided {@link TranslationRequest} using the Translator's
+ * source spec and destination spec.
+ *
+ * @param request {@link TranslationRequest} request to be translate.
+ * @param cancellationSignal signal to cancel the operation in progress.
+ * @param executor Executor to run callback operations
+ * @param callback {@link Consumer} to receive the translation response. Multiple responses may
+ * be received if {@link TranslationRequest#FLAG_PARTIAL_RESPONSES} is set.
+ *
+ * @throws IllegalStateException if this Translator session was destroyed when called.
+ */
+ @Nullable
+ public void translate(@NonNull TranslationRequest request,
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<TranslationResponse> callback) {
+ Objects.requireNonNull(request, "Translation request cannot be null");
+ Objects.requireNonNull(executor, "Executor cannot be null");
+ Objects.requireNonNull(callback, "Callback cannot be null");
+
+ if (isDestroyed()) {
+ // TODO(b/176464808): Disallow multiple Translator now, it will throw
+ // IllegalStateException. Need to discuss if we can allow multiple Translators.
+ throw new IllegalStateException(
+ "This translator has been destroyed");
+ }
+
+ ICancellationSignal transport = null;
+ if (cancellationSignal != null) {
+ transport = CancellationSignal.createTransport();
+ cancellationSignal.setRemote(transport);
+ }
+ final ITranslationCallback responseCallback =
+ new TranslationResponseCallbackImpl(callback, executor);
+
+ try {
+ mDirectServiceBinder.onTranslationRequest(request, mId, transport,
+ responseCallback);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling requestTranslate(): " + e);
+ }
+ }
+
+ /**
+ * Destroy this Translator.
+ */
+ public void destroy() {
+ synchronized (mLock) {
+ if (mDestroyed) {
+ return;
+ }
+ mDestroyed = true;
+ try {
+ mDirectServiceBinder.onFinishTranslationSession(mId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onSessionFinished");
+ }
+ mDirectServiceBinder = null;
+ mManager.removeTranslator(mId);
+ }
+ }
+
+ /**
+ * Returns whether or not this Translator has been destroyed.
+ *
+ * @see #destroy()
+ */
+ public boolean isDestroyed() {
+ synchronized (mLock) {
+ return mDestroyed;
+ }
+ }
+
+ // TODO: add methods for UI-toolkit case.
+ /** @hide */
+ public void requestUiTranslate(@NonNull TranslationRequest request,
+ @NonNull Executor executor,
+ @NonNull Consumer<TranslationResponse> callback) {
+ if (mDirectServiceBinder == null) {
+ Log.wtf(TAG, "Translator created without proper initialization.");
+ return;
+ }
+ final ITranslationCallback translationCallback =
+ new TranslationResponseCallbackImpl(callback, executor);
+ try {
+ mDirectServiceBinder.onTranslationRequest(request, mId,
+ CancellationSignal.createTransport(), translationCallback);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling flushRequest");
+ }
+ }
+
+ private static class TranslationResponseCallbackImpl extends ITranslationCallback.Stub {
+
+ private final Consumer<TranslationResponse> mCallback;
+ private final Executor mExecutor;
+
+ TranslationResponseCallbackImpl(Consumer<TranslationResponse> callback, Executor executor) {
+ mCallback = callback;
+ mExecutor = executor;
+ }
+
+ @Override
+ public void onTranslationResponse(TranslationResponse response) throws RemoteException {
+ if (DEBUG) {
+ Log.i(TAG, "onTranslationResponse called.");
+ }
+ final Runnable runnable =
+ () -> mCallback.accept(response);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(runnable);
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+ }
+}
diff --git a/android/view/translation/UiTranslationController.java b/android/view/translation/UiTranslationController.java
new file mode 100644
index 0000000..a833591
--- /dev/null
+++ b/android/view/translation/UiTranslationController.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_FINISHED;
+import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_PAUSED;
+import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_RESUMED;
+import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_STARTED;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.app.assist.ActivityId;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowManagerGlobal;
+import android.view.autofill.AutofillId;
+import android.view.translation.UiTranslationManager.UiTranslationState;
+import android.widget.TextView;
+import android.widget.TextViewTranslationCallback;
+
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+/**
+ * A controller to manage the ui translation requests for the {@link Activity}.
+ *
+ * @hide
+ */
+public class UiTranslationController {
+
+ public static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
+
+ private static final String TAG = "UiTranslationController";
+ @NonNull
+ private final Activity mActivity;
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final Object mLock = new Object();
+
+ // Each Translator is distinguished by sourceSpec and desSepc.
+ @NonNull
+ private final ArrayMap<Pair<TranslationSpec, TranslationSpec>, Translator> mTranslators;
+ @NonNull
+ private final ArrayMap<AutofillId, WeakReference<View>> mViews;
+ /**
+ * Views for which {@link UiTranslationSpec#shouldPadContentForCompat()} is true.
+ */
+ @NonNull
+ private final ArraySet<AutofillId> mViewsToPadContent;
+ @NonNull
+ private final HandlerThread mWorkerThread;
+ @NonNull
+ private final Handler mWorkerHandler;
+ private int mCurrentState;
+ @NonNull
+ private ArraySet<AutofillId> mLastRequestAutofillIds;
+
+ public UiTranslationController(Activity activity, Context context) {
+ mActivity = activity;
+ mContext = context;
+ mViews = new ArrayMap<>();
+ mTranslators = new ArrayMap<>();
+ mViewsToPadContent = new ArraySet<>();
+
+ mWorkerThread =
+ new HandlerThread("UiTranslationController_" + mActivity.getComponentName(),
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mWorkerThread.start();
+ mWorkerHandler = mWorkerThread.getThreadHandler();
+ }
+
+ /**
+ * Update the Ui translation state.
+ */
+ public void updateUiTranslationState(@UiTranslationState int state, TranslationSpec sourceSpec,
+ TranslationSpec targetSpec, List<AutofillId> views,
+ UiTranslationSpec uiTranslationSpec) {
+ if (!mActivity.isResumed() && (state == STATE_UI_TRANSLATION_STARTED
+ || state == STATE_UI_TRANSLATION_RESUMED)) {
+ return;
+ }
+
+ Log.i(TAG, "updateUiTranslationState state: " + stateToString(state)
+ + (DEBUG ? (", views: " + views + ", spec: " + uiTranslationSpec) : ""));
+ synchronized (mLock) {
+ mCurrentState = state;
+ if (views != null) {
+ setLastRequestAutofillIdsLocked(views);
+ }
+ }
+ switch (state) {
+ case STATE_UI_TRANSLATION_STARTED:
+ if (uiTranslationSpec != null && uiTranslationSpec.shouldPadContentForCompat()) {
+ synchronized (mLock) {
+ mViewsToPadContent.addAll(views);
+ // TODO: Cleanup disappeared views from mViews and mViewsToPadContent at
+ // some appropriate place.
+ }
+ }
+ final Pair<TranslationSpec, TranslationSpec> specs =
+ new Pair<>(sourceSpec, targetSpec);
+ if (!mTranslators.containsKey(specs)) {
+ mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
+ UiTranslationController::createTranslatorAndStart,
+ UiTranslationController.this, sourceSpec, targetSpec, views));
+ } else {
+ onUiTranslationStarted(mTranslators.get(specs), views);
+ }
+ break;
+ case STATE_UI_TRANSLATION_PAUSED:
+ runForEachView((view, callback) -> callback.onHideTranslation(view));
+ break;
+ case STATE_UI_TRANSLATION_RESUMED:
+ runForEachView((view, callback) -> callback.onShowTranslation(view));
+ break;
+ case STATE_UI_TRANSLATION_FINISHED:
+ destroyTranslators();
+ runForEachView((view, callback) -> {
+ callback.onClearTranslation(view);
+ view.clearViewTranslationResponse();
+ if (view.hasTranslationTransientState()) {
+ view.setHasTransientState(false);
+ view.setHasTranslationTransientState(false);
+ }
+ });
+ notifyTranslationFinished(/* activityDestroyed= */ false);
+ synchronized (mLock) {
+ mViews.clear();
+ }
+ break;
+ default:
+ Log.w(TAG, "onAutoTranslationStateChange(): unknown state: " + state);
+ }
+ }
+
+ /**
+ * Called when the Activity is destroyed.
+ */
+ public void onActivityDestroyed() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.i(TAG,
+ "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
+ }
+ if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) {
+ notifyTranslationFinished(/* activityDestroyed= */ true);
+ }
+ mViews.clear();
+ destroyTranslators();
+ mWorkerThread.quitSafely();
+ }
+ }
+
+ private void notifyTranslationFinished(boolean activityDestroyed) {
+ UiTranslationManager manager = mContext.getSystemService(UiTranslationManager.class);
+ if (manager != null) {
+ manager.onTranslationFinished(activityDestroyed,
+ new ActivityId(mActivity.getTaskId(), mActivity.getShareableActivityToken()),
+ mActivity.getComponentName());
+ }
+ }
+
+ private void setLastRequestAutofillIdsLocked(List<AutofillId> views) {
+ if (mLastRequestAutofillIds == null) {
+ mLastRequestAutofillIds = new ArraySet<>();
+ }
+ if (mLastRequestAutofillIds.size() > 0) {
+ mLastRequestAutofillIds.clear();
+ }
+ mLastRequestAutofillIds.addAll(views);
+ }
+
+ /**
+ * Called to dump the translation information for Activity.
+ */
+ public void dump(String outerPrefix, PrintWriter pw) {
+ pw.print(outerPrefix); pw.println("UiTranslationController:");
+ final String pfx = outerPrefix + " ";
+ pw.print(pfx); pw.print("activity: "); pw.print(mActivity);
+ pw.print(pfx); pw.print("resumed: "); pw.println(mActivity.isResumed());
+ pw.print(pfx); pw.print("current state: "); pw.println(mCurrentState);
+ final int translatorSize = mTranslators.size();
+ pw.print(outerPrefix); pw.print("number translator: "); pw.println(translatorSize);
+ for (int i = 0; i < translatorSize; i++) {
+ pw.print(outerPrefix); pw.print("#"); pw.println(i);
+ final Translator translator = mTranslators.valueAt(i);
+ translator.dump(outerPrefix, pw);
+ pw.println();
+ }
+ synchronized (mLock) {
+ final int viewSize = mViews.size();
+ pw.print(outerPrefix); pw.print("number views: "); pw.println(viewSize);
+ for (int i = 0; i < viewSize; i++) {
+ pw.print(outerPrefix); pw.print("#"); pw.println(i);
+ final AutofillId autofillId = mViews.keyAt(i);
+ final View view = mViews.valueAt(i).get();
+ pw.print(pfx); pw.print("autofillId: "); pw.println(autofillId);
+ pw.print(pfx); pw.print("view:"); pw.println(view);
+ }
+ pw.print(outerPrefix); pw.print("padded views: "); pw.println(mViewsToPadContent);
+ }
+ if (DEBUG) {
+ dumpViewByTraversal(outerPrefix, pw);
+ }
+ }
+
+ private void dumpViewByTraversal(String outerPrefix, PrintWriter pw) {
+ final ArrayList<ViewRootImpl> roots =
+ WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
+ pw.print(outerPrefix); pw.println("Dump views:");
+ for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+ final View rootView = roots.get(rootNum).getView();
+ if (rootView instanceof ViewGroup) {
+ dumpChildren((ViewGroup) rootView, outerPrefix, pw);
+ } else {
+ dumpViewInfo(rootView, outerPrefix, pw);
+ }
+ }
+ }
+
+ private void dumpChildren(ViewGroup viewGroup, String outerPrefix, PrintWriter pw) {
+ final int childCount = viewGroup.getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ final View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ pw.print(outerPrefix); pw.println("Children: ");
+ pw.print(outerPrefix); pw.print(outerPrefix); pw.println(child);
+ dumpChildren((ViewGroup) child, outerPrefix, pw);
+ } else {
+ pw.print(outerPrefix); pw.println("End Children: ");
+ pw.print(outerPrefix); pw.print(outerPrefix); pw.print(child);
+ dumpViewInfo(child, outerPrefix, pw);
+ }
+ }
+ }
+
+ private void dumpViewInfo(View view, String outerPrefix, PrintWriter pw) {
+ final AutofillId autofillId = view.getAutofillId();
+ pw.print(outerPrefix); pw.print("autofillId: "); pw.print(autofillId);
+ // TODO: print TranslationTransformation
+ boolean isContainsView = false;
+ boolean isRequestedView = false;
+ synchronized (mLock) {
+ if (mLastRequestAutofillIds.contains(autofillId)) {
+ isRequestedView = true;
+ }
+ final WeakReference<View> viewRef = mViews.get(autofillId);
+ if (viewRef != null && viewRef.get() != null) {
+ isContainsView = true;
+ }
+ }
+ pw.print(outerPrefix); pw.print("isContainsView: "); pw.print(isContainsView);
+ pw.print(outerPrefix); pw.print("isRequestedView: "); pw.println(isRequestedView);
+ }
+
+ /**
+ * The method is used by {@link Translator}, it will be called when the translation is done. The
+ * translation result can be get from here.
+ */
+ public void onTranslationCompleted(TranslationResponse response) {
+ if (response == null || response.getTranslationStatus()
+ != TranslationResponse.TRANSLATION_STATUS_SUCCESS) {
+ Log.w(TAG, "Fail result from TranslationService, status=" + (response == null
+ ? "null"
+ : response.getTranslationStatus()));
+ return;
+ }
+ final SparseArray<ViewTranslationResponse> translatedResult =
+ response.getViewTranslationResponses();
+ final SparseArray<ViewTranslationResponse> viewsResult = new SparseArray<>();
+ final SparseArray<LongSparseArray<ViewTranslationResponse>> virtualViewsResult =
+ new SparseArray<>();
+ final IntArray viewIds = new IntArray(1);
+ for (int i = 0; i < translatedResult.size(); i++) {
+ final ViewTranslationResponse result = translatedResult.valueAt(i);
+ final AutofillId autofillId = result.getAutofillId();
+ if (viewIds.indexOf(autofillId.getViewId()) < 0) {
+ viewIds.add(autofillId.getViewId());
+ }
+ if (autofillId.isNonVirtual()) {
+ viewsResult.put(translatedResult.keyAt(i), result);
+ } else {
+ final boolean isVirtualViewAdded =
+ virtualViewsResult.indexOfKey(autofillId.getViewId()) >= 0;
+ final LongSparseArray<ViewTranslationResponse> childIds =
+ isVirtualViewAdded ? virtualViewsResult.get(autofillId.getViewId())
+ : new LongSparseArray<>();
+ childIds.put(autofillId.getVirtualChildLongId(), result);
+ if (!isVirtualViewAdded) {
+ virtualViewsResult.put(autofillId.getViewId(), childIds);
+ }
+ }
+ }
+ // Traverse tree and get views by the responsed AutofillId
+ findViewsTraversalByAutofillIds(viewIds);
+
+ if (viewsResult.size() > 0) {
+ onTranslationCompleted(viewsResult);
+ }
+ if (virtualViewsResult.size() > 0) {
+ onVirtualViewTranslationCompleted(virtualViewsResult);
+ }
+ }
+
+ /**
+ * The method is used to handle the translation result for the vertual views.
+ */
+ private void onVirtualViewTranslationCompleted(
+ SparseArray<LongSparseArray<ViewTranslationResponse>> translatedResult) {
+ if (!mActivity.isResumed()) {
+ if (DEBUG) {
+ Log.v(TAG, "onTranslationCompleted: Activity is not resumed.");
+ }
+ return;
+ }
+ synchronized (mLock) {
+ if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
+ Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
+ + "Skip to show the translated text.");
+ return;
+ }
+ for (int i = 0; i < translatedResult.size(); i++) {
+ final AutofillId autofillId = new AutofillId(translatedResult.keyAt(i));
+ final View view = mViews.get(autofillId).get();
+ if (view == null) {
+ Log.w(TAG, "onTranslationCompleted: the view for autofill id " + autofillId
+ + " may be gone.");
+ continue;
+ }
+ final LongSparseArray<ViewTranslationResponse> virtualChildResponse =
+ translatedResult.valueAt(i);
+ if (DEBUG) {
+ Log.v(TAG, "onVirtualViewTranslationCompleted: received response for "
+ + "AutofillId " + autofillId);
+ }
+ mActivity.runOnUiThread(() -> {
+ if (view.getViewTranslationCallback() == null) {
+ if (DEBUG) {
+ Log.d(TAG, view + " doesn't support showing translation because of "
+ + "null ViewTranslationCallback.");
+ }
+ return;
+ }
+ view.onVirtualViewTranslationResponses(virtualChildResponse);
+ if (view.getViewTranslationCallback() != null) {
+ view.getViewTranslationCallback().onShowTranslation(view);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * The method is used to handle the translation result for non-vertual views.
+ */
+ private void onTranslationCompleted(SparseArray<ViewTranslationResponse> translatedResult) {
+ if (!mActivity.isResumed()) {
+ if (DEBUG) {
+ Log.v(TAG, "onTranslationCompleted: Activity is not resumed.");
+ }
+ return;
+ }
+ final int resultCount = translatedResult.size();
+ if (DEBUG) {
+ Log.v(TAG, "onTranslationCompleted: receive " + resultCount + " responses.");
+ }
+ synchronized (mLock) {
+ if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
+ Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
+ + "Skip to show the translated text.");
+ return;
+ }
+ for (int i = 0; i < resultCount; i++) {
+ final ViewTranslationResponse response = translatedResult.valueAt(i);
+ if (DEBUG) {
+ Log.v(TAG, "onTranslationCompleted: "
+ + sanitizedViewTranslationResponse(response));
+ }
+ final AutofillId autofillId = response.getAutofillId();
+ if (autofillId == null) {
+ Log.w(TAG, "No AutofillId is set in ViewTranslationResponse");
+ continue;
+ }
+ final View view = mViews.get(autofillId).get();
+ if (view == null) {
+ Log.w(TAG, "onTranslationCompleted: the view for autofill id " + autofillId
+ + " may be gone.");
+ continue;
+ }
+ mActivity.runOnUiThread(() -> {
+ if (view.getViewTranslationResponse() != null
+ && view.getViewTranslationResponse().equals(response)) {
+ if (DEBUG) {
+ Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
+ + ". Ignoring.");
+ }
+ return;
+ }
+ ViewTranslationCallback callback = view.getViewTranslationCallback();
+ if (callback == null) {
+ if (view instanceof TextView) {
+ // developer doesn't provide their override, we set the default TextView
+ // implementation.
+ callback = new TextViewTranslationCallback();
+ view.setViewTranslationCallback(callback);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, view + " doesn't support showing translation because of "
+ + "null ViewTranslationCallback.");
+ }
+ return;
+ }
+ }
+ callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS);
+ if (mViewsToPadContent.contains(autofillId)) {
+ callback.enableContentPadding();
+ }
+ view.onViewTranslationResponse(response);
+ callback.onShowTranslation(view);
+ });
+ }
+ }
+ }
+
+ // TODO: Use a device config value.
+ private static final int ANIMATION_DURATION_MILLIS = 250;
+
+ /**
+ * Creates a Translator for the given source and target translation specs and start the ui
+ * translation when the Translator is created successfully.
+ */
+ @WorkerThread
+ private void createTranslatorAndStart(TranslationSpec sourceSpec, TranslationSpec targetSpec,
+ List<AutofillId> views) {
+ // Create Translator
+ final Translator translator = createTranslatorIfNeeded(sourceSpec, targetSpec);
+ if (translator == null) {
+ Log.w(TAG, "Can not create Translator for sourceSpec:" + sourceSpec + " targetSpec:"
+ + targetSpec);
+ return;
+ }
+ onUiTranslationStarted(translator, views);
+ }
+
+ @WorkerThread
+ private void sendTranslationRequest(Translator translator,
+ List<ViewTranslationRequest> requests) {
+ if (requests.size() == 0) {
+ Log.w(TAG, "No ViewTranslationRequest was collected.");
+ return;
+ }
+ final TranslationRequest request = new TranslationRequest.Builder()
+ .setViewTranslationRequests(requests)
+ .build();
+ if (DEBUG) {
+ StringBuilder msg = new StringBuilder("sendTranslationRequest:{requests=[");
+ for (ViewTranslationRequest viewRequest: requests) {
+ msg.append("{request=")
+ .append(sanitizedViewTranslationRequest(viewRequest))
+ .append("}, ");
+ }
+ Log.d(TAG, "sendTranslationRequest: " + msg.toString());
+ }
+ translator.requestUiTranslate(request, (r) -> r.run(), this::onTranslationCompleted);
+ }
+
+ /**
+ * Called when there is an ui translation request comes to request view translation.
+ */
+ private void onUiTranslationStarted(Translator translator, List<AutofillId> views) {
+ synchronized (mLock) {
+ // Filter the request views' AutofillId
+ SparseIntArray virtualViewChildCount = getRequestVirtualViewChildCount(views);
+ Map<AutofillId, long[]> viewIds = new ArrayMap<>();
+ Map<AutofillId, Integer> unusedIndices = null;
+ for (int i = 0; i < views.size(); i++) {
+ AutofillId autofillId = views.get(i);
+ if (autofillId.isNonVirtual()) {
+ viewIds.put(autofillId, null);
+ } else {
+ if (unusedIndices == null) {
+ unusedIndices = new ArrayMap<>();
+ }
+ // The virtual id get from content capture is long, see getVirtualChildLongId()
+ // e.g. 1001, 1001:2, 1002:1 -> 1001, <1,2>; 1002, <1>
+ AutofillId virtualViewAutofillId = new AutofillId(autofillId.getViewId());
+ long[] childs;
+ int end = 0;
+ if (viewIds.containsKey(virtualViewAutofillId)) {
+ childs = viewIds.get(virtualViewAutofillId);
+ end = unusedIndices.get(virtualViewAutofillId);
+ } else {
+ int childCount = virtualViewChildCount.get(autofillId.getViewId());
+ childs = new long[childCount];
+ viewIds.put(virtualViewAutofillId, childs);
+ }
+ unusedIndices.put(virtualViewAutofillId, end + 1);
+ childs[end] = autofillId.getVirtualChildLongId();
+ }
+ }
+ ArrayList<ViewTranslationRequest> requests = new ArrayList<>();
+ int[] supportedFormats = getSupportedFormatsLocked();
+ ArrayList<ViewRootImpl> roots =
+ WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
+ TranslationCapability capability =
+ getTranslationCapability(translator.getTranslationContext());
+ mActivity.runOnUiThread(() -> {
+ // traverse the hierarchy to collect ViewTranslationRequests
+ for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+ View rootView = roots.get(rootNum).getView();
+ rootView.dispatchCreateViewTranslationRequest(viewIds, supportedFormats,
+ capability, requests);
+ }
+ mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
+ UiTranslationController::sendTranslationRequest,
+ UiTranslationController.this, translator, requests));
+ });
+ }
+ }
+
+ private SparseIntArray getRequestVirtualViewChildCount(List<AutofillId> views) {
+ SparseIntArray virtualViewCount = new SparseIntArray();
+ for (int i = 0; i < views.size(); i++) {
+ AutofillId autofillId = views.get(i);
+ if (!autofillId.isNonVirtual()) {
+ int virtualViewId = autofillId.getViewId();
+ if (virtualViewCount.indexOfKey(virtualViewId) < 0) {
+ virtualViewCount.put(virtualViewId, 1);
+ } else {
+ virtualViewCount.put(virtualViewId, (virtualViewCount.get(virtualViewId) + 1));
+ }
+ }
+ }
+ return virtualViewCount;
+ }
+
+ private int[] getSupportedFormatsLocked() {
+ // We only support text now
+ return new int[] {TranslationSpec.DATA_FORMAT_TEXT};
+ }
+
+ private TranslationCapability getTranslationCapability(TranslationContext translationContext) {
+ // We only support text to text capability now, we will query real status from service when
+ // we support more translation capabilities.
+ return new TranslationCapability(TranslationCapability.STATE_ON_DEVICE,
+ translationContext.getSourceSpec(),
+ translationContext.getTargetSpec(), /* uiTranslationEnabled= */ true,
+ /* supportedTranslationFlags= */ 0);
+ }
+
+ private void findViewsTraversalByAutofillIds(IntArray sourceViewIds) {
+ final ArrayList<ViewRootImpl> roots =
+ WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
+ for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+ final View rootView = roots.get(rootNum).getView();
+ if (rootView instanceof ViewGroup) {
+ findViewsTraversalByAutofillIds((ViewGroup) rootView, sourceViewIds);
+ } else {
+ addViewIfNeeded(sourceViewIds, rootView);
+ }
+ }
+ }
+
+ private void findViewsTraversalByAutofillIds(ViewGroup viewGroup,
+ IntArray sourceViewIds) {
+ final int childCount = viewGroup.getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ final View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ findViewsTraversalByAutofillIds((ViewGroup) child, sourceViewIds);
+ } else {
+ addViewIfNeeded(sourceViewIds, child);
+ }
+ }
+ }
+
+ private void addViewIfNeeded(IntArray sourceViewIds, View view) {
+ final AutofillId autofillId = view.getAutofillId();
+ if ((sourceViewIds.indexOf(autofillId.getViewId()) >= 0)
+ && !mViews.containsKey(autofillId)) {
+ mViews.put(autofillId, new WeakReference<>(view));
+ }
+ }
+
+ private void runForEachView(BiConsumer<View, ViewTranslationCallback> action) {
+ synchronized (mLock) {
+ final ArrayMap<AutofillId, WeakReference<View>> views = new ArrayMap<>(mViews);
+ if (views.size() == 0) {
+ Log.w(TAG, "No views can be excuted for runForEachView.");
+ }
+ mActivity.runOnUiThread(() -> {
+ final int viewCounts = views.size();
+ for (int i = 0; i < viewCounts; i++) {
+ final View view = views.valueAt(i).get();
+ if (DEBUG) {
+ Log.d(TAG, "runForEachView for autofillId = " + (view != null
+ ? view.getAutofillId() : " null"));
+ }
+ if (view == null || view.getViewTranslationCallback() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "View was gone or ViewTranslationCallback for autofillId "
+ + "= " + views.keyAt(i));
+ }
+ continue;
+ }
+ action.accept(view, view.getViewTranslationCallback());
+ }
+ });
+ }
+ }
+
+ private Translator createTranslatorIfNeeded(
+ TranslationSpec sourceSpec, TranslationSpec targetSpec) {
+ final TranslationManager tm = mContext.getSystemService(TranslationManager.class);
+ if (tm == null) {
+ Log.e(TAG, "Can not find TranslationManager when trying to create translator.");
+ return null;
+ }
+ final TranslationContext translationContext = new TranslationContext(sourceSpec,
+ targetSpec, /* translationFlags= */ 0);
+ final Translator translator = tm.createTranslator(translationContext);
+ if (translator != null) {
+ final Pair<TranslationSpec, TranslationSpec> specs = new Pair<>(sourceSpec, targetSpec);
+ mTranslators.put(specs, translator);
+ }
+ return translator;
+ }
+
+ private void destroyTranslators() {
+ synchronized (mLock) {
+ final int count = mTranslators.size();
+ for (int i = 0; i < count; i++) {
+ Translator translator = mTranslators.valueAt(i);
+ translator.destroy();
+ }
+ mTranslators.clear();
+ }
+ }
+
+ /**
+ * Returns a string representation of the state.
+ */
+ public static String stateToString(@UiTranslationState int state) {
+ switch (state) {
+ case STATE_UI_TRANSLATION_STARTED:
+ return "UI_TRANSLATION_STARTED";
+ case STATE_UI_TRANSLATION_PAUSED:
+ return "UI_TRANSLATION_PAUSED";
+ case STATE_UI_TRANSLATION_RESUMED:
+ return "UI_TRANSLATION_RESUMED";
+ case STATE_UI_TRANSLATION_FINISHED:
+ return "UI_TRANSLATION_FINISHED";
+ default:
+ return "Unknown state (" + state + ")";
+ }
+ }
+
+ /**
+ * Returns a sanitized string representation of {@link ViewTranslationRequest};
+ */
+ private static String sanitizedViewTranslationRequest(@NonNull ViewTranslationRequest request) {
+ StringBuilder msg = new StringBuilder("ViewTranslationRequest:{values=[");
+ for (String key: request.getKeys()) {
+ final TranslationRequestValue value = request.getValue(key);
+ msg.append("{text=").append(value.getText() == null
+ ? "null"
+ : "string[" + value.getText().length() + "]}, ");
+ }
+ return msg.toString();
+ }
+
+ /**
+ * Returns a sanitized string representation of {@link ViewTranslationResponse};
+ */
+ private static String sanitizedViewTranslationResponse(
+ @NonNull ViewTranslationResponse response) {
+ StringBuilder msg = new StringBuilder("ViewTranslationResponse:{values=[");
+ for (String key: response.getKeys()) {
+ final TranslationResponseValue value = response.getValue(key);
+ msg.append("{status=").append(value.getStatusCode()).append(", ");
+ msg.append("text=").append(value.getText() == null
+ ? "null"
+ : "string[" + value.getText().length() + "], ");
+ //TODO: append dictionary results.
+ msg.append("transliteration=").append(value.getTransliteration() == null
+ ? "null"
+ : "string[" + value.getTransliteration().length() + "]}, ");
+ }
+ return msg.toString();
+ }
+}
diff --git a/android/view/translation/UiTranslationManager.java b/android/view/translation/UiTranslationManager.java
new file mode 100644
index 0000000..b9ed32c
--- /dev/null
+++ b/android/view/translation/UiTranslationManager.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.assist.ActivityId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.icu.util.ULocale;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+// TODO(b/178044703): Describe what UI Translation is.
+/**
+ * The {@link UiTranslationManager} class provides ways for apps to use the ui translation
+ * function in framework.
+ */
+public final class UiTranslationManager {
+
+ private static final String TAG = "UiTranslationManager";
+
+ /**
+ * The tag which uses for enabling debug log dump. To enable it, we can use command "adb shell
+ * setprop log.tag.UiTranslation DEBUG".
+ *
+ * @hide
+ */
+ public static final String LOG_TAG = "UiTranslation";
+
+ /**
+ * The state caller request to disable utranslation,, it is no longer need to ui translation.
+ *
+ * @hide
+ */
+ public static final int STATE_UI_TRANSLATION_STARTED = 0;
+ /**
+ * The state caller request to pause ui translation, it will switch back to the original text.
+ *
+ * @hide
+ */
+ public static final int STATE_UI_TRANSLATION_PAUSED = 1;
+ /**
+ * The state caller request to resume the paused ui translation, it will show the translated
+ * text again if the text had been translated.
+ *
+ * @hide
+ */
+ public static final int STATE_UI_TRANSLATION_RESUMED = 2;
+ /**
+ * The state the caller request to enable ui translation.
+ *
+ * @hide
+ */
+ public static final int STATE_UI_TRANSLATION_FINISHED = 3;
+ /**
+ * @hide
+ */
+ @IntDef(prefix = {"STATE__TRANSLATION"}, value = {
+ STATE_UI_TRANSLATION_STARTED,
+ STATE_UI_TRANSLATION_PAUSED,
+ STATE_UI_TRANSLATION_RESUMED,
+ STATE_UI_TRANSLATION_FINISHED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UiTranslationState {
+ }
+
+ // Keys for the data transmitted in the internal UI Translation state callback.
+ /** @hide */
+ public static final String EXTRA_STATE = "state";
+ /** @hide */
+ public static final String EXTRA_SOURCE_LOCALE = "source_locale";
+ /** @hide */
+ public static final String EXTRA_TARGET_LOCALE = "target_locale";
+
+ @NonNull
+ private final Context mContext;
+
+ private final ITranslationManager mService;
+
+ /**
+ * @hide
+ */
+ public UiTranslationManager(@NonNull Context context, ITranslationManager service) {
+ mContext = Objects.requireNonNull(context);
+ mService = service;
+ }
+
+ /**
+ * @removed Use {@link #startTranslation(TranslationSpec, TranslationSpec, List, ActivityId,
+ * UiTranslationSpec)} instead.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
+ @Deprecated
+ @SystemApi
+ public void startTranslation(@NonNull TranslationSpec sourceSpec,
+ @NonNull TranslationSpec targetSpec, @NonNull List<AutofillId> viewIds,
+ @NonNull ActivityId activityId) {
+ startTranslation(
+ sourceSpec, targetSpec, viewIds, activityId,
+ new UiTranslationSpec.Builder().setShouldPadContentForCompat(true).build());
+ }
+
+ /**
+ * Request ui translation for a given Views.
+ *
+ * @param sourceSpec {@link TranslationSpec} for the data to be translated.
+ * @param targetSpec {@link TranslationSpec} for the translated data.
+ * @param viewIds A list of the {@link View}'s {@link AutofillId} which needs to be translated
+ * @param activityId the identifier for the Activity which needs ui translation
+ * @param uiTranslationSpec configuration for translation of the specified views
+ * @throws IllegalArgumentException if the no {@link View}'s {@link AutofillId} in the list
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
+ @SystemApi
+ public void startTranslation(@NonNull TranslationSpec sourceSpec,
+ @NonNull TranslationSpec targetSpec, @NonNull List<AutofillId> viewIds,
+ @NonNull ActivityId activityId, @NonNull UiTranslationSpec uiTranslationSpec) {
+ // TODO(b/177789967): Return result code or find a way to notify the status.
+ Objects.requireNonNull(sourceSpec);
+ Objects.requireNonNull(targetSpec);
+ Objects.requireNonNull(viewIds);
+ Objects.requireNonNull(activityId);
+ Objects.requireNonNull(activityId.getToken());
+ Objects.requireNonNull(uiTranslationSpec);
+ if (viewIds.size() == 0) {
+ throw new IllegalArgumentException("Invalid empty views: " + viewIds);
+ }
+ try {
+ mService.updateUiTranslationState(STATE_UI_TRANSLATION_STARTED, sourceSpec,
+ targetSpec, viewIds, activityId.getToken(), activityId.getTaskId(),
+ uiTranslationSpec,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request to disable the ui translation. It will destroy all the {@link Translator}s and no
+ * longer to show to show the translated text.
+ *
+ * @param activityId the identifier for the Activity which needs ui translation
+ * @throws NullPointerException the activityId or
+ * {@link android.app.assist.ActivityId#getToken()} is {@code null}
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
+ @SystemApi
+ public void finishTranslation(@NonNull ActivityId activityId) {
+ try {
+ Objects.requireNonNull(activityId);
+ Objects.requireNonNull(activityId.getToken());
+ mService.updateUiTranslationState(STATE_UI_TRANSLATION_FINISHED,
+ null /* sourceSpec */, null /* targetSpec */, null /* viewIds */,
+ activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request to pause the current ui translation's {@link Translator} which will switch back to
+ * the original language.
+ *
+ * @param activityId the identifier for the Activity which needs ui translation
+ * @throws NullPointerException the activityId or
+ * {@link android.app.assist.ActivityId#getToken()} is {@code null}
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
+ @SystemApi
+ public void pauseTranslation(@NonNull ActivityId activityId) {
+ try {
+ Objects.requireNonNull(activityId);
+ Objects.requireNonNull(activityId.getToken());
+ mService.updateUiTranslationState(STATE_UI_TRANSLATION_PAUSED,
+ null /* sourceSpec */, null /* targetSpec */, null /* viewIds */,
+ activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request to resume the paused ui translation's {@link Translator} which will switch to the
+ * translated language if the text had been translated.
+ *
+ * @param activityId the identifier for the Activity which needs ui translation
+ * @throws NullPointerException the activityId or
+ * {@link android.app.assist.ActivityId#getToken()} is {@code null}
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
+ @SystemApi
+ public void resumeTranslation(@NonNull ActivityId activityId) {
+ try {
+ Objects.requireNonNull(activityId);
+ Objects.requireNonNull(activityId.getToken());
+ mService.updateUiTranslationState(STATE_UI_TRANSLATION_RESUMED,
+ null /* sourceSpec */, null /* targetSpec */, null /* viewIds */,
+ activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */,
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ // TODO(b/178044703): Fix the View API link when it becomes public.
+ /**
+ * Register for notifications of UI Translation state changes on the foreground activity. This
+ * is available to the owning application itself and also the current input method.
+ * <p>
+ * The application whose UI is being translated can use this to customize the UI Translation
+ * behavior in ways that aren't made easy by methods like
+ * View#onCreateTranslationRequest().
+ * <p>
+ * Input methods can use this to offer complementary features to UI Translation; for example,
+ * enabling outgoing message translation when the system is translating incoming messages in a
+ * communication app.
+ *
+ * @param callback the callback to register for receiving the state change
+ * notifications
+ */
+ public void registerUiTranslationStateCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull UiTranslationStateCallback callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ synchronized (mCallbacks) {
+ if (mCallbacks.containsKey(callback)) {
+ Log.w(TAG, "registerUiTranslationStateCallback: callback already registered;"
+ + " ignoring.");
+ return;
+ }
+ final IRemoteCallback remoteCallback =
+ new UiTranslationStateRemoteCallback(executor, callback);
+ try {
+ mService.registerUiTranslationStateCallback(remoteCallback, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCallbacks.put(callback, remoteCallback);
+ }
+ }
+
+ /**
+ * Unregister {@code callback}.
+ *
+ * @see #registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)
+ */
+ public void unregisterUiTranslationStateCallback(@NonNull UiTranslationStateCallback callback) {
+ Objects.requireNonNull(callback);
+
+ synchronized (mCallbacks) {
+ final IRemoteCallback remoteCallback = mCallbacks.get(callback);
+ if (remoteCallback == null) {
+ Log.w(TAG, "unregisterUiTranslationStateCallback: callback not found; ignoring.");
+ return;
+ }
+ try {
+ mService.unregisterUiTranslationStateCallback(remoteCallback, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCallbacks.remove(callback);
+ }
+ }
+
+ /**
+ * Notify apps the translation is finished because {@link #finishTranslation(ActivityId)} is
+ * called or Activity is destroyed.
+ *
+ * @param activityDestroyed if the ui translation is finished because of activity destroyed.
+ * @param activityId the identifier for the Activity which needs ui translation
+ * @param componentName the ui translated Activity componentName.
+ *
+ * @hide
+ */
+ public void onTranslationFinished(boolean activityDestroyed, ActivityId activityId,
+ ComponentName componentName) {
+ try {
+ mService.onTranslationFinished(activityDestroyed,
+ activityId.getToken(), componentName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @NonNull
+ @GuardedBy("mCallbacks")
+ private final Map<UiTranslationStateCallback, IRemoteCallback> mCallbacks = new ArrayMap<>();
+
+ private static class UiTranslationStateRemoteCallback extends IRemoteCallback.Stub {
+ private final Executor mExecutor;
+ private final UiTranslationStateCallback mCallback;
+ private ULocale mSourceLocale;
+ private ULocale mTargetLocale;
+
+ UiTranslationStateRemoteCallback(Executor executor,
+ UiTranslationStateCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void sendResult(Bundle bundle) {
+ Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> onStateChange(bundle)));
+ }
+
+ private void onStateChange(Bundle bundle) {
+ int state = bundle.getInt(EXTRA_STATE);
+ switch (state) {
+ case STATE_UI_TRANSLATION_STARTED:
+ mSourceLocale = (ULocale) bundle.getSerializable(EXTRA_SOURCE_LOCALE);
+ mTargetLocale = (ULocale) bundle.getSerializable(EXTRA_TARGET_LOCALE);
+ mCallback.onStarted(mSourceLocale, mTargetLocale);
+ break;
+ case STATE_UI_TRANSLATION_RESUMED:
+ mCallback.onResumed(mSourceLocale, mTargetLocale);
+ break;
+ case STATE_UI_TRANSLATION_PAUSED:
+ mCallback.onPaused();
+ break;
+ case STATE_UI_TRANSLATION_FINISHED:
+ mCallback.onFinished();
+ break;
+ default:
+ Log.wtf(TAG, "Unexpected translation state:" + state);
+ }
+ }
+ }
+}
diff --git a/android/view/translation/UiTranslationSpec.java b/android/view/translation/UiTranslationSpec.java
new file mode 100644
index 0000000..3d1ebfd
--- /dev/null
+++ b/android/view/translation/UiTranslationSpec.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.TextView;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Specifications for configuring UI translation.
+ *
+ * @hide
+ */
+@DataClass(
+ genBuilder = true, genEqualsHashCode = true, genHiddenConstDefs = true, genToString = true)
[email protected]("isShouldPadContentForCompat")
+@SystemApi
+public final class UiTranslationSpec implements Parcelable {
+
+ /**
+ * Whether the original content of the view should be made to appear as if it is the same size
+ * as the translated content. Defaults to {@code false}.
+ * <p>
+ * For {@link TextView}, the system does not directly modify the original text, rather
+ * changes the displayed content using a {@link android.text.method.TransformationMethod}. This
+ * can cause issues in apps that do not account for length-changing TransformationMethods. For
+ * example, an app using DynamicLayout may use the calculated line-offsets to operate on the
+ * original text, but this can cause crashes when the layout was calculated on translated text
+ * with a different length.
+ * <p>
+ * If this is {@code true}, for a TextView the default implementation appends spaces to the
+ * result of {@link TextView#getText()} to make the length the same as the translated text.
+ * <p>
+ * This only affects apps with target SDK R or lower.
+ */
+ private boolean mShouldPadContentForCompat = false;
+
+ /**
+ * Whether the original content of the view should be made to appear as if it is the same size
+ * as the translated content.
+ * <p>
+ * For {@link TextView}, the system does not directly modify the original text, rather
+ * changes the displayed content using a {@link android.text.method.TransformationMethod}. This
+ * can cause issues in apps that do not account for length-changing TransformationMethods. For
+ * example, an app using DynamicLayout may use the calculated line-offsets to operate on the
+ * original text, but this can cause crashes when the layout was calculated on translated text
+ * with a different length.
+ * <p>
+ * If this is {@code true}, for a TextView the default implementation appends spaces to the
+ * result of {@link TextView#getText()} to make the length the same as the translated text.
+ * <p>
+ * This only affects apps with target SDK R or lower.
+ */
+ public boolean shouldPadContentForCompat() {
+ return mShouldPadContentForCompat;
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/UiTranslationSpec.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ /* package-private */ UiTranslationSpec(
+ boolean shouldPadContentForCompat) {
+ this.mShouldPadContentForCompat = shouldPadContentForCompat;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "UiTranslationSpec { " +
+ "shouldPadContentForCompat = " + mShouldPadContentForCompat +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@android.annotation.Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(UiTranslationSpec other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ UiTranslationSpec that = (UiTranslationSpec) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && mShouldPadContentForCompat == that.mShouldPadContentForCompat;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + Boolean.hashCode(mShouldPadContentForCompat);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mShouldPadContentForCompat) flg |= 0x1;
+ dest.writeByte(flg);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ UiTranslationSpec(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean shouldPadContentForCompat = (flg & 0x1) != 0;
+
+ this.mShouldPadContentForCompat = shouldPadContentForCompat;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<UiTranslationSpec> CREATOR
+ = new Parcelable.Creator<UiTranslationSpec>() {
+ @Override
+ public UiTranslationSpec[] newArray(int size) {
+ return new UiTranslationSpec[size];
+ }
+
+ @Override
+ public UiTranslationSpec createFromParcel(@NonNull Parcel in) {
+ return new UiTranslationSpec(in);
+ }
+ };
+
+ /**
+ * A builder for {@link UiTranslationSpec}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder {
+
+ private boolean mShouldPadContentForCompat;
+
+ private long mBuilderFieldsSet = 0L;
+
+ public Builder() {
+ }
+
+ /**
+ * Whether the original content of the view should be made to appear as if it is the same size
+ * as the translated content. Defaults to {@code false}.
+ * <p>
+ * For {@link TextView}, the system does not directly modify the original text, rather
+ * changes the displayed content using a {@link android.text.method.TransformationMethod}. This
+ * can cause issues in apps that do not account for length-changing TransformationMethods. For
+ * example, an app using DynamicLayout may use the calculated line-offsets to operate on the
+ * original text, but this can cause crashes when the layout was calculated on translated text
+ * with a different length.
+ * <p>
+ * If this is {@code true}, for a TextView the default implementation appends spaces to the
+ * result of {@link TextView#getText()} to make the length the same as the translated text.
+ * <p>
+ * This only affects apps with target SDK R or lower.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setShouldPadContentForCompat(boolean value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mShouldPadContentForCompat = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull UiTranslationSpec build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ mShouldPadContentForCompat = false;
+ }
+ UiTranslationSpec o = new UiTranslationSpec(
+ mShouldPadContentForCompat);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x2) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1620790033058L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/UiTranslationSpec.java",
+ inputSignatures = "private boolean mShouldPadContentForCompat\npublic boolean shouldPadContentForCompat()\nclass UiTranslationSpec extends java.lang.Object implements [android.os.Parcelable]\[email protected](genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genToString=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/UiTranslationStateCallback.java b/android/view/translation/UiTranslationStateCallback.java
new file mode 100644
index 0000000..d7dc284
--- /dev/null
+++ b/android/view/translation/UiTranslationStateCallback.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.icu.util.ULocale;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Callback for listening to UI Translation state changes. See {@link
+ * UiTranslationManager#registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)}.
+ */
+public interface UiTranslationStateCallback {
+
+ /**
+ * @removed use {@link #onStarted(ULocale, ULocale)} instead.
+ */
+ @Deprecated
+ default void onStarted(@NonNull String sourceLocale, @NonNull String targetLocale) {
+ // no-op
+ }
+
+ /**
+ * The system is requesting translation of the UI from {@code sourceLocale} to {@code
+ * targetLocale}.
+ * <p>
+ * This is also called if either the requested {@code sourceLocale} or {@code targetLocale} has
+ * changed.
+ */
+ default void onStarted(@NonNull ULocale sourceLocale, @NonNull ULocale targetLocale) {
+ onStarted(sourceLocale.getLanguage(), targetLocale.getLanguage());
+ }
+
+ /**
+ * The system is requesting that the application temporarily show the UI contents in their
+ * original language.
+ */
+ void onPaused();
+
+ /**
+ * The system is requesting that the application restore from the temporarily paused state and
+ * show the content in translated language.
+ */
+ // TODO: Remove the default implementation when clients have implemented this.
+ default void onResumed(@NonNull ULocale sourceLocale, @NonNull ULocale targetLocale) {
+ }
+
+ /**
+ * The UI Translation session has ended.
+ */
+ void onFinished();
+}
diff --git a/android/view/translation/ViewTranslationCallback.java b/android/view/translation/ViewTranslationCallback.java
new file mode 100644
index 0000000..6efd621
--- /dev/null
+++ b/android/view/translation/ViewTranslationCallback.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.view.View;
+
+/**
+ * Callback for handling the translated information show or hide in the {@link View}.
+ */
+@UiThread
+public interface ViewTranslationCallback {
+ /**
+ * Called when the translated text is ready to show or if the user has requested to reshow the
+ * translated content after hiding it.
+ * <p>
+ * The translated content can be obtained from {@link View#getViewTranslationResponse}. This
+ * method will not be called before {@link View#onViewTranslationResponse} or
+ * {@link View#onVirtualViewTranslationResponses}.
+ *
+ * See {@link View#onViewTranslationResponse} for how to get the translated information.
+ *
+ * @return {@code true} if the View handles showing the translation.
+ */
+ boolean onShowTranslation(@NonNull View view);
+ /**
+ * Called when the user wants to show the original text instead of the translated text. This
+ * method will not be called before {@link View#onViewTranslationResponse} or
+ * {@link View#onViewTranslationResponse}.
+ *
+ * @return {@code true} if the View handles hiding the translation.
+ */
+ boolean onHideTranslation(@NonNull View view);
+ /**
+ * Called when the user finish the Ui translation and no longer to show the translated text.
+ *
+ * @return {@code true} if the View handles clearing the translation.
+ */
+ boolean onClearTranslation(@NonNull View view);
+
+ /**
+ * Enables padding on the view's original content.
+ * <p>
+ * This is useful when we do not modify the content directly, rather use a mechanism like
+ * {@link android.text.method.TransformationMethod}. If the app misbehaves when the displayed
+ * translation and the underlying content have different sizes, the platform intelligence can
+ * request that the original content be padded to make the sizes match.
+ *
+ * @hide
+ */
+ default void enableContentPadding() {}
+
+ /**
+ * Sets the duration for animations while transitioning the view between the original and
+ * translated contents.
+ *
+ * @hide
+ */
+ default void setAnimationDurationMillis(int durationMillis) {}
+}
diff --git a/android/view/translation/ViewTranslationRequest.java b/android/view/translation/ViewTranslationRequest.java
new file mode 100644
index 0000000..a41749a
--- /dev/null
+++ b/android/view/translation/ViewTranslationRequest.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Wrapper class representing a translation request associated with a {@link android.view.View} to
+ * be used by {@link android.service.translation.TranslationService}.
+ */
+@DataClass(genBuilder = false, genToString = true, genEqualsHashCode = true, genGetters = false,
+ genHiddenConstructor = true, genHiddenConstDefs = true)
+public final class ViewTranslationRequest implements Parcelable {
+
+ /**
+ * Constant id for the default view text to be translated. This is used by
+ * {@link Builder#setValue(String, TranslationRequestValue)}.
+ */
+ public static final String ID_TEXT = "android:text";
+
+ /**
+ * Constant id for the default view content description to be translated. This is used by
+ * {@link Builder#setValue(String, TranslationRequestValue)}.
+ *
+ * @hide
+ */
+ public static final String ID_CONTENT_DESCRIPTION = "android:content_description";
+
+ /**
+ * The {@link AutofillId} of the view associated with this request.
+ */
+ @NonNull
+ private final AutofillId mAutofillId;
+
+ @NonNull
+ @DataClass.PluralOf("translationRequestValue")
+ private final Map<String, TranslationRequestValue> mTranslationRequestValues;
+
+ /**
+ * Gets the corresponding {@link TranslationRequestValue} of the provided key.
+ * @param key String id of the translation request value to be translated.
+ * @return the {@link TranslationRequestValue}.
+ * @throws IllegalArgumentException if the key does not exist.
+ */
+ @NonNull
+ public TranslationRequestValue getValue(@NonNull String key) {
+ Objects.requireNonNull(key, "key should not be null");
+ if (!mTranslationRequestValues.containsKey(key)) {
+ throw new IllegalArgumentException("Request does not contain value for key=" + key);
+ }
+ return mTranslationRequestValues.get(key);
+ }
+
+ /**
+ * Returns all keys in this request as a {@link Set} of Strings. The keys are used by
+ * {@link #getValue(String)} to get the {@link TranslationRequestValue}s.
+ */
+ @NonNull
+ public Set<String> getKeys() {
+ return mTranslationRequestValues.keySet();
+ }
+
+
+ /**
+ * Returns the associated {@link AutofillId} of this request.
+ */
+ @NonNull
+ public AutofillId getAutofillId() {
+ return mAutofillId;
+ }
+
+ private static Map<String, TranslationRequestValue> defaultTranslationRequestValues() {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * A builder for building ViewTranslationRequest.
+ */
+ public static final class Builder {
+
+ private @NonNull AutofillId mAutofillId;
+ private @NonNull Map<String, TranslationRequestValue> mTranslationRequestValues;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param autofillId The {@link AutofillId} of the view associated with this request.
+ */
+ public Builder(@NonNull AutofillId autofillId) {
+ mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ }
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param autofillId the {@link AutofillId} of the non-virtual view hosting the virtual view
+ * hierarchy associated with this request.
+ * @param virtualChildId the id of the virtual view in the host view. This id is the same
+ * virtual id provided through content capture.
+ */
+ public Builder(@NonNull AutofillId autofillId, long virtualChildId) {
+ mAutofillId = new AutofillId(autofillId, virtualChildId, AutofillId.NO_SESSION);
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ }
+
+ /**
+ * Sets the corresponding {@link TranslationRequestValue} for the provided key.
+ *
+ * @param key The key for this translation request value.
+ * @param value the translation request value holding the content to be translated.
+ * @return this builder.
+ */
+ @SuppressLint("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setValue(@NonNull String key, @NonNull TranslationRequestValue value) {
+ if (mTranslationRequestValues == null) {
+ setTranslationRequestValues(new ArrayMap<>());
+ }
+ mTranslationRequestValues.put(key, value);
+ return this;
+ }
+
+ /**
+ * Builds the instance. This builder should not be touched after calling this!
+ */
+ @NonNull
+ public ViewTranslationRequest build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mTranslationRequestValues = defaultTranslationRequestValues();
+ }
+ ViewTranslationRequest o = new ViewTranslationRequest(
+ mAutofillId,
+ mTranslationRequestValues);
+ return o;
+ }
+
+ Builder setTranslationRequestValues(@NonNull Map<String, TranslationRequestValue> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mTranslationRequestValues = value;
+ return this;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x4) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+
+
+ // Code below generated by codegen v1.0.23.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/ViewTranslationRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @android.annotation.StringDef(prefix = "ID_", value = {
+ ID_TEXT,
+ ID_CONTENT_DESCRIPTION
+ })
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface Id {}
+
+ /**
+ * Creates a new ViewTranslationRequest.
+ *
+ * @param autofillId
+ * The {@link AutofillId} of the view associated with this request.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public ViewTranslationRequest(
+ @NonNull AutofillId autofillId,
+ @NonNull Map<String,TranslationRequestValue> translationRequestValues) {
+ this.mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ this.mTranslationRequestValues = translationRequestValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationRequestValues);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "ViewTranslationRequest { " +
+ "autofillId = " + mAutofillId + ", " +
+ "translationRequestValues = " + mTranslationRequestValues +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(ViewTranslationRequest other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ ViewTranslationRequest that = (ViewTranslationRequest) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && Objects.equals(mAutofillId, that.mAutofillId)
+ && Objects.equals(mTranslationRequestValues, that.mTranslationRequestValues);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + Objects.hashCode(mAutofillId);
+ _hash = 31 * _hash + Objects.hashCode(mTranslationRequestValues);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedObject(mAutofillId, flags);
+ dest.writeMap(mTranslationRequestValues);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ ViewTranslationRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ AutofillId autofillId = (AutofillId) in.readTypedObject(AutofillId.CREATOR);
+ Map<String,TranslationRequestValue> translationRequestValues = new java.util.LinkedHashMap<>();
+ in.readMap(translationRequestValues, TranslationRequestValue.class.getClassLoader());
+
+ this.mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ this.mTranslationRequestValues = translationRequestValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationRequestValues);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<ViewTranslationRequest> CREATOR
+ = new Parcelable.Creator<ViewTranslationRequest>() {
+ @Override
+ public ViewTranslationRequest[] newArray(int size) {
+ return new ViewTranslationRequest[size];
+ }
+
+ @Override
+ public ViewTranslationRequest createFromParcel(@NonNull Parcel in) {
+ return new ViewTranslationRequest(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1621230365943L,
+ codegenVersion = "1.0.23",
+ sourceFile = "frameworks/base/core/java/android/view/translation/ViewTranslationRequest.java",
+ inputSignatures = "public static final java.lang.String ID_TEXT\npublic static final java.lang.String ID_CONTENT_DESCRIPTION\nprivate final @android.annotation.NonNull android.view.autofill.AutofillId mAutofillId\nprivate final @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"translationRequestValue\") java.util.Map<java.lang.String,android.view.translation.TranslationRequestValue> mTranslationRequestValues\npublic @android.annotation.NonNull android.view.translation.TranslationRequestValue getValue(java.lang.String)\npublic @android.annotation.NonNull java.util.Set<java.lang.String> getKeys()\npublic @android.annotation.NonNull android.view.autofill.AutofillId getAutofillId()\nprivate static java.util.Map<java.lang.String,android.view.translation.TranslationRequestValue> defaultTranslationRequestValues()\nclass ViewTranslationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate @android.annotation.NonNull android.view.autofill.AutofillId mAutofillId\nprivate @android.annotation.NonNull java.util.Map<java.lang.String,android.view.translation.TranslationRequestValue> mTranslationRequestValues\nprivate long mBuilderFieldsSet\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.view.translation.ViewTranslationRequest.Builder setValue(java.lang.String,android.view.translation.TranslationRequestValue)\npublic @android.annotation.NonNull android.view.translation.ViewTranslationRequest build()\n android.view.translation.ViewTranslationRequest.Builder setTranslationRequestValues(java.util.Map<java.lang.String,android.view.translation.TranslationRequestValue>)\nprivate void checkNotUsed()\nclass Builder extends java.lang.Object implements []\[email protected](genBuilder=false, genToString=true, genEqualsHashCode=true, genGetters=false, genHiddenConstructor=true, genHiddenConstDefs=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/view/translation/ViewTranslationResponse.java b/android/view/translation/ViewTranslationResponse.java
new file mode 100644
index 0000000..d993114
--- /dev/null
+++ b/android/view/translation/ViewTranslationResponse.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Wrapper class representing a translation response associated with a {@link android.view.View} to
+ * be used by {@link android.service.translation.TranslationService}.
+ */
+@DataClass(genBuilder = true, genToString = true, genEqualsHashCode = true, genGetters = false)
+public final class ViewTranslationResponse implements Parcelable {
+
+ /**
+ * The {@link AutofillId} of the view associated with this response.
+ */
+ @NonNull
+ private final AutofillId mAutofillId;
+
+ @NonNull
+ @DataClass.PluralOf("translationResponseValue")
+ private final Map<String, TranslationResponseValue> mTranslationResponseValues;
+
+ /**
+ * Gets the {@link TranslationResponseValue} of the corresponding key.
+ * @param key String id of the translated translation response value.
+ * @return the {@link TranslationResponseValue}.
+ * @throws IllegalArgumentException if the key does not exist.
+ */
+ @NonNull
+ public TranslationResponseValue getValue(@NonNull String key) {
+ Objects.requireNonNull(key);
+ if (!mTranslationResponseValues.containsKey(key)) {
+ throw new IllegalArgumentException("Request does not contain value for key=" + key);
+ }
+ return mTranslationResponseValues.get(key);
+ }
+
+ /**
+ * Returns all keys in this response as a {@link Set} of Strings. The keys are used by
+ * {@link #getValue(String)} to get the {@link TranslationResponseValue}s.
+ */
+ @NonNull
+ public Set<String> getKeys() {
+ return mTranslationResponseValues.keySet();
+ }
+
+
+ /**
+ * Returns the associated {@link AutofillId} of this response.
+ */
+ @NonNull
+ public AutofillId getAutofillId() {
+ return mAutofillId;
+ }
+
+ private static Map<String, TranslationResponseValue> defaultTranslationResponseValues() {
+ return Collections.emptyMap();
+ }
+
+ @DataClass.Suppress({"addTranslationResponseValue", "setAutofillId"})
+ abstract static class BaseBuilder {
+
+ abstract Builder setTranslationResponseValues(Map<String, TranslationResponseValue> value);
+
+ /**
+ * Sets the corresponding {@link TranslationResponseValue} for the provided key.
+ *
+ * @param key The key for this translation response value.
+ * @param value the translation response value holding the translated content.
+ * @return this builder.
+ */
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setValue(String key,
+ TranslationResponseValue value) {
+ final Builder builder = (Builder) this;
+ if (builder.mTranslationResponseValues == null) {
+ setTranslationResponseValues(new ArrayMap<>());
+ }
+ builder.mTranslationResponseValues.put(key, value);
+ return builder;
+ }
+ }
+
+
+
+ // Code below generated by codegen v1.0.22.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/ViewTranslationResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ /* package-private */ ViewTranslationResponse(
+ @NonNull AutofillId autofillId,
+ @NonNull Map<String,TranslationResponseValue> translationResponseValues) {
+ this.mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ this.mTranslationResponseValues = translationResponseValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationResponseValues);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "ViewTranslationResponse { " +
+ "autofillId = " + mAutofillId + ", " +
+ "translationResponseValues = " + mTranslationResponseValues +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(ViewTranslationResponse other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ ViewTranslationResponse that = (ViewTranslationResponse) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && Objects.equals(mAutofillId, that.mAutofillId)
+ && Objects.equals(mTranslationResponseValues, that.mTranslationResponseValues);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + Objects.hashCode(mAutofillId);
+ _hash = 31 * _hash + Objects.hashCode(mTranslationResponseValues);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeTypedObject(mAutofillId, flags);
+ dest.writeMap(mTranslationResponseValues);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ ViewTranslationResponse(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ AutofillId autofillId = (AutofillId) in.readTypedObject(AutofillId.CREATOR);
+ Map<String,TranslationResponseValue> translationResponseValues = new java.util.LinkedHashMap<>();
+ in.readMap(translationResponseValues, TranslationResponseValue.class.getClassLoader());
+
+ this.mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ this.mTranslationResponseValues = translationResponseValues;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mTranslationResponseValues);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<ViewTranslationResponse> CREATOR
+ = new Parcelable.Creator<ViewTranslationResponse>() {
+ @Override
+ public ViewTranslationResponse[] newArray(int size) {
+ return new ViewTranslationResponse[size];
+ }
+
+ @Override
+ public ViewTranslationResponse createFromParcel(@NonNull Parcel in) {
+ return new ViewTranslationResponse(in);
+ }
+ };
+
+ /**
+ * A builder for {@link ViewTranslationResponse}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @NonNull AutofillId mAutofillId;
+ private @NonNull Map<String,TranslationResponseValue> mTranslationResponseValues;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param autofillId
+ * The {@link AutofillId} of the view associated with this response.
+ */
+ public Builder(
+ @NonNull AutofillId autofillId) {
+ mAutofillId = autofillId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mAutofillId);
+ }
+
+ @DataClass.Generated.Member
+ @Override
+ @NonNull Builder setTranslationResponseValues(@NonNull Map<String,TranslationResponseValue> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mTranslationResponseValues = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull ViewTranslationResponse build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mTranslationResponseValues = defaultTranslationResponseValues();
+ }
+ ViewTranslationResponse o = new ViewTranslationResponse(
+ mAutofillId,
+ mTranslationResponseValues);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x4) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1614992272865L,
+ codegenVersion = "1.0.22",
+ sourceFile = "frameworks/base/core/java/android/view/translation/ViewTranslationResponse.java",
+ inputSignatures = "private final @android.annotation.NonNull android.view.autofill.AutofillId mAutofillId\nprivate final @android.annotation.NonNull @com.android.internal.util.DataClass.PluralOf(\"translationResponseValue\") java.util.Map<java.lang.String,android.view.translation.TranslationResponseValue> mTranslationResponseValues\npublic @android.annotation.NonNull android.view.translation.TranslationResponseValue getValue(java.lang.String)\npublic @android.annotation.NonNull java.util.Set<java.lang.String> getKeys()\npublic @android.annotation.NonNull android.view.autofill.AutofillId getAutofillId()\nprivate static java.util.Map<java.lang.String,android.view.translation.TranslationResponseValue> defaultTranslationResponseValues()\nclass ViewTranslationResponse extends java.lang.Object implements [android.os.Parcelable]\nabstract android.view.translation.ViewTranslationResponse.Builder setTranslationResponseValues(java.util.Map<java.lang.String,android.view.translation.TranslationResponseValue>)\npublic @android.annotation.SuppressLint android.view.translation.ViewTranslationResponse.Builder setValue(java.lang.String,android.view.translation.TranslationResponseValue)\nclass BaseBuilder extends java.lang.Object implements []\[email protected](genBuilder=true, genToString=true, genEqualsHashCode=true, genGetters=false)\nabstract android.view.translation.ViewTranslationResponse.Builder setTranslationResponseValues(java.util.Map<java.lang.String,android.view.translation.TranslationResponseValue>)\npublic @android.annotation.SuppressLint android.view.translation.ViewTranslationResponse.Builder setValue(java.lang.String,android.view.translation.TranslationResponseValue)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}