diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-10 15:20:13 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-10 15:20:13 -0400 |
commit | 93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6 (patch) | |
tree | 49f76f879a89c256a4f65b674086be50760bdffb /android/view | |
parent | bc81c7ada5aab3806dd0b17498f5c9672c9b33c4 (diff) | |
download | android-28-93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6.tar.gz |
Import Android SDK Platform P [4386628]
/google/data/ro/projects/android/fetch_artifact \
--bid 4386628 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4386628.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I9b8400ac92116cae4f033d173f7a5682b26ccba9
Diffstat (limited to 'android/view')
-rw-r--r-- | android/view/DragEvent.java | 5 | ||||
-rw-r--r-- | android/view/Gravity.java | 53 | ||||
-rw-r--r-- | android/view/MenuInflater_Delegate.java | 14 | ||||
-rw-r--r-- | android/view/Surface.java | 2 | ||||
-rw-r--r-- | android/view/SurfaceControl.java | 21 | ||||
-rw-r--r-- | android/view/SurfaceView.java | 1135 | ||||
-rw-r--r-- | android/view/View.java | 145 | ||||
-rw-r--r-- | android/view/ViewDebug.java | 75 | ||||
-rw-r--r-- | android/view/ViewRootImpl.java | 6 | ||||
-rw-r--r-- | android/view/ViewStructure.java | 2 | ||||
-rw-r--r-- | android/view/WindowManager.java | 395 | ||||
-rw-r--r-- | android/view/WindowManagerPolicy.java | 67 | ||||
-rw-r--r-- | android/view/accessibility/AccessibilityManager.java | 916 | ||||
-rw-r--r-- | android/view/autofill/AutofillManager.java | 129 | ||||
-rw-r--r-- | android/view/autofill/AutofillPopupWindow.java | 29 | ||||
-rw-r--r-- | android/view/textclassifier/TextClassification.java | 188 | ||||
-rw-r--r-- | android/view/textclassifier/TextClassifierImpl.java | 118 | ||||
-rw-r--r-- | android/view/textclassifier/logging/SmartSelectionEventTracker.java | 1 | ||||
-rw-r--r-- | android/view/textservice/TextServicesManager.java | 200 |
19 files changed, 3192 insertions, 309 deletions
diff --git a/android/view/DragEvent.java b/android/view/DragEvent.java index 16f2d7d0..2c9f8712 100644 --- a/android/view/DragEvent.java +++ b/android/view/DragEvent.java @@ -103,7 +103,7 @@ import com.android.internal.view.IDragAndDropPermissions; * <tr> * <td>ACTION_DRAG_ENDED</td> * <td style="text-align: center;"> </td> - * <td style="text-align: center;"> </td> + * <td style="text-align: center;">X</td> * <td style="text-align: center;"> </td> * <td style="text-align: center;"> </td> * <td style="text-align: center;"> </td> @@ -112,6 +112,7 @@ import com.android.internal.view.IDragAndDropPermissions; * </table> * <p> * The {@link android.view.DragEvent#getAction()}, + * {@link android.view.DragEvent#getLocalState()} * {@link android.view.DragEvent#describeContents()}, * {@link android.view.DragEvent#writeToParcel(Parcel,int)}, and * {@link android.view.DragEvent#toString()} methods always return valid data. @@ -397,7 +398,7 @@ public class DragEvent implements Parcelable { * operation. In all other activities this method will return null * </p> * <p> - * This method returns valid data for all event actions except for {@link #ACTION_DRAG_ENDED}. + * This method returns valid data for all event actions. * </p> * @return The local state object sent to the system by startDragAndDrop(). */ diff --git a/android/view/Gravity.java b/android/view/Gravity.java index 324a1ae3..232ff255 100644 --- a/android/view/Gravity.java +++ b/android/view/Gravity.java @@ -440,4 +440,57 @@ public class Gravity } return result; } + + /** + * @hide + */ + public static String toString(int gravity) { + final StringBuilder result = new StringBuilder(); + if ((gravity & FILL) != 0) { + result.append("FILL").append(' '); + } else { + if ((gravity & FILL_VERTICAL) != 0) { + result.append("FILL_VERTICAL").append(' '); + } else { + if ((gravity & TOP) != 0) { + result.append("TOP").append(' '); + } + if ((gravity & BOTTOM) != 0) { + result.append("BOTTOM").append(' '); + } + } + if ((gravity & FILL_HORIZONTAL) != 0) { + result.append("FILL_HORIZONTAL").append(' '); + } else { + if ((gravity & START) != 0) { + result.append("START").append(' '); + } else if ((gravity & LEFT) != 0) { + result.append("LEFT").append(' '); + } + if ((gravity & END) != 0) { + result.append("END").append(' '); + } else if ((gravity & RIGHT) != 0) { + result.append("RIGHT").append(' '); + } + } + } + if ((gravity & CENTER) != 0) { + result.append("CENTER").append(' '); + } else { + if ((gravity & CENTER_VERTICAL) != 0) { + result.append("CENTER_VERTICAL").append(' '); + } + if ((gravity & CENTER_HORIZONTAL) != 0) { + result.append("CENTER_HORIZONTAL").append(' '); + } + } + if ((gravity & DISPLAY_CLIP_VERTICAL) != 0) { + result.append("DISPLAY_CLIP_VERTICAL").append(' '); + } + if ((gravity & DISPLAY_CLIP_VERTICAL) != 0) { + result.append("DISPLAY_CLIP_VERTICAL").append(' '); + } + result.deleteCharAt(result.length() - 1); + return result.toString(); + } } diff --git a/android/view/MenuInflater_Delegate.java b/android/view/MenuInflater_Delegate.java index 08a97d64..977a2a72 100644 --- a/android/view/MenuInflater_Delegate.java +++ b/android/view/MenuInflater_Delegate.java @@ -42,7 +42,6 @@ import android.util.AttributeSet; * ViewInfo}, we check the corresponding view key in the menu item for the view and add it */ public class MenuInflater_Delegate { - @LayoutlibDelegate /*package*/ static void registerMenu(MenuInflater thisInflater, MenuItem menuItem, AttributeSet attrs) { @@ -56,10 +55,15 @@ public class MenuInflater_Delegate { return; } } - // This means that Bridge did not take over the instantiation of some object properly. - // This is most likely a bug in the LayoutLib code. - Bridge.getLog().warning(LayoutLog.TAG_BROKEN, - "Action Bar Menu rendering may be incorrect.", null); + + if (menuItem == null || !menuItem.getClass().getName().startsWith("android.support.")) { + // This means that Bridge did not take over the instantiation of some object properly. + // This is most likely a bug in the LayoutLib code. + // We suppress this error for AppCompat menus since we do not support them in the menu + // editor yet. + Bridge.getLog().warning(LayoutLog.TAG_BROKEN, + "Action Bar Menu rendering may be incorrect.", null); + } } diff --git a/android/view/Surface.java b/android/view/Surface.java index 2c1f7346..ddced6cd 100644 --- a/android/view/Surface.java +++ b/android/view/Surface.java @@ -762,7 +762,7 @@ public class Surface implements Parcelable { return "ROTATION_270"; } default: { - throw new IllegalArgumentException("Invalid rotation: " + rotation); + return Integer.toString(rotation); } } } diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index 91932dd4..31daefff 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -18,6 +18,7 @@ package android.view; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import android.annotation.Size; import android.graphics.Bitmap; import android.graphics.GraphicBuffer; import android.graphics.Rect; @@ -65,6 +66,7 @@ public class SurfaceControl { private static native void nativeSetSize(long nativeObject, int w, int h); private static native void nativeSetTransparentRegionHint(long nativeObject, Region region); private static native void nativeSetAlpha(long nativeObject, float alpha); + private static native void nativeSetColor(long nativeObject, float[] color); private static native void nativeSetMatrix(long nativeObject, float dsdx, float dtdx, float dtdy, float dsdy); private static native void nativeSetFlags(long nativeObject, int flags, int mask); @@ -105,8 +107,8 @@ public class SurfaceControl { long surfaceObject, long frame); private static native void nativeReparentChildren(long nativeObject, IBinder handle); - private static native void nativeReparentChild(long nativeObject, - IBinder parentHandle, IBinder childHandle); + private static native void nativeReparent(long nativeObject, + IBinder parentHandle); private static native void nativeSeverChildren(long nativeObject); private static native void nativeSetOverrideScalingMode(long nativeObject, int scalingMode); @@ -455,9 +457,9 @@ public class SurfaceControl { nativeReparentChildren(mNativeObject, newParentHandle); } - /** Re-parents a specific child layer to a new parent */ - public void reparentChild(IBinder newParentHandle, IBinder childHandle) { - nativeReparentChild(mNativeObject, newParentHandle, childHandle); + /** Re-parents this layer to a new parent. */ + public void reparent(IBinder newParentHandle) { + nativeReparent(mNativeObject, newParentHandle); } public void detachChildren() { @@ -552,6 +554,15 @@ public class SurfaceControl { nativeSetAlpha(mNativeObject, alpha); } + /** + * Sets a color for the Surface. + * @param color A float array with three values to represent r, g, b in range [0..1] + */ + public void setColor(@Size(3) float[] color) { + checkNotReleased(); + nativeSetColor(mNativeObject, color); + } + public void setMatrix(float dsdx, float dtdx, float dtdy, float dsdy) { checkNotReleased(); nativeSetMatrix(mNativeObject, dsdx, dtdx, dtdy, dsdy); diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index ebb2af45..462dad3f 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,115 +16,1208 @@ package android.view; -import com.android.layoutlib.bridge.MockView; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; +import static android.view.WindowManagerPolicy.APPLICATION_MEDIA_OVERLAY_SUBLAYER; +import static android.view.WindowManagerPolicy.APPLICATION_MEDIA_SUBLAYER; +import static android.view.WindowManagerPolicy.APPLICATION_PANEL_SUBLAYER; import android.content.Context; +import android.content.res.CompatibilityInfo.Translator; +import android.content.res.Configuration; import android.graphics.Canvas; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Log; + +import com.android.internal.view.SurfaceCallbackHelper; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantLock; /** - * Mock version of the SurfaceView. - * Only non override public methods from the real SurfaceView have been added in there. - * Methods that take an unknown class as parameter or as return object, have been removed for now. + * Provides a dedicated drawing surface embedded inside of a view hierarchy. + * You can control the format of this surface and, if you like, its size; the + * SurfaceView takes care of placing the surface at the correct location on the + * screen + * + * <p>The surface is Z ordered so that it is behind the window holding its + * SurfaceView; the SurfaceView punches a hole in its window to allow its + * surface to be displayed. The view hierarchy will take care of correctly + * compositing with the Surface any siblings of the SurfaceView that would + * normally appear on top of it. This can be used to place overlays such as + * buttons on top of the Surface, though note however that it can have an + * impact on performance since a full alpha-blended composite will be performed + * each time the Surface changes. + * + * <p> The transparent region that makes the surface visible is based on the + * layout positions in the view hierarchy. If the post-layout transform + * properties are used to draw a sibling view on top of the SurfaceView, the + * view may not be properly composited with the surface. * - * TODO: generate automatically. + * <p>Access to the underlying surface is provided via the SurfaceHolder interface, + * which can be retrieved by calling {@link #getHolder}. * + * <p>The Surface will be created for you while the SurfaceView's window is + * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated} + * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the + * Surface is created and destroyed as the window is shown and hidden. + * + * <p>One of the purposes of this class is to provide a surface in which a + * secondary thread can render into the screen. If you are going to use it + * this way, you need to be aware of some threading semantics: + * + * <ul> + * <li> All SurfaceView and + * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called + * from the thread running the SurfaceView's window (typically the main thread + * of the application). They thus need to correctly synchronize with any + * state that is also touched by the drawing thread. + * <li> You must ensure that the drawing thread only touches the underlying + * Surface while it is valid -- between + * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()} + * and + * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}. + * </ul> + * + * <p class="note"><strong>Note:</strong> Starting in platform version + * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is + * updated synchronously with other View rendering. This means that translating + * and scaling a SurfaceView on screen will not cause rendering artifacts. Such + * artifacts may occur on previous versions of the platform when its window is + * positioned asynchronously.</p> */ -public class SurfaceView extends MockView { +public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback { + private static final String TAG = "SurfaceView"; + private static final boolean DEBUG = false; + + final ArrayList<SurfaceHolder.Callback> mCallbacks + = new ArrayList<SurfaceHolder.Callback>(); + + final int[] mLocation = new int[2]; + + final ReentrantLock mSurfaceLock = new ReentrantLock(); + final Surface mSurface = new Surface(); // Current surface in use + boolean mDrawingStopped = true; + // We use this to track if the application has produced a frame + // in to the Surface. Up until that point, we should be careful not to punch + // holes. + boolean mDrawFinished = false; + + final Rect mScreenRect = new Rect(); + SurfaceSession mSurfaceSession; + + SurfaceControl mSurfaceControl; + // In the case of format changes we switch out the surface in-place + // we need to preserve the old one until the new one has drawn. + SurfaceControl mDeferredDestroySurfaceControl; + final Rect mTmpRect = new Rect(); + final Configuration mConfiguration = new Configuration(); + + int mSubLayer = APPLICATION_MEDIA_SUBLAYER; + + boolean mIsCreating = false; + private volatile boolean mRtHandlingPositionUpdates = false; + + private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener + = new ViewTreeObserver.OnScrollChangedListener() { + @Override + public void onScrollChanged() { + updateSurface(); + } + }; + + private final ViewTreeObserver.OnPreDrawListener mDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + // reposition ourselves where the surface is + mHaveFrame = getWidth() > 0 && getHeight() > 0; + updateSurface(); + return true; + } + }; + + boolean mRequestedVisible = false; + boolean mWindowVisibility = false; + boolean mLastWindowVisibility = false; + boolean mViewVisibility = false; + boolean mWindowStopped = false; + + int mRequestedWidth = -1; + int mRequestedHeight = -1; + /* Set SurfaceView's format to 565 by default to maintain backward + * compatibility with applications assuming this format. + */ + int mRequestedFormat = PixelFormat.RGB_565; + + boolean mHaveFrame = false; + boolean mSurfaceCreated = false; + long mLastLockTime = 0; + + boolean mVisible = false; + int mWindowSpaceLeft = -1; + int mWindowSpaceTop = -1; + int mSurfaceWidth = -1; + int mSurfaceHeight = -1; + int mFormat = -1; + final Rect mSurfaceFrame = new Rect(); + int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1; + private Translator mTranslator; + + private boolean mGlobalListenersAdded; + private boolean mAttachedToWindow; + + private int mSurfaceFlags = SurfaceControl.HIDDEN; + + private int mPendingReportDraws; public SurfaceView(Context context) { this(context, null); } public SurfaceView(Context context, AttributeSet attrs) { - this(context, attrs , 0); + this(context, attrs, 0); } - public SurfaceView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + mRenderNode.requestPositionUpdates(this); + + setWillNotDraw(true); + } + + /** + * Return the SurfaceHolder providing access and control over this + * SurfaceView's underlying surface. + * + * @return SurfaceHolder The holder of the surface. + */ + public SurfaceHolder getHolder() { + return mSurfaceHolder; + } + + private void updateRequestedVisibility() { + mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped; + } + + /** @hide */ + @Override + public void windowStopped(boolean stopped) { + mWindowStopped = stopped; + updateRequestedVisibility(); + updateSurface(); } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + getViewRootImpl().addWindowStoppedCallback(this); + mWindowStopped = false; + + mViewVisibility = getVisibility() == VISIBLE; + updateRequestedVisibility(); + + mAttachedToWindow = true; + mParent.requestTransparentRegion(SurfaceView.this); + if (!mGlobalListenersAdded) { + ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnScrollChangedListener(mScrollChangedListener); + observer.addOnPreDrawListener(mDrawListener); + mGlobalListenersAdded = true; + } + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mWindowVisibility = visibility == VISIBLE; + updateRequestedVisibility(); + updateSurface(); + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + mViewVisibility = visibility == VISIBLE; + boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped; + if (newRequestedVisible != mRequestedVisible) { + // our base class (View) invalidates the layout only when + // we go from/to the GONE state. However, SurfaceView needs + // to request a re-layout when the visibility changes at all. + // This is needed because the transparent region is computed + // as part of the layout phase, and it changes (obviously) when + // the visibility changes. + requestLayout(); + } + mRequestedVisible = newRequestedVisible; + updateSurface(); + } + + private void performDrawFinished() { + if (mPendingReportDraws > 0) { + mDrawFinished = true; + if (mAttachedToWindow) { + notifyDrawFinished(); + invalidate(); + } + } else { + Log.e(TAG, System.identityHashCode(this) + "finished drawing" + + " but no pending report draw (extra call" + + " to draw completion runnable?)"); + } + } + + void notifyDrawFinished() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null) { + viewRoot.pendingDrawFinished(); + } + mPendingReportDraws--; + } + + @Override + protected void onDetachedFromWindow() { + ViewRootImpl viewRoot = getViewRootImpl(); + // It's possible to create a SurfaceView using the default constructor and never + // attach it to a view hierarchy, this is a common use case when dealing with + // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage + // the lifecycle. Instead of attaching it to a view, he/she can just pass + // the SurfaceHolder forward, most live wallpapers do it. + if (viewRoot != null) { + viewRoot.removeWindowStoppedCallback(this); + } + + mAttachedToWindow = false; + if (mGlobalListenersAdded) { + ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnScrollChangedListener(mScrollChangedListener); + observer.removeOnPreDrawListener(mDrawListener); + mGlobalListenersAdded = false; + } + + while (mPendingReportDraws > 0) { + notifyDrawFinished(); + } + + mRequestedVisible = false; + + updateSurface(); + if (mSurfaceControl != null) { + mSurfaceControl.destroy(); + } + mSurfaceControl = null; + + mHaveFrame = false; + + super.onDetachedFromWindow(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = mRequestedWidth >= 0 + ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0) + : getDefaultSize(0, widthMeasureSpec); + int height = mRequestedHeight >= 0 + ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0) + : getDefaultSize(0, heightMeasureSpec); + setMeasuredDimension(width, height); + } + + /** @hide */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + boolean result = super.setFrame(left, top, right, bottom); + updateSurface(); + return result; + } + + @Override public boolean gatherTransparentRegion(Region region) { - return false; + if (isAboveParent() || !mDrawFinished) { + return super.gatherTransparentRegion(region); + } + + boolean opaque = true; + if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { + // this view draws, remove it from the transparent region + opaque = super.gatherTransparentRegion(region); + } else if (region != null) { + int w = getWidth(); + int h = getHeight(); + if (w>0 && h>0) { + getLocationInWindow(mLocation); + // otherwise, punch a hole in the whole hierarchy + int l = mLocation[0]; + int t = mLocation[1]; + region.op(l, t, l+w, t+h, Region.Op.UNION); + } + } + if (PixelFormat.formatHasAlpha(mRequestedFormat)) { + opaque = false; + } + return opaque; } + @Override + public void draw(Canvas canvas) { + if (mDrawFinished && !isAboveParent()) { + // draw() is not called when SKIP_DRAW is set + if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { + // punch a whole in the view-hierarchy below us + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + } + super.draw(canvas); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mDrawFinished && !isAboveParent()) { + // draw() is not called when SKIP_DRAW is set + if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { + // punch a whole in the view-hierarchy below us + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + } + super.dispatchDraw(canvas); + } + + /** + * Control whether the surface view's surface is placed on top of another + * regular surface view in the window (but still behind the window itself). + * This is typically used to place overlays on top of an underlying media + * surface view. + * + * <p>Note that this must be set before the surface view's containing + * window is attached to the window manager. + * + * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. + */ public void setZOrderMediaOverlay(boolean isMediaOverlay) { + mSubLayer = isMediaOverlay + ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER; } + /** + * Control whether the surface view's surface is placed on top of its + * window. Normally it is placed behind the window, to allow it to + * (for the most part) appear to composite with the views in the + * hierarchy. By setting this, you cause it to be placed above the + * window. This means that none of the contents of the window this + * SurfaceView is in will be visible on top of its surface. + * + * <p>Note that this must be set before the surface view's containing + * window is attached to the window manager. + * + * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + */ public void setZOrderOnTop(boolean onTop) { + if (onTop) { + mSubLayer = APPLICATION_PANEL_SUBLAYER; + } else { + mSubLayer = APPLICATION_MEDIA_SUBLAYER; + } } + /** + * Control whether the surface view's content should be treated as secure, + * preventing it from appearing in screenshots or from being viewed on + * non-secure displays. + * + * <p>Note that this must be set before the surface view's containing + * window is attached to the window manager. + * + * <p>See {@link android.view.Display#FLAG_SECURE} for details. + * + * @param isSecure True if the surface view is secure. + */ public void setSecure(boolean isSecure) { + if (isSecure) { + mSurfaceFlags |= SurfaceControl.SECURE; + } else { + mSurfaceFlags &= ~SurfaceControl.SECURE; + } } - public SurfaceHolder getHolder() { - return mSurfaceHolder; + private void updateOpaqueFlag() { + if (!PixelFormat.formatHasAlpha(mRequestedFormat)) { + mSurfaceFlags |= SurfaceControl.OPAQUE; + } else { + mSurfaceFlags &= ~SurfaceControl.OPAQUE; + } } - private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + private Rect getParentSurfaceInsets() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } else { + return root.mWindowAttributes.surfaceInsets; + } + } + + /** @hide */ + protected void updateSurface() { + if (!mHaveFrame) { + return; + } + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) { + return; + } + + mTranslator = viewRoot.mTranslator; + if (mTranslator != null) { + mSurface.setCompatibilityTranslator(mTranslator); + } + + int myWidth = mRequestedWidth; + if (myWidth <= 0) myWidth = getWidth(); + int myHeight = mRequestedHeight; + if (myHeight <= 0) myHeight = getHeight(); + + final boolean formatChanged = mFormat != mRequestedFormat; + final boolean visibleChanged = mVisible != mRequestedVisible; + final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged) + && mRequestedVisible; + final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight; + final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility; + boolean redrawNeeded = false; + + if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) { + getLocationInWindow(mLocation); + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "Changes: creating=" + creating + + " format=" + formatChanged + " size=" + sizeChanged + + " visible=" + visibleChanged + + " left=" + (mWindowSpaceLeft != mLocation[0]) + + " top=" + (mWindowSpaceTop != mLocation[1])); + + try { + final boolean visible = mVisible = mRequestedVisible; + mWindowSpaceLeft = mLocation[0]; + mWindowSpaceTop = mLocation[1]; + mSurfaceWidth = myWidth; + mSurfaceHeight = myHeight; + mFormat = mRequestedFormat; + mLastWindowVisibility = mWindowVisibility; + + mScreenRect.left = mWindowSpaceLeft; + mScreenRect.top = mWindowSpaceTop; + mScreenRect.right = mWindowSpaceLeft + getWidth(); + mScreenRect.bottom = mWindowSpaceTop + getHeight(); + if (mTranslator != null) { + mTranslator.translateRectInAppWindowToScreen(mScreenRect); + } + + final Rect surfaceInsets = getParentSurfaceInsets(); + mScreenRect.offset(surfaceInsets.left, surfaceInsets.top); + + if (creating) { + mSurfaceSession = new SurfaceSession(viewRoot.mSurface); + mDeferredDestroySurfaceControl = mSurfaceControl; + + updateOpaqueFlag(); + mSurfaceControl = new SurfaceControlWithBackground(mSurfaceSession, + "SurfaceView - " + viewRoot.getTitle().toString(), + mSurfaceWidth, mSurfaceHeight, mFormat, + mSurfaceFlags); + } else if (mSurfaceControl == null) { + return; + } + + boolean realSizeChanged = false; + + mSurfaceLock.lock(); + try { + mDrawingStopped = !visible; + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "Cur surface: " + mSurface); + + SurfaceControl.openTransaction(); + try { + mSurfaceControl.setLayer(mSubLayer); + if (mViewVisibility) { + mSurfaceControl.show(); + } else { + mSurfaceControl.hide(); + } + + // While creating the surface, we will set it's initial + // geometry. Outside of that though, we should generally + // leave it to the RenderThread. + // + // There is one more case when the buffer size changes we aren't yet + // prepared to sync (as even following the transaction applying + // we still need to latch a buffer). + // b/28866173 + if (sizeChanged || creating || !mRtHandlingPositionUpdates) { + mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top); + mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth, + 0.0f, 0.0f, + mScreenRect.height() / (float) mSurfaceHeight); + } + if (sizeChanged) { + mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight); + } + } finally { + SurfaceControl.closeTransaction(); + } + + if (sizeChanged || creating) { + redrawNeeded = true; + } + + mSurfaceFrame.left = 0; + mSurfaceFrame.top = 0; + if (mTranslator == null) { + mSurfaceFrame.right = mSurfaceWidth; + mSurfaceFrame.bottom = mSurfaceHeight; + } else { + float appInvertedScale = mTranslator.applicationInvertedScale; + mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); + mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); + } + + final int surfaceWidth = mSurfaceFrame.right; + final int surfaceHeight = mSurfaceFrame.bottom; + realSizeChanged = mLastSurfaceWidth != surfaceWidth + || mLastSurfaceHeight != surfaceHeight; + mLastSurfaceWidth = surfaceWidth; + mLastSurfaceHeight = surfaceHeight; + } finally { + mSurfaceLock.unlock(); + } + + try { + redrawNeeded |= visible && !mDrawFinished; + + SurfaceHolder.Callback callbacks[] = null; + + final boolean surfaceChanged = creating; + if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) { + mSurfaceCreated = false; + if (mSurface.isValid()) { + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "visibleChanged -- surfaceDestroyed"); + callbacks = getSurfaceCallbacks(); + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceDestroyed(mSurfaceHolder); + } + // Since Android N the same surface may be reused and given to us + // again by the system server at a later point. However + // as we didn't do this in previous releases, clients weren't + // necessarily required to clean up properly in + // surfaceDestroyed. This leads to problems for example when + // clients don't destroy their EGL context, and try + // and create a new one on the same surface following reuse. + // Since there is no valid use of the surface in-between + // surfaceDestroyed and surfaceCreated, we force a disconnect, + // so the next connect will always work if we end up reusing + // the surface. + if (mSurface.isValid()) { + mSurface.forceScopedDisconnect(); + } + } + } + + if (creating) { + mSurface.copyFrom(mSurfaceControl); + } + + if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion + < Build.VERSION_CODES.O) { + // Some legacy applications use the underlying native {@link Surface} object + // as a key to whether anything has changed. In these cases, updates to the + // existing {@link Surface} will be ignored when the size changes. + // Therefore, we must explicitly recreate the {@link Surface} in these + // cases. + mSurface.createFrom(mSurfaceControl); + } + + if (visible && mSurface.isValid()) { + if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) { + mSurfaceCreated = true; + mIsCreating = true; + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "visibleChanged -- surfaceCreated"); + if (callbacks == null) { + callbacks = getSurfaceCallbacks(); + } + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceCreated(mSurfaceHolder); + } + } + if (creating || formatChanged || sizeChanged + || visibleChanged || realSizeChanged) { + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "surfaceChanged -- format=" + mFormat + + " w=" + myWidth + " h=" + myHeight); + if (callbacks == null) { + callbacks = getSurfaceCallbacks(); + } + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight); + } + } + if (redrawNeeded) { + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "surfaceRedrawNeeded"); + if (callbacks == null) { + callbacks = getSurfaceCallbacks(); + } + + mPendingReportDraws++; + viewRoot.drawPending(); + SurfaceCallbackHelper sch = + new SurfaceCallbackHelper(this::onDrawFinished); + sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks); + } + } + } finally { + mIsCreating = false; + if (mSurfaceControl != null && !mSurfaceCreated) { + mSurface.release(); + // If we are not in the stopped state, then the destruction of the Surface + // represents a visual change we need to display, and we should go ahead + // and destroy the SurfaceControl. However if we are in the stopped state, + // we can just leave the Surface around so it can be a part of animations, + // and we let the life-time be tied to the parent surface. + if (!mWindowStopped) { + mSurfaceControl.destroy(); + mSurfaceControl = null; + } + } + } + } catch (Exception ex) { + Log.e(TAG, "Exception configuring surface", ex); + } + if (DEBUG) Log.v( + TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top + + " w=" + mScreenRect.width() + " h=" + mScreenRect.height() + + ", frame=" + mSurfaceFrame); + } else { + // Calculate the window position in case RT loses the window + // and we need to fallback to a UI-thread driven position update + getLocationInSurface(mLocation); + final boolean positionChanged = mWindowSpaceLeft != mLocation[0] + || mWindowSpaceTop != mLocation[1]; + final boolean layoutSizeChanged = getWidth() != mScreenRect.width() + || getHeight() != mScreenRect.height(); + if (positionChanged || layoutSizeChanged) { // Only the position has changed + mWindowSpaceLeft = mLocation[0]; + mWindowSpaceTop = mLocation[1]; + // For our size changed check, we keep mScreenRect.width() and mScreenRect.height() + // in view local space. + mLocation[0] = getWidth(); + mLocation[1] = getHeight(); + + mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop, + mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]); + + if (mTranslator != null) { + mTranslator.translateRectInAppWindowToScreen(mScreenRect); + } + + if (mSurfaceControl == null) { + return; + } + + if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) { + try { + if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " + + "postion = [%d, %d, %d, %d]", System.identityHashCode(this), + mScreenRect.left, mScreenRect.top, + mScreenRect.right, mScreenRect.bottom)); + setParentSpaceRectangle(mScreenRect, -1); + } catch (Exception ex) { + Log.e(TAG, "Exception configuring surface", ex); + } + } + } + } + } + + private void onDrawFinished() { + if (DEBUG) { + Log.i(TAG, System.identityHashCode(this) + " " + + "finishedDrawing"); + } + + if (mDeferredDestroySurfaceControl != null) { + mDeferredDestroySurfaceControl.destroy(); + mDeferredDestroySurfaceControl = null; + } + + runOnUiThread(() -> { + performDrawFinished(); + }); + } + + private void setParentSpaceRectangle(Rect position, long frameNumber) { + ViewRootImpl viewRoot = getViewRootImpl(); + + SurfaceControl.openTransaction(); + try { + if (frameNumber > 0) { + mSurfaceControl.deferTransactionUntil(viewRoot.mSurface, frameNumber); + } + mSurfaceControl.setPosition(position.left, position.top); + mSurfaceControl.setMatrix(position.width() / (float) mSurfaceWidth, + 0.0f, 0.0f, + position.height() / (float) mSurfaceHeight); + } finally { + SurfaceControl.closeTransaction(); + } + } + + private Rect mRTLastReportedPosition = new Rect(); + + /** + * Called by native by a Rendering Worker thread to update the window position + * @hide + */ + public final void updateSurfacePosition_renderWorker(long frameNumber, + int left, int top, int right, int bottom) { + if (mSurfaceControl == null) { + return; + } + + // TODO: This is teensy bit racey in that a brand new SurfaceView moving on + // its 2nd frame if RenderThread is running slowly could potentially see + // this as false, enter the branch, get pre-empted, then this comes along + // and reports a new position, then the UI thread resumes and reports + // its position. This could therefore be de-sync'd in that interval, but + // the synchronization would violate the rule that RT must never block + // on the UI thread which would open up potential deadlocks. The risk of + // a single-frame desync is therefore preferable for now. + mRtHandlingPositionUpdates = true; + if (mRTLastReportedPosition.left == left + && mRTLastReportedPosition.top == top + && mRTLastReportedPosition.right == right + && mRTLastReportedPosition.bottom == bottom) { + return; + } + try { + if (DEBUG) { + Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " + + "postion = [%d, %d, %d, %d]", System.identityHashCode(this), + frameNumber, left, top, right, bottom)); + } + mRTLastReportedPosition.set(left, top, right, bottom); + setParentSpaceRectangle(mRTLastReportedPosition, frameNumber); + // Now overwrite mRTLastReportedPosition with our values + } catch (Exception ex) { + Log.e(TAG, "Exception from repositionChild", ex); + } + } + + /** + * Called by native on RenderThread to notify that the view is no longer in the + * draw tree. UI thread is blocked at this point. + * @hide + */ + public final void surfacePositionLost_uiRtSync(long frameNumber) { + if (DEBUG) { + Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d", + System.identityHashCode(this), frameNumber)); + } + mRTLastReportedPosition.setEmpty(); + + if (mSurfaceControl == null) { + return; + } + if (mRtHandlingPositionUpdates) { + mRtHandlingPositionUpdates = false; + // This callback will happen while the UI thread is blocked, so we can + // safely access other member variables at this time. + // So do what the UI thread would have done if RT wasn't handling position + // updates. + if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) { + try { + if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " + + "postion = [%d, %d, %d, %d]", System.identityHashCode(this), + mScreenRect.left, mScreenRect.top, + mScreenRect.right, mScreenRect.bottom)); + setParentSpaceRectangle(mScreenRect, frameNumber); + } catch (Exception ex) { + Log.e(TAG, "Exception configuring surface", ex); + } + } + } + } + + private SurfaceHolder.Callback[] getSurfaceCallbacks() { + SurfaceHolder.Callback callbacks[]; + synchronized (mCallbacks) { + callbacks = new SurfaceHolder.Callback[mCallbacks.size()]; + mCallbacks.toArray(callbacks); + } + return callbacks; + } + + /** + * This method still exists only for compatibility reasons because some applications have relied + * on this method via reflection. See Issue 36345857 for details. + * + * @deprecated No platform code is using this method anymore. + * @hide + */ + @Deprecated + public void setWindowType(int type) { + if (getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) { + throw new UnsupportedOperationException( + "SurfaceView#setWindowType() has never been a public API."); + } + + if (type == TYPE_APPLICATION_PANEL) { + Log.e(TAG, "If you are calling SurfaceView#setWindowType(TYPE_APPLICATION_PANEL) " + + "just to make the SurfaceView to be placed on top of its window, you must " + + "call setZOrderOnTop(true) instead.", new Throwable()); + setZOrderOnTop(true); + return; + } + Log.e(TAG, "SurfaceView#setWindowType(int) is deprecated and now does nothing. " + + "type=" + type, new Throwable()); + } + + private void runOnUiThread(Runnable runnable) { + Handler handler = getHandler(); + if (handler != null && handler.getLooper() != Looper.myLooper()) { + handler.post(runnable); + } else { + runnable.run(); + } + } + + /** + * Check to see if the surface has fixed size dimensions or if the surface's + * dimensions are dimensions are dependent on its current layout. + * + * @return true if the surface has dimensions that are fixed in size + * @hide + */ + public boolean isFixedSize() { + return (mRequestedWidth != -1 || mRequestedHeight != -1); + } + + private boolean isAboveParent() { + return mSubLayer >= 0; + } + + private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + private static final String LOG_TAG = "SurfaceHolder"; @Override public boolean isCreating() { - return false; + return mIsCreating; } @Override public void addCallback(Callback callback) { + synchronized (mCallbacks) { + // This is a linear search, but in practice we'll + // have only a couple callbacks, so it doesn't matter. + if (mCallbacks.contains(callback) == false) { + mCallbacks.add(callback); + } + } } @Override public void removeCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.remove(callback); + } } @Override public void setFixedSize(int width, int height) { + if (mRequestedWidth != width || mRequestedHeight != height) { + mRequestedWidth = width; + mRequestedHeight = height; + requestLayout(); + } } @Override public void setSizeFromLayout() { + if (mRequestedWidth != -1 || mRequestedHeight != -1) { + mRequestedWidth = mRequestedHeight = -1; + requestLayout(); + } } @Override public void setFormat(int format) { + // for backward compatibility reason, OPAQUE always + // means 565 for SurfaceView + if (format == PixelFormat.OPAQUE) + format = PixelFormat.RGB_565; + + mRequestedFormat = format; + if (mSurfaceControl != null) { + updateSurface(); + } } + /** + * @deprecated setType is now ignored. + */ @Override - public void setType(int type) { - } + @Deprecated + public void setType(int type) { } @Override public void setKeepScreenOn(boolean screenOn) { + runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn)); } + /** + * Gets a {@link Canvas} for drawing into the SurfaceView's Surface + * + * After drawing into the provided {@link Canvas}, the caller must + * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. + * + * The caller must redraw the entire surface. + * @return A canvas for drawing into the surface. + */ @Override public Canvas lockCanvas() { - return null; + return internalLockCanvas(null, false); } + /** + * Gets a {@link Canvas} for drawing into the SurfaceView's Surface + * + * After drawing into the provided {@link Canvas}, the caller must + * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. + * + * @param inOutDirty A rectangle that represents the dirty region that the caller wants + * to redraw. This function may choose to expand the dirty rectangle if for example + * the surface has been resized or if the previous contents of the surface were + * not available. The caller must redraw the entire dirty region as represented + * by the contents of the inOutDirty rectangle upon return from this function. + * The caller may also pass <code>null</code> instead, in the case where the + * entire surface should be redrawn. + * @return A canvas for drawing into the surface. + */ @Override - public Canvas lockCanvas(Rect dirty) { + public Canvas lockCanvas(Rect inOutDirty) { + return internalLockCanvas(inOutDirty, false); + } + + @Override + public Canvas lockHardwareCanvas() { + return internalLockCanvas(null, true); + } + + private Canvas internalLockCanvas(Rect dirty, boolean hardware) { + mSurfaceLock.lock(); + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped=" + + mDrawingStopped + ", surfaceControl=" + mSurfaceControl); + + Canvas c = null; + if (!mDrawingStopped && mSurfaceControl != null) { + try { + if (hardware) { + c = mSurface.lockHardwareCanvas(); + } else { + c = mSurface.lockCanvas(dirty); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Exception locking surface", e); + } + } + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c); + if (c != null) { + mLastLockTime = SystemClock.uptimeMillis(); + return c; + } + + // If the Surface is not ready to be drawn, then return null, + // but throttle calls to this function so it isn't called more + // than every 100ms. + long now = SystemClock.uptimeMillis(); + long nextTime = mLastLockTime + 100; + if (nextTime > now) { + try { + Thread.sleep(nextTime-now); + } catch (InterruptedException e) { + } + now = SystemClock.uptimeMillis(); + } + mLastLockTime = now; + mSurfaceLock.unlock(); + return null; } + /** + * Posts the new contents of the {@link Canvas} to the surface and + * releases the {@link Canvas}. + * + * @param canvas The canvas previously obtained from {@link #lockCanvas}. + */ @Override public void unlockCanvasAndPost(Canvas canvas) { + mSurface.unlockCanvasAndPost(canvas); + mSurfaceLock.unlock(); } @Override public Surface getSurface() { - return null; + return mSurface; } @Override public Rect getSurfaceFrame() { - return null; + return mSurfaceFrame; } }; -} + class SurfaceControlWithBackground extends SurfaceControl { + private SurfaceControl mBackgroundControl; + private boolean mOpaque = true; + public boolean mVisible = false; + + public SurfaceControlWithBackground(SurfaceSession s, + String name, int w, int h, int format, int flags) + throws Exception { + super(s, name, w, h, format, flags); + mBackgroundControl = new SurfaceControl(s, "Background for - " + name, w, h, + PixelFormat.OPAQUE, flags | SurfaceControl.FX_SURFACE_DIM); + mOpaque = (flags & SurfaceControl.OPAQUE) != 0; + } + + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + mBackgroundControl.setAlpha(alpha); + } + + @Override + public void setLayer(int zorder) { + super.setLayer(zorder); + // -3 is below all other child layers as SurfaceView never goes below -2 + mBackgroundControl.setLayer(-3); + } + + @Override + public void setPosition(float x, float y) { + super.setPosition(x, y); + mBackgroundControl.setPosition(x, y); + } + + @Override + public void setSize(int w, int h) { + super.setSize(w, h); + mBackgroundControl.setSize(w, h); + } + + @Override + public void setWindowCrop(Rect crop) { + super.setWindowCrop(crop); + mBackgroundControl.setWindowCrop(crop); + } + + @Override + public void setFinalCrop(Rect crop) { + super.setFinalCrop(crop); + mBackgroundControl.setFinalCrop(crop); + } + + @Override + public void setLayerStack(int layerStack) { + super.setLayerStack(layerStack); + mBackgroundControl.setLayerStack(layerStack); + } + + @Override + public void setOpaque(boolean isOpaque) { + super.setOpaque(isOpaque); + mOpaque = isOpaque; + updateBackgroundVisibility(); + } + + @Override + public void setSecure(boolean isSecure) { + super.setSecure(isSecure); + } + + @Override + public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { + super.setMatrix(dsdx, dtdx, dsdy, dtdy); + mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy); + } + + @Override + public void hide() { + super.hide(); + mVisible = false; + updateBackgroundVisibility(); + } + + @Override + public void show() { + super.show(); + mVisible = true; + updateBackgroundVisibility(); + } + + @Override + public void destroy() { + super.destroy(); + mBackgroundControl.destroy(); + } + + @Override + public void release() { + super.release(); + mBackgroundControl.release(); + } + + @Override + public void setTransparentRegionHint(Region region) { + super.setTransparentRegionHint(region); + mBackgroundControl.setTransparentRegionHint(region); + } + + @Override + public void deferTransactionUntil(IBinder handle, long frame) { + super.deferTransactionUntil(handle, frame); + mBackgroundControl.deferTransactionUntil(handle, frame); + } + + @Override + public void deferTransactionUntil(Surface barrier, long frame) { + super.deferTransactionUntil(barrier, frame); + mBackgroundControl.deferTransactionUntil(barrier, frame); + } + + void updateBackgroundVisibility() { + if (mOpaque && mVisible) { + mBackgroundControl.show(); + } else { + mBackgroundControl.hide(); + } + } + } +} diff --git a/android/view/View.java b/android/view/View.java index e5bd5ac0..b6be2961 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -127,6 +127,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -1078,6 +1079,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE}</code>). * + * <p>When annotating a view with this hint, it's recommended to use a date autofill value to + * avoid ambiguity when the autofill service provides a value for it. To understand why a + * value can be ambiguous, consider "April of 2020", which could be represented as either of + * the following options: + * + * <ul> + * <li>{@code "04/2020"} + * <li>{@code "4/2020"} + * <li>{@code "2020/04"} + * <li>{@code "2020/4"} + * <li>{@code "April/2020"} + * <li>{@code "Apr/2020"} + * </ul> + * + * <p>You define a date autofill value for the view by overriding the following methods: + * + * <ol> + * <li>{@link #getAutofillType()} to return {@link #AUTOFILL_TYPE_DATE}. + * <li>{@link #getAutofillValue()} to return a + * {@link AutofillValue#forDate(long) date autofillvalue}. + * <li>{@link #autofill(AutofillValue)} to expect a data autofillvalue. + * </ol> + * * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints. */ public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE = @@ -1090,6 +1114,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH}</code>). * + * <p>When annotating a view with this hint, it's recommended to use a text autofill value + * whose value is the numerical representation of the month, starting on {@code 1} to avoid + * ambiguity when the autofill service provides a value for it. To understand why a + * value can be ambiguous, consider "January", which could be represented as either of + * + * <ul> + * <li>{@code "1"}: recommended way. + * <li>{@code "0"}: if following the {@link Calendar#MONTH} convention. + * <li>{@code "January"}: full name, in English. + * <li>{@code "jan"}: abbreviated name, in English. + * <li>{@code "Janeiro"}: full name, in another language. + * </ul> + * + * <p>Another recommended approach is to use a date autofill value - see + * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE} for more details. + * * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints. */ public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = @@ -3702,15 +3742,90 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @ViewDebug.ExportedProperty(flagMapping = { - @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LOW_PROFILE, - equals = SYSTEM_UI_FLAG_LOW_PROFILE, - name = "SYSTEM_UI_FLAG_LOW_PROFILE", outputIf = true), - @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_HIDE_NAVIGATION, - equals = SYSTEM_UI_FLAG_HIDE_NAVIGATION, - name = "SYSTEM_UI_FLAG_HIDE_NAVIGATION", outputIf = true), - @ViewDebug.FlagToString(mask = PUBLIC_STATUS_BAR_VISIBILITY_MASK, - equals = SYSTEM_UI_FLAG_VISIBLE, - name = "SYSTEM_UI_FLAG_VISIBLE", outputIf = true) + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LOW_PROFILE, + equals = SYSTEM_UI_FLAG_LOW_PROFILE, + name = "LOW_PROFILE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_HIDE_NAVIGATION, + equals = SYSTEM_UI_FLAG_HIDE_NAVIGATION, + name = "HIDE_NAVIGATION"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_FULLSCREEN, + equals = SYSTEM_UI_FLAG_FULLSCREEN, + name = "FULLSCREEN"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_STABLE, + equals = SYSTEM_UI_FLAG_LAYOUT_STABLE, + name = "LAYOUT_STABLE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION, + equals = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION, + name = "LAYOUT_HIDE_NAVIGATION"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + equals = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + name = "LAYOUT_FULLSCREEN"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_IMMERSIVE, + equals = SYSTEM_UI_FLAG_IMMERSIVE, + name = "IMMERSIVE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_IMMERSIVE_STICKY, + equals = SYSTEM_UI_FLAG_IMMERSIVE_STICKY, + name = "IMMERSIVE_STICKY"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + equals = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + name = "LIGHT_STATUS_BAR"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, + equals = SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, + name = "LIGHT_NAVIGATION_BAR"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_EXPAND, + equals = STATUS_BAR_DISABLE_EXPAND, + name = "STATUS_BAR_DISABLE_EXPAND"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_ICONS, + equals = STATUS_BAR_DISABLE_NOTIFICATION_ICONS, + name = "STATUS_BAR_DISABLE_NOTIFICATION_ICONS"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_ALERTS, + equals = STATUS_BAR_DISABLE_NOTIFICATION_ALERTS, + name = "STATUS_BAR_DISABLE_NOTIFICATION_ALERTS"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_TICKER, + equals = STATUS_BAR_DISABLE_NOTIFICATION_TICKER, + name = "STATUS_BAR_DISABLE_NOTIFICATION_TICKER"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_SYSTEM_INFO, + equals = STATUS_BAR_DISABLE_SYSTEM_INFO, + name = "STATUS_BAR_DISABLE_SYSTEM_INFO"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_HOME, + equals = STATUS_BAR_DISABLE_HOME, + name = "STATUS_BAR_DISABLE_HOME"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_BACK, + equals = STATUS_BAR_DISABLE_BACK, + name = "STATUS_BAR_DISABLE_BACK"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_CLOCK, + equals = STATUS_BAR_DISABLE_CLOCK, + name = "STATUS_BAR_DISABLE_CLOCK"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_RECENT, + equals = STATUS_BAR_DISABLE_RECENT, + name = "STATUS_BAR_DISABLE_RECENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_SEARCH, + equals = STATUS_BAR_DISABLE_SEARCH, + name = "STATUS_BAR_DISABLE_SEARCH"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSIENT, + equals = STATUS_BAR_TRANSIENT, + name = "STATUS_BAR_TRANSIENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSIENT, + equals = NAVIGATION_BAR_TRANSIENT, + name = "NAVIGATION_BAR_TRANSIENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_UNHIDE, + equals = STATUS_BAR_UNHIDE, + name = "STATUS_BAR_UNHIDE"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_UNHIDE, + equals = NAVIGATION_BAR_UNHIDE, + name = "NAVIGATION_BAR_UNHIDE"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSLUCENT, + equals = STATUS_BAR_TRANSLUCENT, + name = "STATUS_BAR_TRANSLUCENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSLUCENT, + equals = NAVIGATION_BAR_TRANSLUCENT, + name = "NAVIGATION_BAR_TRANSLUCENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSPARENT, + equals = NAVIGATION_BAR_TRANSPARENT, + name = "NAVIGATION_BAR_TRANSPARENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSPARENT, + equals = STATUS_BAR_TRANSPARENT, + name = "STATUS_BAR_TRANSPARENT") }, formatToHexString = true) int mSystemUiVisibility; @@ -15414,7 +15529,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * {@code dirty}. * * @param dirty the rectangle representing the bounds of the dirty region + * + * @deprecated The switch to hardware accelerated rendering in API 14 reduced + * the importance of the dirty rectangle. In API 21 the given rectangle is + * ignored entirely in favor of an internally-calculated area instead. + * Because of this, clients are encouraged to just call {@link #invalidate()}. */ + @Deprecated public void invalidate(Rect dirty) { final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -15435,7 +15556,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param t the top position of the dirty region * @param r the right position of the dirty region * @param b the bottom position of the dirty region + * + * @deprecated The switch to hardware accelerated rendering in API 14 reduced + * the importance of the dirty rectangle. In API 21 the given rectangle is + * ignored entirely in favor of an internally-calculated area instead. + * Because of this, clients are encouraged to just call {@link #invalidate()}. */ + @Deprecated public void invalidate(int l, int t, int r, int b) { final int scrollX = mScrollX; final int scrollY = mScrollY; diff --git a/android/view/ViewDebug.java b/android/view/ViewDebug.java index 66c05785..3426485e 100644 --- a/android/view/ViewDebug.java +++ b/android/view/ViewDebug.java @@ -1375,6 +1375,81 @@ public class ViewDebug { } } + /** + * Converts an integer from a field that is mapped with {@link IntToString} to its string + * representation. + * + * @param clazz The class the field is defined on. + * @param field The field on which the {@link ExportedProperty} is defined on. + * @param integer The value to convert. + * @return The value converted into its string representation. + * @hide + */ + public static String intToString(Class<?> clazz, String field, int integer) { + final IntToString[] mapping = getMapping(clazz, field); + if (mapping == null) { + return Integer.toString(integer); + } + final int count = mapping.length; + for (int j = 0; j < count; j++) { + final IntToString map = mapping[j]; + if (map.from() == integer) { + return map.to(); + } + } + return Integer.toString(integer); + } + + /** + * Converts a set of flags from a field that is mapped with {@link FlagToString} to its string + * representation. + * + * @param clazz The class the field is defined on. + * @param field The field on which the {@link ExportedProperty} is defined on. + * @param flags The flags to convert. + * @return The flags converted into their string representations. + * @hide + */ + public static String flagsToString(Class<?> clazz, String field, int flags) { + final FlagToString[] mapping = getFlagMapping(clazz, field); + if (mapping == null) { + return Integer.toHexString(flags); + } + final StringBuilder result = new StringBuilder(); + final int count = mapping.length; + for (int j = 0; j < count; j++) { + final FlagToString flagMapping = mapping[j]; + final boolean ifTrue = flagMapping.outputIf(); + final int maskResult = flags & flagMapping.mask(); + final boolean test = maskResult == flagMapping.equals(); + if (test && ifTrue) { + final String name = flagMapping.name(); + result.append(name).append(' '); + } + } + if (result.length() > 0) { + result.deleteCharAt(result.length() - 1); + } + return result.toString(); + } + + private static FlagToString[] getFlagMapping(Class<?> clazz, String field) { + try { + return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class) + .flagMapping(); + } catch (NoSuchFieldException e) { + return null; + } + } + + private static IntToString[] getMapping(Class<?> clazz, String field) { + try { + return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping(); + } catch (NoSuchFieldException e) { + return null; + } + } + private static void exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix) throws IOException { diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 415aad54..71106ada 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -366,7 +366,7 @@ public final class ViewRootImpl implements ViewParent, // These can be accessed by any thread, must be protected with a lock. // Surface can never be reassigned or cleared (use Surface.clear()). - final Surface mSurface = new Surface(); + public final Surface mSurface = new Surface(); boolean mAdded; boolean mAddedTouchMode; @@ -512,7 +512,7 @@ public final class ViewRootImpl implements ViewParent, mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); if (!sCompatibilityDone) { - sAlwaysAssignFocus = true; + sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P; sCompatibilityDone = true; } @@ -7714,7 +7714,7 @@ public final class ViewRootImpl implements ViewParent, public void onAccessibilityStateChanged(boolean enabled) { if (enabled) { ensureConnection(); - if (mAttachInfo.mHasWindowFocus) { + if (mAttachInfo.mHasWindowFocus && (mView != null)) { mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); View focusedView = mView.findFocus(); if (focusedView != null && focusedView != mView) { diff --git a/android/view/ViewStructure.java b/android/view/ViewStructure.java index 0ecd20da..f671c349 100644 --- a/android/view/ViewStructure.java +++ b/android/view/ViewStructure.java @@ -378,7 +378,7 @@ public abstract class ViewStructure { * * <p>Typically used when the view is a container for an HTML document. * - * @param domain URL representing the domain; only the host part will be used. + * @param domain RFC 2396-compliant URI representing the domain. */ public abstract void setWebDomain(@Nullable String domain); diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java index e56a82ff..c29a1daf 100644 --- a/android/view/WindowManager.java +++ b/android/view/WindowManager.java @@ -16,6 +16,8 @@ package android.view; +import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT; + import android.Manifest.permission; import android.annotation.IntDef; import android.annotation.NonNull; @@ -268,93 +270,93 @@ public interface WindowManager extends ViewManager { */ @ViewDebug.ExportedProperty(mapping = { @ViewDebug.IntToString(from = TYPE_BASE_APPLICATION, - to = "TYPE_BASE_APPLICATION"), + to = "BASE_APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION, - to = "TYPE_APPLICATION"), + to = "APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION_STARTING, - to = "TYPE_APPLICATION_STARTING"), + to = "APPLICATION_STARTING"), @ViewDebug.IntToString(from = TYPE_DRAWN_APPLICATION, - to = "TYPE_DRAWN_APPLICATION"), + to = "DRAWN_APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION_PANEL, - to = "TYPE_APPLICATION_PANEL"), + to = "APPLICATION_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA, - to = "TYPE_APPLICATION_MEDIA"), + to = "APPLICATION_MEDIA"), @ViewDebug.IntToString(from = TYPE_APPLICATION_SUB_PANEL, - to = "TYPE_APPLICATION_SUB_PANEL"), + to = "APPLICATION_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_ABOVE_SUB_PANEL, - to = "TYPE_APPLICATION_ABOVE_SUB_PANEL"), + to = "APPLICATION_ABOVE_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_ATTACHED_DIALOG, - to = "TYPE_APPLICATION_ATTACHED_DIALOG"), + to = "APPLICATION_ATTACHED_DIALOG"), @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA_OVERLAY, - to = "TYPE_APPLICATION_MEDIA_OVERLAY"), + to = "APPLICATION_MEDIA_OVERLAY"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR, - to = "TYPE_STATUS_BAR"), + to = "STATUS_BAR"), @ViewDebug.IntToString(from = TYPE_SEARCH_BAR, - to = "TYPE_SEARCH_BAR"), + to = "SEARCH_BAR"), @ViewDebug.IntToString(from = TYPE_PHONE, - to = "TYPE_PHONE"), + to = "PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ALERT, - to = "TYPE_SYSTEM_ALERT"), + to = "SYSTEM_ALERT"), @ViewDebug.IntToString(from = TYPE_TOAST, - to = "TYPE_TOAST"), + to = "TOAST"), @ViewDebug.IntToString(from = TYPE_SYSTEM_OVERLAY, - to = "TYPE_SYSTEM_OVERLAY"), + to = "SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRIORITY_PHONE, - to = "TYPE_PRIORITY_PHONE"), + to = "PRIORITY_PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_DIALOG, - to = "TYPE_SYSTEM_DIALOG"), + to = "SYSTEM_DIALOG"), @ViewDebug.IntToString(from = TYPE_KEYGUARD_DIALOG, - to = "TYPE_KEYGUARD_DIALOG"), + to = "KEYGUARD_DIALOG"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ERROR, - to = "TYPE_SYSTEM_ERROR"), + to = "SYSTEM_ERROR"), @ViewDebug.IntToString(from = TYPE_INPUT_METHOD, - to = "TYPE_INPUT_METHOD"), + to = "INPUT_METHOD"), @ViewDebug.IntToString(from = TYPE_INPUT_METHOD_DIALOG, - to = "TYPE_INPUT_METHOD_DIALOG"), + to = "INPUT_METHOD_DIALOG"), @ViewDebug.IntToString(from = TYPE_WALLPAPER, - to = "TYPE_WALLPAPER"), + to = "WALLPAPER"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR_PANEL, - to = "TYPE_STATUS_BAR_PANEL"), + to = "STATUS_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_SECURE_SYSTEM_OVERLAY, - to = "TYPE_SECURE_SYSTEM_OVERLAY"), + to = "SECURE_SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_DRAG, - to = "TYPE_DRAG"), + to = "DRAG"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR_SUB_PANEL, - to = "TYPE_STATUS_BAR_SUB_PANEL"), + to = "STATUS_BAR_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_POINTER, - to = "TYPE_POINTER"), + to = "POINTER"), @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR, - to = "TYPE_NAVIGATION_BAR"), + to = "NAVIGATION_BAR"), @ViewDebug.IntToString(from = TYPE_VOLUME_OVERLAY, - to = "TYPE_VOLUME_OVERLAY"), + to = "VOLUME_OVERLAY"), @ViewDebug.IntToString(from = TYPE_BOOT_PROGRESS, - to = "TYPE_BOOT_PROGRESS"), + to = "BOOT_PROGRESS"), @ViewDebug.IntToString(from = TYPE_INPUT_CONSUMER, - to = "TYPE_INPUT_CONSUMER"), + to = "INPUT_CONSUMER"), @ViewDebug.IntToString(from = TYPE_DREAM, - to = "TYPE_DREAM"), + to = "DREAM"), @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR_PANEL, - to = "TYPE_NAVIGATION_BAR_PANEL"), + to = "NAVIGATION_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_DISPLAY_OVERLAY, - to = "TYPE_DISPLAY_OVERLAY"), + to = "DISPLAY_OVERLAY"), @ViewDebug.IntToString(from = TYPE_MAGNIFICATION_OVERLAY, - to = "TYPE_MAGNIFICATION_OVERLAY"), + to = "MAGNIFICATION_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRESENTATION, - to = "TYPE_PRESENTATION"), + to = "PRESENTATION"), @ViewDebug.IntToString(from = TYPE_PRIVATE_PRESENTATION, - to = "TYPE_PRIVATE_PRESENTATION"), + to = "PRIVATE_PRESENTATION"), @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION, - to = "TYPE_VOICE_INTERACTION"), + to = "VOICE_INTERACTION"), @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION_STARTING, - to = "TYPE_VOICE_INTERACTION_STARTING"), + to = "VOICE_INTERACTION_STARTING"), @ViewDebug.IntToString(from = TYPE_DOCK_DIVIDER, - to = "TYPE_DOCK_DIVIDER"), + to = "DOCK_DIVIDER"), @ViewDebug.IntToString(from = TYPE_QS_DIALOG, - to = "TYPE_QS_DIALOG"), + to = "QS_DIALOG"), @ViewDebug.IntToString(from = TYPE_SCREENSHOT, - to = "TYPE_SCREENSHOT"), + to = "SCREENSHOT"), @ViewDebug.IntToString(from = TYPE_APPLICATION_OVERLAY, - to = "TYPE_APPLICATION_OVERLAY") + to = "APPLICATION_OVERLAY") }) public int type; @@ -1198,63 +1200,69 @@ public interface WindowManager extends ViewManager { */ @ViewDebug.ExportedProperty(flagMapping = { @ViewDebug.FlagToString(mask = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, equals = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, - name = "FLAG_ALLOW_LOCK_WHILE_SCREEN_ON"), + name = "ALLOW_LOCK_WHILE_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_DIM_BEHIND, equals = FLAG_DIM_BEHIND, - name = "FLAG_DIM_BEHIND"), + name = "DIM_BEHIND"), @ViewDebug.FlagToString(mask = FLAG_BLUR_BEHIND, equals = FLAG_BLUR_BEHIND, - name = "FLAG_BLUR_BEHIND"), + name = "BLUR_BEHIND"), @ViewDebug.FlagToString(mask = FLAG_NOT_FOCUSABLE, equals = FLAG_NOT_FOCUSABLE, - name = "FLAG_NOT_FOCUSABLE"), + name = "NOT_FOCUSABLE"), @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCHABLE, equals = FLAG_NOT_TOUCHABLE, - name = "FLAG_NOT_TOUCHABLE"), + name = "NOT_TOUCHABLE"), @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCH_MODAL, equals = FLAG_NOT_TOUCH_MODAL, - name = "FLAG_NOT_TOUCH_MODAL"), + name = "NOT_TOUCH_MODAL"), @ViewDebug.FlagToString(mask = FLAG_TOUCHABLE_WHEN_WAKING, equals = FLAG_TOUCHABLE_WHEN_WAKING, - name = "FLAG_TOUCHABLE_WHEN_WAKING"), + name = "TOUCHABLE_WHEN_WAKING"), @ViewDebug.FlagToString(mask = FLAG_KEEP_SCREEN_ON, equals = FLAG_KEEP_SCREEN_ON, - name = "FLAG_KEEP_SCREEN_ON"), + name = "KEEP_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_SCREEN, equals = FLAG_LAYOUT_IN_SCREEN, - name = "FLAG_LAYOUT_IN_SCREEN"), + name = "LAYOUT_IN_SCREEN"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_NO_LIMITS, equals = FLAG_LAYOUT_NO_LIMITS, - name = "FLAG_LAYOUT_NO_LIMITS"), + name = "LAYOUT_NO_LIMITS"), @ViewDebug.FlagToString(mask = FLAG_FULLSCREEN, equals = FLAG_FULLSCREEN, - name = "FLAG_FULLSCREEN"), + name = "FULLSCREEN"), @ViewDebug.FlagToString(mask = FLAG_FORCE_NOT_FULLSCREEN, equals = FLAG_FORCE_NOT_FULLSCREEN, - name = "FLAG_FORCE_NOT_FULLSCREEN"), + name = "FORCE_NOT_FULLSCREEN"), @ViewDebug.FlagToString(mask = FLAG_DITHER, equals = FLAG_DITHER, - name = "FLAG_DITHER"), + name = "DITHER"), @ViewDebug.FlagToString(mask = FLAG_SECURE, equals = FLAG_SECURE, - name = "FLAG_SECURE"), + name = "SECURE"), @ViewDebug.FlagToString(mask = FLAG_SCALED, equals = FLAG_SCALED, - name = "FLAG_SCALED"), + name = "SCALED"), @ViewDebug.FlagToString(mask = FLAG_IGNORE_CHEEK_PRESSES, equals = FLAG_IGNORE_CHEEK_PRESSES, - name = "FLAG_IGNORE_CHEEK_PRESSES"), + name = "IGNORE_CHEEK_PRESSES"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_INSET_DECOR, equals = FLAG_LAYOUT_INSET_DECOR, - name = "FLAG_LAYOUT_INSET_DECOR"), + name = "LAYOUT_INSET_DECOR"), @ViewDebug.FlagToString(mask = FLAG_ALT_FOCUSABLE_IM, equals = FLAG_ALT_FOCUSABLE_IM, - name = "FLAG_ALT_FOCUSABLE_IM"), + name = "ALT_FOCUSABLE_IM"), @ViewDebug.FlagToString(mask = FLAG_WATCH_OUTSIDE_TOUCH, equals = FLAG_WATCH_OUTSIDE_TOUCH, - name = "FLAG_WATCH_OUTSIDE_TOUCH"), + name = "WATCH_OUTSIDE_TOUCH"), @ViewDebug.FlagToString(mask = FLAG_SHOW_WHEN_LOCKED, equals = FLAG_SHOW_WHEN_LOCKED, - name = "FLAG_SHOW_WHEN_LOCKED"), + name = "SHOW_WHEN_LOCKED"), @ViewDebug.FlagToString(mask = FLAG_SHOW_WALLPAPER, equals = FLAG_SHOW_WALLPAPER, - name = "FLAG_SHOW_WALLPAPER"), + name = "SHOW_WALLPAPER"), @ViewDebug.FlagToString(mask = FLAG_TURN_SCREEN_ON, equals = FLAG_TURN_SCREEN_ON, - name = "FLAG_TURN_SCREEN_ON"), + name = "TURN_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_DISMISS_KEYGUARD, equals = FLAG_DISMISS_KEYGUARD, - name = "FLAG_DISMISS_KEYGUARD"), + name = "DISMISS_KEYGUARD"), @ViewDebug.FlagToString(mask = FLAG_SPLIT_TOUCH, equals = FLAG_SPLIT_TOUCH, - name = "FLAG_SPLIT_TOUCH"), + name = "SPLIT_TOUCH"), @ViewDebug.FlagToString(mask = FLAG_HARDWARE_ACCELERATED, equals = FLAG_HARDWARE_ACCELERATED, - name = "FLAG_HARDWARE_ACCELERATED"), - @ViewDebug.FlagToString(mask = FLAG_LOCAL_FOCUS_MODE, equals = FLAG_LOCAL_FOCUS_MODE, - name = "FLAG_LOCAL_FOCUS_MODE"), + name = "HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_OVERSCAN, equals = FLAG_LAYOUT_IN_OVERSCAN, + name = "LOCAL_FOCUS_MODE"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_STATUS, equals = FLAG_TRANSLUCENT_STATUS, - name = "FLAG_TRANSLUCENT_STATUS"), + name = "TRANSLUCENT_STATUS"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_NAVIGATION, equals = FLAG_TRANSLUCENT_NAVIGATION, - name = "FLAG_TRANSLUCENT_NAVIGATION"), + name = "TRANSLUCENT_NAVIGATION"), + @ViewDebug.FlagToString(mask = FLAG_LOCAL_FOCUS_MODE, equals = FLAG_LOCAL_FOCUS_MODE, + name = "LOCAL_FOCUS_MODE"), + @ViewDebug.FlagToString(mask = FLAG_SLIPPERY, equals = FLAG_SLIPPERY, + name = "FLAG_SLIPPERY"), + @ViewDebug.FlagToString(mask = FLAG_LAYOUT_ATTACHED_IN_DECOR, equals = FLAG_LAYOUT_ATTACHED_IN_DECOR, + name = "FLAG_LAYOUT_ATTACHED_IN_DECOR"), @ViewDebug.FlagToString(mask = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, equals = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - name = "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS") + name = "DRAWS_SYSTEM_BAR_BACKGROUNDS") }, formatToHexString = true) public int flags; @@ -1438,6 +1446,88 @@ public interface WindowManager extends ViewManager { * Control flags that are private to the platform. * @hide */ + @ViewDebug.ExportedProperty(flagMapping = { + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED, + equals = PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED, + name = "FAKE_HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED, + equals = PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED, + name = "FORCE_HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS, + equals = PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS, + name = "WANTS_OFFSET_NOTIFICATIONS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SHOW_FOR_ALL_USERS, + equals = PRIVATE_FLAG_SHOW_FOR_ALL_USERS, + name = "SHOW_FOR_ALL_USERS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_NO_MOVE_ANIMATION, + equals = PRIVATE_FLAG_NO_MOVE_ANIMATION, + name = "NO_MOVE_ANIMATION"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_COMPATIBLE_WINDOW, + equals = PRIVATE_FLAG_COMPATIBLE_WINDOW, + name = "COMPATIBLE_WINDOW"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SYSTEM_ERROR, + equals = PRIVATE_FLAG_SYSTEM_ERROR, + name = "SYSTEM_ERROR"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR, + equals = PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR, + name = "INHERIT_TRANSLUCENT_DECOR"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_KEYGUARD, + equals = PRIVATE_FLAG_KEYGUARD, + name = "KEYGUARD"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, + equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, + name = "DISABLE_WALLPAPER_TOUCH_EVENTS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT, + equals = PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT, + name = "FORCE_STATUS_BAR_VISIBLE_TRANSPARENT"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_PRESERVE_GEOMETRY, + equals = PRIVATE_FLAG_PRESERVE_GEOMETRY, + name = "PRESERVE_GEOMETRY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, + equals = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, + name = "FORCE_DECOR_VIEW_VISIBILITY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, + equals = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, + name = "WILL_NOT_REPLACE_ON_RELAUNCH"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, + equals = PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, + name = "LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND, + equals = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND, + name = "FORCE_DRAW_STATUS_BAR_BACKGROUND"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + name = "SUSTAINED_PERFORMANCE_MODE"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + equals = PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, + equals = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, + name = "IS_ROUNDED_CORNERS_OVERLAY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, + equals = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, + name = "ACQUIRES_SLEEP_TOKEN") + }) @TestApi public int privateFlags; @@ -1977,7 +2067,7 @@ public interface WindowManager extends ViewManager { * @hide */ @ActivityInfo.ColorMode - private int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; + private int mColorMode = COLOR_MODE_DEFAULT; public LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -2442,9 +2532,15 @@ public interface WindowManager extends ViewManager { @Override public String toString() { + return toString(""); + } + + /** + * @hide + */ + public String toString(String prefix) { StringBuilder sb = new StringBuilder(256); - sb.append("WM.LayoutParams{"); - sb.append("("); + sb.append("{("); sb.append(x); sb.append(','); sb.append(y); @@ -2464,26 +2560,19 @@ public interface WindowManager extends ViewManager { sb.append(verticalMargin); } if (gravity != 0) { - sb.append(" gr=#"); - sb.append(Integer.toHexString(gravity)); + sb.append(" gr="); + sb.append(Gravity.toString(gravity)); } if (softInputMode != 0) { - sb.append(" sim=#"); - sb.append(Integer.toHexString(softInputMode)); + sb.append(" sim={"); + sb.append(softInputModeToString(softInputMode)); + sb.append('}'); } sb.append(" ty="); - sb.append(type); - sb.append(" fl=#"); - sb.append(Integer.toHexString(flags)); - if (privateFlags != 0) { - if ((privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) { - sb.append(" compatible=true"); - } - sb.append(" pfl=0x").append(Integer.toHexString(privateFlags)); - } + sb.append(ViewDebug.intToString(LayoutParams.class, "type", type)); if (format != PixelFormat.OPAQUE) { sb.append(" fmt="); - sb.append(format); + sb.append(PixelFormat.formatToString(format)); } if (windowAnimations != 0) { sb.append(" wanim=0x"); @@ -2491,7 +2580,7 @@ public interface WindowManager extends ViewManager { } if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { sb.append(" or="); - sb.append(screenOrientation); + sb.append(ActivityInfo.screenOrientationToString(screenOrientation)); } if (alpha != 1.0f) { sb.append(" alpha="); @@ -2507,7 +2596,7 @@ public interface WindowManager extends ViewManager { } if (rotationAnimation != ROTATION_ANIMATION_ROTATE) { sb.append(" rotAnim="); - sb.append(rotationAnimation); + sb.append(rotationAnimationToString(rotationAnimation)); } if (preferredRefreshRate != 0) { sb.append(" preferredRefreshRate="); @@ -2517,20 +2606,12 @@ public interface WindowManager extends ViewManager { sb.append(" preferredDisplayMode="); sb.append(preferredDisplayModeId); } - if (systemUiVisibility != 0) { - sb.append(" sysui=0x"); - sb.append(Integer.toHexString(systemUiVisibility)); - } - if (subtreeSystemUiVisibility != 0) { - sb.append(" vsysui=0x"); - sb.append(Integer.toHexString(subtreeSystemUiVisibility)); - } if (hasSystemUiListeners) { sb.append(" sysuil="); sb.append(hasSystemUiListeners); } if (inputFeatures != 0) { - sb.append(" if=0x").append(Integer.toHexString(inputFeatures)); + sb.append(" if=").append(inputFeatureToString(inputFeatures)); } if (userActivityTimeout >= 0) { sb.append(" userActivityTimeout=").append(userActivityTimeout); @@ -2546,11 +2627,30 @@ public interface WindowManager extends ViewManager { sb.append(" (!preservePreviousSurfaceInsets)"); } } - if (needsMenuKey != NEEDS_MENU_UNSET) { - sb.append(" needsMenuKey="); - sb.append(needsMenuKey); + if (needsMenuKey == NEEDS_MENU_SET_TRUE) { + sb.append(" needsMenuKey"); + } + if (mColorMode != COLOR_MODE_DEFAULT) { + sb.append(" colorMode=").append(ActivityInfo.colorModeToString(mColorMode)); + } + sb.append(System.lineSeparator()); + sb.append(prefix).append(" fl=").append( + ViewDebug.flagsToString(LayoutParams.class, "flags", flags)); + if (privateFlags != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString( + LayoutParams.class, "privateFlags", privateFlags)); + } + if (systemUiVisibility != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" sysui=").append(ViewDebug.flagsToString( + View.class, "mSystemUiVisibility", systemUiVisibility)); + } + if (subtreeSystemUiVisibility != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" vsysui=").append(ViewDebug.flagsToString( + View.class, "mSystemUiVisibility", subtreeSystemUiVisibility)); } - sb.append(" colorMode=").append(mColorMode); sb.append('}'); return sb.toString(); } @@ -2634,5 +2734,88 @@ public interface WindowManager extends ViewManager { && width == WindowManager.LayoutParams.MATCH_PARENT && height == WindowManager.LayoutParams.MATCH_PARENT; } + + private static String softInputModeToString(@SoftInputModeFlags int softInputMode) { + final StringBuilder result = new StringBuilder(); + final int state = softInputMode & SOFT_INPUT_MASK_STATE; + if (state != 0) { + result.append("state="); + switch (state) { + case SOFT_INPUT_STATE_UNCHANGED: + result.append("unchanged"); + break; + case SOFT_INPUT_STATE_HIDDEN: + result.append("hidden"); + break; + case SOFT_INPUT_STATE_ALWAYS_HIDDEN: + result.append("always_hidden"); + break; + case SOFT_INPUT_STATE_VISIBLE: + result.append("visible"); + break; + case SOFT_INPUT_STATE_ALWAYS_VISIBLE: + result.append("always_visible"); + break; + default: + result.append(state); + break; + } + result.append(' '); + } + final int adjust = softInputMode & SOFT_INPUT_MASK_ADJUST; + if (adjust != 0) { + result.append("adjust="); + switch (adjust) { + case SOFT_INPUT_ADJUST_RESIZE: + result.append("resize"); + break; + case SOFT_INPUT_ADJUST_PAN: + result.append("pan"); + break; + case SOFT_INPUT_ADJUST_NOTHING: + result.append("nothing"); + break; + default: + result.append(adjust); + break; + } + result.append(' '); + } + if ((softInputMode & SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + result.append("forwardNavigation").append(' '); + } + result.deleteCharAt(result.length() - 1); + return result.toString(); + } + + private static String rotationAnimationToString(int rotationAnimation) { + switch (rotationAnimation) { + case ROTATION_ANIMATION_UNSPECIFIED: + return "UNSPECIFIED"; + case ROTATION_ANIMATION_ROTATE: + return "ROTATE"; + case ROTATION_ANIMATION_CROSSFADE: + return "CROSSFADE"; + case ROTATION_ANIMATION_JUMPCUT: + return "JUMPCUT"; + case ROTATION_ANIMATION_SEAMLESS: + return "SEAMLESS"; + default: + return Integer.toString(rotationAnimation); + } + } + + private static String inputFeatureToString(int inputFeature) { + switch (inputFeature) { + case INPUT_FEATURE_DISABLE_POINTER_GESTURES: + return "DISABLE_POINTER_GESTURES"; + case INPUT_FEATURE_NO_INPUT_CHANNEL: + return "NO_INPUT_CHANNEL"; + case INPUT_FEATURE_DISABLE_USER_ACTIVITY: + return "DISABLE_USER_ACTIVITY"; + default: + return Integer.toString(inputFeature); + } + } } } diff --git a/android/view/WindowManagerPolicy.java b/android/view/WindowManagerPolicy.java index 66506a18..da72535d 100644 --- a/android/view/WindowManagerPolicy.java +++ b/android/view/WindowManagerPolicy.java @@ -592,9 +592,10 @@ public interface WindowManagerPolicy { int getDockedDividerInsetsLw(); /** - * Retrieves the {@param outBounds} from the stack with id {@param stackId}. + * Retrieves the {@param outBounds} from the stack matching the {@param windowingMode} and + * {@param activityType}. */ - void getStackBounds(int stackId, Rect outBounds); + void getStackBounds(int windowingMode, int activityType, Rect outBounds); /** * Notifies window manager that {@link #isShowingDreamLw} has changed. @@ -617,6 +618,38 @@ public interface WindowManagerPolicy { * @param listener callback to call when display can be turned off */ void screenTurningOff(ScreenOffListener listener); + + /** + * Convert the lid state to a human readable format. + */ + static String lidStateToString(int lid) { + switch (lid) { + case LID_ABSENT: + return "LID_ABSENT"; + case LID_CLOSED: + return "LID_CLOSED"; + case LID_OPEN: + return "LID_OPEN"; + default: + return Integer.toString(lid); + } + } + + /** + * Convert the camera lens state to a human readable format. + */ + static String cameraLensStateToString(int lens) { + switch (lens) { + case CAMERA_LENS_COVER_ABSENT: + return "CAMERA_LENS_COVER_ABSENT"; + case CAMERA_LENS_UNCOVERED: + return "CAMERA_LENS_UNCOVERED"; + case CAMERA_LENS_COVERED: + return "CAMERA_LENS_COVERED"; + default: + return Integer.toString(lens); + } + } } public interface PointerEventListener { @@ -1750,4 +1783,34 @@ public interface WindowManagerPolicy { * @return true if ready; false otherwise. */ boolean canDismissBootAnimation(); + + /** + * Convert the user rotation mode to a human readable format. + */ + static String userRotationModeToString(int mode) { + switch(mode) { + case USER_ROTATION_FREE: + return "USER_ROTATION_FREE"; + case USER_ROTATION_LOCKED: + return "USER_ROTATION_LOCKED"; + default: + return Integer.toString(mode); + } + } + + /** + * Convert the off reason to a human readable format. + */ + static String offReasonToString(int why) { + switch (why) { + case OFF_BECAUSE_OF_ADMIN: + return "OFF_BECAUSE_OF_ADMIN"; + case OFF_BECAUSE_OF_USER: + return "OFF_BECAUSE_OF_USER"; + case OFF_BECAUSE_OF_TIMEOUT: + return "OFF_BECAUSE_OF_TIMEOUT"; + default: + return Integer.toString(why); + } + } } diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 11cb046a..0b9bc576 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,46 +16,152 @@ package android.view.accessibility; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; + +import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemService; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; import android.view.IWindow; import android.view.View; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IntPair; + +import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. - * Such events are generated when something notable happens in the user interface, + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, + * and provides facilities for querying the accessibility state of the system. + * Accessibility events are generated when something notable happens in the user interface, * for example an {@link android.app.Activity} starts, the focus or selection of a * {@link android.view.View} changes etc. Parties interested in handling accessibility * events implement and register an accessibility service which extends - * {@code android.accessibilityservice.AccessibilityService}. + * {@link android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see android.content.Context#getSystemService + * @see AccessibilityNodeInfo + * @see android.accessibilityservice.AccessibilityService + * @see Context#getSystemService + * @see Context#ACCESSIBILITY_SERVICE */ -@SuppressWarnings("UnusedDeclaration") +@SystemService(Context.ACCESSIBILITY_SERVICE) public final class AccessibilityManager { + private static final boolean DEBUG = false; + + private static final String LOG_TAG = "AccessibilityManager"; + + /** @hide */ + public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; + + /** @hide */ + public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; + + /** @hide */ + public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; + + /** @hide */ + public static final int DALTONIZER_DISABLED = -1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; + + /** @hide */ + public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; + + /** @hide */ + public static final int AUTOCLICK_DELAY_DEFAULT = 600; + + /** + * Activity action: Launch UI to manage which accessibility service or feature is assigned + * to the navigation bar Accessibility button. + * <p> + * Input: Nothing. + * </p> + * <p> + * Output: Nothing. + * </p> + * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON = + "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; + + static final Object sInstanceSync = new Object(); + + private static AccessibilityManager sInstance; + + private final Object mLock = new Object(); + + private IAccessibilityManager mService; + + final int mUserId; + + final Handler mHandler; + + final Handler.Callback mCallback; + + boolean mIsEnabled; - private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); + int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + boolean mIsTouchExplorationEnabled; + + boolean mIsHighTextContrastEnabled; + + private final ArrayMap<AccessibilityStateChangeListener, Handler> + mAccessibilityStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<TouchExplorationStateChangeListener, Handler> + mTouchExplorationStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<HighTextContrastChangeListener, Handler> + mHighTextContrastStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<AccessibilityServicesStateChangeListener, Handler> + mServicesStateChangeListeners = new ArrayMap<>(); + + /** + * Map from a view's accessibility id to the list of request preparers set for that view + */ + private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists; /** - * Listener for the accessibility state. + * Listener for the system accessibility state. To listen for changes to the + * accessibility state on the device, implement this interface and register + * it with the system by calling {@link #addAccessibilityStateChangeListener}. */ public interface AccessibilityStateChangeListener { /** - * Called back on change in the accessibility state. + * Called when the accessibility enabled state changes. * * @param enabled Whether accessibility is enabled. */ - public void onAccessibilityStateChanged(boolean enabled); + void onAccessibilityStateChanged(boolean enabled); } /** @@ -71,7 +177,24 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - public void onTouchExplorationStateChanged(boolean enabled); + void onTouchExplorationStateChanged(boolean enabled); + } + + /** + * Listener for changes to the state of accessibility services. Changes include services being + * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service. + * {@see #addAccessibilityServicesStateChangeListener}. + * + * @hide + */ + public interface AccessibilityServicesStateChangeListener { + + /** + * Called when the state of accessibility services changes. + * + * @param manager The manager that is calling back + */ + void onAccessibilityServicesStateChanged(AccessibilityManager manager); } /** @@ -79,6 +202,8 @@ public final class AccessibilityManager { * the high text contrast state on the device, implement this interface and * register it with the system by calling * {@link #addHighTextContrastStateChangeListener}. + * + * @hide */ public interface HighTextContrastChangeListener { @@ -87,26 +212,72 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - public void onHighTextContrastStateChanged(boolean enabled); + void onHighTextContrastStateChanged(boolean enabled); } private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - public void setState(int state) { - } + @Override + public void setState(int state) { + // We do not want to change this immediately as the application may + // have already checked that accessibility is on and fired an event, + // that is now propagating up the view tree, Hence, if accessibility + // is now off an exception will be thrown. We want to have the exception + // enforcement to guard against apps that fire unnecessary accessibility + // events when accessibility is off. + mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget(); + } - public void notifyServicesStateChanged() { + @Override + public void notifyServicesStateChanged() { + final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mServicesStateChangeListeners.isEmpty()) { + return; } + listeners = new ArrayMap<>(mServicesStateChangeListeners); + } - public void setRelevantEventTypes(int eventTypes) { - } - }; + int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityServicesStateChangeListener listener = + mServicesStateChangeListeners.keyAt(i); + mServicesStateChangeListeners.valueAt(i).post(() -> listener + .onAccessibilityServicesStateChanged(AccessibilityManager.this)); + } + } + + @Override + public void setRelevantEventTypes(int eventTypes) { + mRelevantEventTypes = eventTypes; + } + }; /** * Get an AccessibilityManager instance (create one if necessary). * + * @param context Context in which this manager operates. + * + * @hide */ public static AccessibilityManager getInstance(Context context) { + synchronized (sInstanceSync) { + if (sInstance == null) { + final int userId; + if (Binder.getCallingUid() == Process.SYSTEM_UID + || context.checkCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS) + == PackageManager.PERMISSION_GRANTED + || context.checkCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL) + == PackageManager.PERMISSION_GRANTED) { + userId = UserHandle.USER_CURRENT; + } else { + userId = UserHandle.myUserId(); + } + sInstance = new AccessibilityManager(context, null, userId); + } + } return sInstance; } @@ -114,21 +285,68 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. + * @param service An interface to the backing service. + * @param userId User id under which to run. + * + * @hide */ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { + // Constructor can't be chained because we can't create an instance of an inner class + // before calling another constructor. + mCallback = new MyCallback(); + mHandler = new Handler(context.getMainLooper(), mCallback); + mUserId = userId; + synchronized (mLock) { + tryConnectToServiceLocked(service); + } + } + + /** + * Create an instance. + * + * @param handler The handler to use + * @param service An interface to the backing service. + * @param userId User id under which to run. + * + * @hide + */ + public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) { + mCallback = new MyCallback(); + mHandler = handler; + mUserId = userId; + synchronized (mLock) { + tryConnectToServiceLocked(service); + } } + /** + * @hide + */ public IAccessibilityManagerClient getClient() { return mClient; } /** - * Returns if the {@link AccessibilityManager} is enabled. + * @hide + */ + @VisibleForTesting + public Handler.Callback getCallback() { + return mCallback; + } + + /** + * Returns if the accessibility in the system is enabled. * - * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + * @return True if accessibility is enabled, false otherwise. */ public boolean isEnabled() { - return false; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsEnabled; + } } /** @@ -137,7 +355,13 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - return true; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsTouchExplorationEnabled; + } } /** @@ -147,35 +371,169 @@ public final class AccessibilityManager { * doing its own rendering and does not rely on the platform rendering pipeline. * </p> * + * @return True if high text contrast is enabled, false otherwise. + * + * @hide */ public boolean isHighTextContrastEnabled() { - return false; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsHighTextContrastEnabled; + } } /** * Sends an {@link AccessibilityEvent}. + * + * @param event The event to send. + * + * @throws IllegalStateException if accessibility is not enabled. + * + * <strong>Note:</strong> The preferred mechanism for sending custom accessibility + * events is through calling + * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * instead of this method to allow predecessors to augment/filter events sent by + * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + if (!mIsEnabled) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); + return; + } + } + if ((event.getEventType() & mRelevantEventTypes) == 0) { + if (DEBUG) { + Log.i(LOG_TAG, "Not dispatching irrelevant event: " + event + + " that is not among " + + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); + } + return; + } + userId = mUserId; + } + try { + event.setEventTime(SystemClock.uptimeMillis()); + // it is possible that this manager is in the same process as the service but + // client using it is called through Binder from another process. Example: MMS + // app adds a SMS notification and the NotificationManagerService calls this method + long identityToken = Binder.clearCallingIdentity(); + service.sendAccessibilityEvent(event, userId); + Binder.restoreCallingIdentity(identityToken); + if (DEBUG) { + Log.i(LOG_TAG, event + " sent"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error during sending " + event + " ", re); + } finally { + event.recycle(); + } } /** - * Requests interruption of the accessibility feedback from all accessibility services. + * Requests feedback interruption from all accessibility services. */ public void interrupt() { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + if (!mIsEnabled) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "Interrupt called with accessibility disabled"); + return; + } + } + userId = mUserId; + } + try { + service.interrupt(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Requested interrupt from all services"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); + } } /** * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. + * + * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ @Deprecated public List<ServiceInfo> getAccessibilityServiceList() { - return Collections.emptyList(); + List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); + List<ServiceInfo> services = new ArrayList<>(); + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityServiceInfo info = infos.get(i); + services.add(info.getResolveInfo().serviceInfo); + } + return Collections.unmodifiableList(services); } + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { - return Collections.emptyList(); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + + List<AccessibilityServiceInfo> services = null; + try { + services = service.getInstalledAccessibilityServiceList(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** @@ -190,21 +548,48 @@ public final class AccessibilityManager { * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE */ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( int feedbackTypeFlags) { - return Collections.emptyList(); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + + List<AccessibilityServiceInfo> services = null; + try { + services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. + * the global accessibility state of the system. Equivalent to calling + * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { + @NonNull AccessibilityStateChangeListener listener) { + addAccessibilityStateChangeListener(listener, null); return true; } @@ -218,22 +603,40 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mAccessibilityStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + /** + * Unregisters an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if the listener was previously registered. + */ public boolean removeAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { - return true; + @NonNull AccessibilityStateChangeListener listener) { + synchronized (mLock) { + int index = mAccessibilityStateChangeListeners.indexOfKey(listener); + mAccessibilityStateChangeListeners.remove(listener); + return (index >= 0); + } } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. + * the global touch exploration state of the system. Equivalent to calling + * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { + addTouchExplorationStateChangeListener(listener, null); return true; } @@ -247,17 +650,103 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mTouchExplorationStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if successfully unregistered. + * @return True if listener was previously registered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - return true; + synchronized (mLock) { + int index = mTouchExplorationStateChangeListeners.indexOfKey(listener); + mTouchExplorationStateChangeListeners.remove(listener); + return (index >= 0); + } + } + + /** + * Registers a {@link AccessibilityServicesStateChangeListener}. + * + * @param listener The listener. + * @param handler The handler on which the listener should be called back, or {@code null} + * for a callback on the process's main handler. + * @hide + */ + public void addAccessibilityServicesStateChangeListener( + @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mServicesStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + + /** + * Unregisters a {@link AccessibilityServicesStateChangeListener}. + * + * @param listener The listener. + * + * @hide + */ + public void removeAccessibilityServicesStateChangeListener( + @NonNull AccessibilityServicesStateChangeListener listener) { + // Final CopyOnWriteArrayList - no lock needed. + mServicesStateChangeListeners.remove(listener); + } + + /** + * Registers a {@link AccessibilityRequestPreparer}. + */ + public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { + if (mRequestPreparerLists == null) { + mRequestPreparerLists = new SparseArray<>(1); + } + int id = preparer.getView().getAccessibilityViewId(); + List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id); + if (requestPreparerList == null) { + requestPreparerList = new ArrayList<>(1); + mRequestPreparerLists.put(id, requestPreparerList); + } + requestPreparerList.add(preparer); + } + + /** + * Unregisters a {@link AccessibilityRequestPreparer}. + */ + public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { + if (mRequestPreparerLists == null) { + return; + } + int viewId = preparer.getView().getAccessibilityViewId(); + List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId); + if (requestPreparerList != null) { + requestPreparerList.remove(preparer); + if (requestPreparerList.isEmpty()) { + mRequestPreparerLists.remove(viewId); + } + } + } + + /** + * Get the preparers that are registered for an accessibility ID + * + * @param id The ID of interest + * @return The list of preparers, or {@code null} if there are none. + * + * @hide + */ + public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) { + if (mRequestPreparerLists == null) { + return null; + } + return mRequestPreparerLists.get(id); } /** @@ -269,7 +758,12 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -279,7 +773,51 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) {} + @NonNull HighTextContrastChangeListener listener) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners.remove(listener); + } + } + + /** + * Check if the accessibility volume stream is active. + * + * @return True if accessibility volume is active (i.e. some service has requested it). False + * otherwise. + * @hide + */ + public boolean isAccessibilityVolumeStreamActive() { + List<AccessibilityServiceInfo> serviceInfos = + getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + for (int i = 0; i < serviceInfos.size(); i++) { + if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) { + return true; + } + } + return false; + } + + /** + * Report a fingerprint gesture to accessibility. Only available for the system process. + * + * @param keyCode The key code of the gesture + * @return {@code true} if accessibility consumes the event. {@code false} if not. + * @hide + */ + public boolean sendFingerprintGesture(int keyCode) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + try { + return service.sendFingerprintGesture(keyCode); + } catch (RemoteException e) { + return false; + } + } /** * Sets the current state and notifies listeners, if necessary. @@ -287,14 +825,314 @@ public final class AccessibilityManager { * @param stateFlags The state flags. */ private void setStateLocked(int stateFlags) { + final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; + final boolean touchExplorationEnabled = + (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; + final boolean highTextContrastEnabled = + (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; + + final boolean wasEnabled = mIsEnabled; + final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; + final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; + + // Ensure listeners get current state from isZzzEnabled() calls. + mIsEnabled = enabled; + mIsTouchExplorationEnabled = touchExplorationEnabled; + mIsHighTextContrastEnabled = highTextContrastEnabled; + + if (wasEnabled != enabled) { + notifyAccessibilityStateChanged(); + } + + if (wasTouchExplorationEnabled != touchExplorationEnabled) { + notifyTouchExplorationStateChanged(); + } + + if (wasHighTextContrastEnabled != highTextContrastEnabled) { + notifyHighTextContrastStateChanged(); + } } + /** + * Find an installed service with the specified {@link ComponentName}. + * + * @param componentName The name to match to the service. + * + * @return The info corresponding to the installed service, or {@code null} if no such service + * is installed. + * @hide + */ + public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName( + ComponentName componentName) { + final List<AccessibilityServiceInfo> installedServiceInfos = + getInstalledAccessibilityServiceList(); + if ((installedServiceInfos == null) || (componentName == null)) { + return null; + } + for (int i = 0; i < installedServiceInfos.size(); i++) { + if (componentName.equals(installedServiceInfos.get(i).getComponentName())) { + return installedServiceInfos.get(i); + } + } + return null; + } + + /** + * Adds an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is added. + * @param connection The connection. + * + * @hide + */ public int addAccessibilityInteractionConnection(IWindow windowToken, IAccessibilityInteractionConnection connection) { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return View.NO_ID; + } + userId = mUserId; + } + try { + return service.addAccessibilityInteractionConnection(windowToken, connection, userId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); + } return View.NO_ID; } + /** + * Removed an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is removed. + * + * @hide + */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.removeAccessibilityInteractionConnection(windowToken); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); + } + } + + /** + * Perform the accessibility shortcut if the caller has permission. + * + * @hide + */ + public void performAccessibilityShortcut() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.performAccessibilityShortcut(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re); + } + } + + /** + * Notifies that the accessibility button in the system's navigation area has been clicked + * + * @hide + */ + public void notifyAccessibilityButtonClicked() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonClicked(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); + } + } + + /** + * Notifies that the visibility of the accessibility button in the system's navigation area + * has changed. + * + * @param shown {@code true} if the accessibility button is visible within the system + * navigation area, {@code false} otherwise + * @hide + */ + public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonVisibilityChanged(shown); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re); + } + } + + /** + * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture + * window. Intended for use by the System UI only. + * + * @param connection The connection to handle the actions. Set to {@code null} to avoid + * affecting the actions. + * + * @hide + */ + public void setPictureInPictureActionReplacingConnection( + @Nullable IAccessibilityInteractionConnection connection) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.setPictureInPictureActionReplacingConnection(connection); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error setting picture in picture action replacement", re); + } } + private IAccessibilityManager getServiceLocked() { + if (mService == null) { + tryConnectToServiceLocked(null); + } + return mService; + } + + private void tryConnectToServiceLocked(IAccessibilityManager service) { + if (service == null) { + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + if (iBinder == null) { + return; + } + service = IAccessibilityManager.Stub.asInterface(iBinder); + } + + try { + final long userStateAndRelevantEvents = service.addClient(mClient, mUserId); + setStateLocked(IntPair.first(userStateAndRelevantEvents)); + mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); + mService = service; + } catch (RemoteException re) { + Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); + } + } + + /** + * Notifies the registered {@link AccessibilityStateChangeListener}s. + */ + private void notifyAccessibilityStateChanged() { + final boolean isEnabled; + final ArrayMap<AccessibilityStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mAccessibilityStateChangeListeners.isEmpty()) { + return; + } + isEnabled = mIsEnabled; + listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); + } + + int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityStateChangeListener listener = + mAccessibilityStateChangeListeners.keyAt(i); + mAccessibilityStateChangeListeners.valueAt(i) + .post(() -> listener.onAccessibilityStateChanged(isEnabled)); + } + } + + /** + * Notifies the registered {@link TouchExplorationStateChangeListener}s. + */ + private void notifyTouchExplorationStateChanged() { + final boolean isTouchExplorationEnabled; + final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mTouchExplorationStateChangeListeners.isEmpty()) { + return; + } + isTouchExplorationEnabled = mIsTouchExplorationEnabled; + listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners); + } + + int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final TouchExplorationStateChangeListener listener = + mTouchExplorationStateChangeListeners.keyAt(i); + mTouchExplorationStateChangeListeners.valueAt(i) + .post(() -> listener.onTouchExplorationStateChanged(isTouchExplorationEnabled)); + } + } + + /** + * Notifies the registered {@link HighTextContrastChangeListener}s. + */ + private void notifyHighTextContrastStateChanged() { + final boolean isHighTextContrastEnabled; + final ArrayMap<HighTextContrastChangeListener, Handler> listeners; + synchronized (mLock) { + if (mHighTextContrastStateChangeListeners.isEmpty()) { + return; + } + isHighTextContrastEnabled = mIsHighTextContrastEnabled; + listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners); + } + + int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final HighTextContrastChangeListener listener = + mHighTextContrastStateChangeListeners.keyAt(i); + mHighTextContrastStateChangeListeners.valueAt(i) + .post(() -> listener.onHighTextContrastStateChanged(isHighTextContrastEnabled)); + } + } + + /** + * Determines if the accessibility button within the system navigation area is supported. + * + * @return {@code true} if the accessibility button is supported on this device, + * {@code false} otherwise + */ + public static boolean isAccessibilityButtonSupported() { + final Resources res = Resources.getSystem(); + return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); + } + + private final class MyCallback implements Handler.Callback { + public static final int MSG_SET_STATE = 1; + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_SET_STATE: { + // See comment at mClient + final int state = message.arg1; + synchronized (mLock) { + setStateLocked(state); + } + } break; + } + return true; + } + } } diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java index 61cbce97..4fb2a99a 100644 --- a/android/view/autofill/AutofillManager.java +++ b/android/view/autofill/AutofillManager.java @@ -37,14 +37,13 @@ import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.DebugUtils; import android.util.Log; import android.util.SparseArray; import android.view.View; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -202,9 +201,12 @@ public final class AutofillManager { * Initial state of the autofill context, set when there is no session (i.e., when * {@link #mSessionId} is {@link #NO_SESSION}). * + * <p>In this state, app callbacks (such as {@link #notifyViewEntered(View)}) are notified to + * the server. + * * @hide */ - public static final int STATE_UNKNOWN = 1; + public static final int STATE_UNKNOWN = 0; /** * State where the autofill context hasn't been {@link #commit() finished} nor @@ -212,7 +214,18 @@ public final class AutofillManager { * * @hide */ - public static final int STATE_ACTIVE = 2; + public static final int STATE_ACTIVE = 1; + + /** + * State where the autofill context was finished by the server because the autofill + * service could not autofill the page. + * + * <p>In this state, most apps callback (such as {@link #notifyViewEntered(View)}) are ignored, + * exception {@link #requestAutofill(View)} (and {@link #requestAutofill(View, int, Rect)}). + * + * @hide + */ + public static final int STATE_FINISHED = 2; /** * State where the autofill context has been {@link #commit() finished} but the server still has @@ -220,7 +233,7 @@ public final class AutofillManager { * * @hide */ - public static final int STATE_SHOWING_SAVE_UI = 4; + public static final int STATE_SHOWING_SAVE_UI = 3; /** * Makes an authentication id from a request id and a dataset id. @@ -559,6 +572,14 @@ public final class AutofillManager { } AutofillCallback callback = null; synchronized (mLock) { + if (isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "notifyViewEntered(flags=" + flags + ", view=" + view + + "): ignored on state " + getStateAsStringLocked()); + } + return; + } + ensureServiceClientAddedIfNeededLocked(); if (!mEnabled) { @@ -682,6 +703,14 @@ public final class AutofillManager { } AutofillCallback callback = null; synchronized (mLock) { + if (isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "notifyViewEntered(flags=" + flags + ", view=" + view + + ", virtualId=" + virtualId + + "): ignored on state " + getStateAsStringLocked()); + } + return; + } ensureServiceClientAddedIfNeededLocked(); if (!mEnabled) { @@ -765,6 +794,10 @@ public final class AutofillManager { } if (!mEnabled || !isActiveLocked()) { + if (sVerbose && mEnabled) { + Log.v(TAG, "notifyValueChanged(" + view + "): ignoring on state " + + getStateAsStringLocked()); + } return; } @@ -904,10 +937,7 @@ public final class AutofillManager { } private AutofillClient getClientLocked() { - if (mContext instanceof AutofillClient) { - return (AutofillClient) mContext; - } - return null; + return mContext.getAutofillClient(); } /** @hide */ @@ -950,10 +980,13 @@ public final class AutofillManager { @NonNull AutofillValue value, int flags) { if (sVerbose) { Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value - + ", flags=" + flags + ", state=" + mState); + + ", flags=" + flags + ", state=" + getStateAsStringLocked()); } - if (mState != STATE_UNKNOWN) { - if (sDebug) Log.d(TAG, "not starting session for " + id + " on state " + mState); + if (mState != STATE_UNKNOWN && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "not automatically starting session for " + id + + " on state " + getStateAsStringLocked()); + } return; } try { @@ -973,7 +1006,7 @@ public final class AutofillManager { } private void finishSessionLocked() { - if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + mState); + if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + getStateAsStringLocked()); if (!isActiveLocked()) return; @@ -987,7 +1020,7 @@ public final class AutofillManager { } private void cancelSessionLocked() { - if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + mState); + if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + getStateAsStringLocked()); if (!isActiveLocked()) return; @@ -1245,10 +1278,10 @@ public final class AutofillManager { } } - final LogMaker log = new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_DATASET_APPLIED); - log.addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount); - log.addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, - numApplied); + final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_DATASET_APPLIED) + .setPackageName(mContext.getPackageName()) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied); mMetricsLogger.write(log); } } @@ -1306,6 +1339,20 @@ public final class AutofillManager { } } + /** + * Marks the state of the session as finished. + * + * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null} + * FillResponse) or {@link #STATE_UNKNOWN} (because the session was removed). + */ + private void setSessionFinished(int newState) { + synchronized (mLock) { + if (sVerbose) Log.v(TAG, "setSessionFinished(): from " + mState + " to " + newState); + resetSessionLocked(); + mState = newState; + } + } + private void requestHideFillUi(AutofillId id) { final View anchor = findView(id); if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor); @@ -1341,7 +1388,11 @@ public final class AutofillManager { } } - private void notifyNoFillUi(int sessionId, AutofillId id) { + private void notifyNoFillUi(int sessionId, AutofillId id, boolean sessionFinished) { + if (sVerbose) { + Log.v(TAG, "notifyNoFillUi(): sessionId=" + sessionId + ", autofillId=" + id + + ", finished=" + sessionFinished); + } final View anchor = findView(id); if (anchor == null) { return; @@ -1361,7 +1412,11 @@ public final class AutofillManager { } else { callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_UNAVAILABLE); } + } + if (sessionFinished) { + // Callback call was "hijacked" to also update the session state. + setSessionFinished(STATE_FINISHED); } } @@ -1434,8 +1489,7 @@ public final class AutofillManager { pw.print(outerPrefix); pw.println("AutofillManager:"); final String pfx = outerPrefix + " "; pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId); - pw.print(pfx); pw.print("state: "); pw.println( - DebugUtils.flagsToString(AutofillManager.class, "STATE_", mState)); + pw.print(pfx); pw.print("state: "); pw.println(getStateAsStringLocked()); pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled); pw.print(pfx); pw.print("hasService: "); pw.println(mService != null); pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null); @@ -1452,10 +1506,29 @@ public final class AutofillManager { pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); } + private String getStateAsStringLocked() { + switch (mState) { + case STATE_UNKNOWN: + return "STATE_UNKNOWN"; + case STATE_ACTIVE: + return "STATE_ACTIVE"; + case STATE_FINISHED: + return "STATE_FINISHED"; + case STATE_SHOWING_SAVE_UI: + return "STATE_SHOWING_SAVE_UI"; + default: + return "INVALID:" + mState; + } + } + private boolean isActiveLocked() { return mState == STATE_ACTIVE; } + private boolean isFinishedLocked() { + return mState == STATE_FINISHED; + } + private void post(Runnable runnable) { final AutofillClient client = getClientLocked(); if (client == null) { @@ -1787,10 +1860,10 @@ public final class AutofillManager { } @Override - public void notifyNoFillUi(int sessionId, AutofillId id) { + public void notifyNoFillUi(int sessionId, AutofillId id, boolean sessionFinished) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> afm.notifyNoFillUi(sessionId, id)); + afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinished)); } } @@ -1823,7 +1896,15 @@ public final class AutofillManager { public void setSaveUiState(int sessionId, boolean shown) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() ->afm.setSaveUiState(sessionId, shown)); + afm.post(() -> afm.setSaveUiState(sessionId, shown)); + } + } + + @Override + public void setSessionFinished(int newState) { + final AutofillManager afm = mAfm.get(); + if (afm != null) { + afm.post(() -> afm.setSessionFinished(newState)); } } } diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java index 5f476380..b4688bb1 100644 --- a/android/view/autofill/AutofillPopupWindow.java +++ b/android/view/autofill/AutofillPopupWindow.java @@ -47,6 +47,19 @@ public class AutofillPopupWindow extends PopupWindow { private final WindowPresenter mWindowPresenter; private WindowManager.LayoutParams mWindowLayoutParams; + private final View.OnAttachStateChangeListener mOnAttachStateChangeListener = + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + /* ignore - handled by the super class */ + } + + @Override + public void onViewDetachedFromWindow(View v) { + dismiss(); + } + }; + /** * Creates a popup window with a presenter owning the window and responsible for * showing/hiding/updating the backing window. This can be useful of the window is @@ -208,7 +221,21 @@ public class AutofillPopupWindow extends PopupWindow { p.packageName = anchor.getContext().getPackageName(); mWindowPresenter.show(p, getTransitionEpicenter(), isLayoutInsetDecor(), anchor.getLayoutDirection()); - return; + } + + @Override + protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { + super.attachToAnchor(anchor, xoff, yoff, gravity); + anchor.addOnAttachStateChangeListener(mOnAttachStateChangeListener); + } + + @Override + protected void detachFromAnchor() { + final View anchor = getAnchor(); + if (anchor != null) { + anchor.removeOnAttachStateChangeListener(mOnAttachStateChangeListener); + } + super.detachFromAnchor(); } @Override diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java index 1849368f..8c3b8a2e 100644 --- a/android/view/textclassifier/TextClassification.java +++ b/android/view/textclassifier/TextClassification.java @@ -28,6 +28,7 @@ import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; +import java.util.ArrayList; import java.util.List; /** @@ -41,10 +42,10 @@ public final class TextClassification { static final TextClassification EMPTY = new TextClassification.Builder().build(); @NonNull private final String mText; - @Nullable private final Drawable mIcon; - @Nullable private final String mLabel; - @Nullable private final Intent mIntent; - @Nullable private final OnClickListener mOnClickListener; + @NonNull private final List<Drawable> mIcons; + @NonNull private final List<String> mLabels; + @NonNull private final List<Intent> mIntents; + @NonNull private final List<OnClickListener> mOnClickListeners; @NonNull private final EntityConfidence<String> mEntityConfidence; @NonNull private final List<String> mEntities; private int mLogType; @@ -52,18 +53,21 @@ public final class TextClassification { private TextClassification( @Nullable String text, - @Nullable Drawable icon, - @Nullable String label, - @Nullable Intent intent, - @Nullable OnClickListener onClickListener, + @NonNull List<Drawable> icons, + @NonNull List<String> labels, + @NonNull List<Intent> intents, + @NonNull List<OnClickListener> onClickListeners, @NonNull EntityConfidence<String> entityConfidence, int logType, @NonNull String versionInfo) { + Preconditions.checkArgument(labels.size() == intents.size()); + Preconditions.checkArgument(icons.size() == intents.size()); + Preconditions.checkArgument(onClickListeners.size() == intents.size()); mText = text; - mIcon = icon; - mLabel = label; - mIntent = intent; - mOnClickListener = onClickListener; + mIcons = icons; + mLabels = labels; + mIntents = intents; + mOnClickListeners = onClickListeners; mEntityConfidence = new EntityConfidence<>(entityConfidence); mEntities = mEntityConfidence.getEntities(); mLogType = logType; @@ -109,35 +113,106 @@ public final class TextClassification { } /** - * Returns an icon that may be rendered on a widget used to act on the classified text. + * Returns the number of actions that are available to act on the classified text. + * @see #getIntent(int) + * @see #getLabel(int) + * @see #getIcon(int) + * @see #getOnClickListener(int) + */ + @IntRange(from = 0) + public int getActionCount() { + return mIntents.size(); + } + + /** + * Returns one of the icons that maybe rendered on a widget used to act on the classified text. + * @param index Index of the action to get the icon for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() for the number of entities available. + * @see #getIntent(int) + * @see #getLabel(int) + * @see #getOnClickListener(int) + */ + @Nullable + public Drawable getIcon(int index) { + return mIcons.get(index); + } + + /** + * Returns an icon for the default intent that may be rendered on a widget used to act on the + * classified text. */ @Nullable public Drawable getIcon() { - return mIcon; + return mIcons.isEmpty() ? null : mIcons.get(0); } /** - * Returns a label that may be rendered on a widget used to act on the classified text. + * Returns one of the labels that may be rendered on a widget used to act on the classified + * text. + * @param index Index of the action to get the label for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() + * @see #getIntent(int) + * @see #getIcon(int) + * @see #getOnClickListener(int) + */ + @Nullable + public CharSequence getLabel(int index) { + return mLabels.get(index); + } + + /** + * Returns a label for the default intent that may be rendered on a widget used to act on the + * classified text. */ @Nullable public CharSequence getLabel() { - return mLabel; + return mLabels.isEmpty() ? null : mLabels.get(0); } /** - * Returns an intent that may be fired to act on the classified text. + * Returns one of the intents that may be fired to act on the classified text. + * @param index Index of the action to get the intent for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() + * @see #getLabel(int) + * @see #getIcon(int) + * @see #getOnClickListener(int) + */ + @Nullable + public Intent getIntent(int index) { + return mIntents.get(index); + } + + /** + * Returns the default intent that may be fired to act on the classified text. */ @Nullable public Intent getIntent() { - return mIntent; + return mIntents.isEmpty() ? null : mIntents.get(0); } /** - * Returns an OnClickListener that may be triggered to act on the classified text. + * Returns one of the OnClickListeners that may be triggered to act on the classified text. + * @param index Index of the action to get the click listener for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() + * @see #getIntent(int) + * @see #getLabel(int) + * @see #getIcon(int) + */ + @Nullable + public OnClickListener getOnClickListener(int index) { + return mOnClickListeners.get(index); + } + + /** + * Returns the default OnClickListener that may be triggered to act on the classified text. */ @Nullable public OnClickListener getOnClickListener() { - return mOnClickListener; + return mOnClickListeners.isEmpty() ? null : mOnClickListeners.get(0); } /** @@ -160,8 +235,8 @@ public final class TextClassification { @Override public String toString() { return String.format("TextClassification {" - + "text=%s, entities=%s, label=%s, intent=%s}", - mText, mEntityConfidence, mLabel, mIntent); + + "text=%s, entities=%s, labels=%s, intents=%s}", + mText, mEntityConfidence, mLabels, mIntents); } /** @@ -184,10 +259,10 @@ public final class TextClassification { public static final class Builder { @NonNull private String mText; - @Nullable private Drawable mIcon; - @Nullable private String mLabel; - @Nullable private Intent mIntent; - @Nullable private OnClickListener mOnClickListener; + @NonNull private final List<Drawable> mIcons = new ArrayList<>(); + @NonNull private final List<String> mLabels = new ArrayList<>(); + @NonNull private final List<Intent> mIntents = new ArrayList<>(); + @NonNull private final List<OnClickListener> mOnClickListeners = new ArrayList<>(); @NonNull private final EntityConfidence<String> mEntityConfidence = new EntityConfidence<>(); private int mLogType; @@ -216,26 +291,57 @@ public final class TextClassification { } /** - * Sets an icon that may be rendered on a widget used to act on the classified text. + * Adds an action that may be performed on the classified text. The label and icon are used + * for rendering of widgets that offer the intent. Actions should be added in order of + * priority and the first one will be treated as the default. + */ + public Builder addAction( + Intent intent, @Nullable String label, @Nullable Drawable icon, + @Nullable OnClickListener onClickListener) { + mIntents.add(intent); + mLabels.add(label); + mIcons.add(icon); + mOnClickListeners.add(onClickListener); + return this; + } + + /** + * Removes all actions. + */ + public Builder clearActions() { + mIntents.clear(); + mOnClickListeners.clear(); + mLabels.clear(); + mIcons.clear(); + return this; + } + + /** + * Sets the icon for the default action that may be rendered on a widget used to act on the + * classified text. */ public Builder setIcon(@Nullable Drawable icon) { - mIcon = icon; + ensureDefaultActionAvailable(); + mIcons.set(0, icon); return this; } /** - * Sets a label that may be rendered on a widget used to act on the classified text. + * Sets the label for the default action that may be rendered on a widget used to act on the + * classified text. */ public Builder setLabel(@Nullable String label) { - mLabel = label; + ensureDefaultActionAvailable(); + mLabels.set(0, label); return this; } /** - * Sets an intent that may be fired to act on the classified text. + * Sets the intent for the default action that may be fired to act on the classified text. */ public Builder setIntent(@Nullable Intent intent) { - mIntent = intent; + ensureDefaultActionAvailable(); + mIntents.set(0, intent); return this; } @@ -249,10 +355,12 @@ public final class TextClassification { } /** - * Sets an OnClickListener that may be triggered to act on the classified text. + * Sets the OnClickListener for the default action that may be triggered to act on the + * classified text. */ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) { - mOnClickListener = onClickListener; + ensureDefaultActionAvailable(); + mOnClickListeners.set(0, onClickListener); return this; } @@ -266,11 +374,21 @@ public final class TextClassification { } /** + * Ensures that we have at we have storage for the default action. + */ + private void ensureDefaultActionAvailable() { + if (mIntents.isEmpty()) mIntents.add(null); + if (mLabels.isEmpty()) mLabels.add(null); + if (mIcons.isEmpty()) mIcons.add(null); + if (mOnClickListeners.isEmpty()) mOnClickListeners.add(null); + } + + /** * Builds and returns a {@link TextClassification} object. */ public TextClassification build() { return new TextClassification( - mText, mIcon, mLabel, mIntent, mOnClickListener, mEntityConfidence, + mText, mIcons, mLabels, mIntents, mOnClickListeners, mEntityConfidence, mLogType, mVersionInfo); } } diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index 7e93b78c..2aa81a2c 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -29,6 +29,7 @@ import android.net.Uri; import android.os.LocaleList; import android.os.ParcelFileDescriptor; import android.provider.Browser; +import android.provider.ContactsContract; import android.text.Spannable; import android.text.TextUtils; import android.text.method.WordIterator; @@ -356,7 +357,16 @@ final class TextClassifierImpl implements TextClassifier { final String type = getHighestScoringType(classifications); builder.setLogType(IntentFactory.getLogType(type)); - final Intent intent = IntentFactory.create(mContext, type, text.toString()); + final List<Intent> intents = IntentFactory.create(mContext, type, text.toString()); + for (Intent intent : intents) { + extendClassificationWithIntent(intent, builder); + } + + return builder.setVersionInfo(getVersionInfo()).build(); + } + + /** Extends the classification with the intent if it can be resolved. */ + private void extendClassificationWithIntent(Intent intent, TextClassification.Builder builder) { final PackageManager pm; final ResolveInfo resolveInfo; if (intent != null) { @@ -367,30 +377,29 @@ final class TextClassifierImpl implements TextClassifier { resolveInfo = null; } if (resolveInfo != null && resolveInfo.activityInfo != null) { - builder.setIntent(intent) - .setOnClickListener(TextClassification.createStartActivityOnClickListener( - mContext, intent)); - final String packageName = resolveInfo.activityInfo.packageName; + CharSequence label; + Drawable icon; if ("android".equals(packageName)) { // Requires the chooser to find an activity to handle the intent. - builder.setLabel(IntentFactory.getLabel(mContext, type)); + label = IntentFactory.getLabel(mContext, intent); + icon = null; } else { // A default activity will handle the intent. intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name)); - Drawable icon = resolveInfo.activityInfo.loadIcon(pm); + icon = resolveInfo.activityInfo.loadIcon(pm); if (icon == null) { icon = resolveInfo.loadIcon(pm); } - builder.setIcon(icon); - CharSequence label = resolveInfo.activityInfo.loadLabel(pm); + label = resolveInfo.activityInfo.loadLabel(pm); if (label == null) { label = resolveInfo.loadLabel(pm); } - builder.setLabel(label != null ? label.toString() : null); } + builder.addAction( + intent, label != null ? label.toString() : null, icon, + TextClassification.createStartActivityOnClickListener(mContext, intent)); } - return builder.setVersionInfo(getVersionInfo()).build(); } private static int getHintFlags(CharSequence text, int start, int end) { @@ -477,10 +486,11 @@ final class TextClassifierImpl implements TextClassifier { if (results.length > 0) { final String type = getHighestScoringType(results); if (matches(type, linkMask)) { - final Intent intent = IntentFactory.create( + // For links without disambiguation, we simply use the default intent. + final List<Intent> intents = IntentFactory.create( context, type, text.substring(selectionStart, selectionEnd)); - if (hasActivityHandler(context, intent)) { - final ClickableSpan span = createSpan(context, intent); + if (!intents.isEmpty() && hasActivityHandler(context, intents.get(0))) { + final ClickableSpan span = createSpan(context, intents.get(0)); spans.add(new SpanSpec(selectionStart, selectionEnd, span)); } } @@ -564,7 +574,7 @@ final class TextClassifierImpl implements TextClassifier { }; } - private static boolean hasActivityHandler(Context context, @Nullable Intent intent) { + private static boolean hasActivityHandler(Context context, Intent intent) { if (intent == null) { return false; } @@ -625,20 +635,32 @@ final class TextClassifierImpl implements TextClassifier { private IntentFactory() {} - @Nullable - public static Intent create(Context context, String type, String text) { + @NonNull + public static List<Intent> create(Context context, String type, String text) { + final List<Intent> intents = new ArrayList<>(); type = type.trim().toLowerCase(Locale.ENGLISH); text = text.trim(); switch (type) { case TextClassifier.TYPE_EMAIL: - return new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse(String.format("mailto:%s", text))); + intents.add(new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse(String.format("mailto:%s", text)))); + intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.EMAIL, text)); + break; case TextClassifier.TYPE_PHONE: - return new Intent(Intent.ACTION_DIAL) - .setData(Uri.parse(String.format("tel:%s", text))); + intents.add(new Intent(Intent.ACTION_DIAL) + .setData(Uri.parse(String.format("tel:%s", text)))); + intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.PHONE, text)); + intents.add(new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse(String.format("smsto:%s", text)))); + break; case TextClassifier.TYPE_ADDRESS: - return new Intent(Intent.ACTION_VIEW) - .setData(Uri.parse(String.format("geo:0,0?q=%s", text))); + intents.add(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(String.format("geo:0,0?q=%s", text)))); + break; case TextClassifier.TYPE_URL: final String httpPrefix = "http://"; final String httpsPrefix = "https://"; @@ -649,25 +671,47 @@ final class TextClassifierImpl implements TextClassifier { } else { text = httpPrefix + text; } - return new Intent(Intent.ACTION_VIEW, Uri.parse(text)) - .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); - default: - return null; + intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text)) + .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName())); + break; } + return intents; } @Nullable - public static String getLabel(Context context, String type) { - type = type.trim().toLowerCase(Locale.ENGLISH); - switch (type) { - case TextClassifier.TYPE_EMAIL: - return context.getString(com.android.internal.R.string.email); - case TextClassifier.TYPE_PHONE: + public static String getLabel(Context context, @Nullable Intent intent) { + if (intent == null || intent.getAction() == null) { + return null; + } + switch (intent.getAction()) { + case Intent.ACTION_DIAL: return context.getString(com.android.internal.R.string.dial); - case TextClassifier.TYPE_ADDRESS: - return context.getString(com.android.internal.R.string.map); - case TextClassifier.TYPE_URL: - return context.getString(com.android.internal.R.string.browse); + case Intent.ACTION_SENDTO: + switch (intent.getScheme()) { + case "mailto": + return context.getString(com.android.internal.R.string.email); + case "smsto": + return context.getString(com.android.internal.R.string.sms); + default: + return null; + } + case Intent.ACTION_INSERT_OR_EDIT: + switch (intent.getDataString()) { + case ContactsContract.Contacts.CONTENT_ITEM_TYPE: + return context.getString(com.android.internal.R.string.add_contact); + default: + return null; + } + case Intent.ACTION_VIEW: + switch (intent.getScheme()) { + case "geo": + return context.getString(com.android.internal.R.string.map); + case "http": // fall through + case "https": + return context.getString(com.android.internal.R.string.browse); + default: + return null; + } default: return null; } diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java index 8d88ba60..83af19bb 100644 --- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -581,6 +581,7 @@ public final class SmartSelectionEventTracker { case ActionType.SMART_SHARE: // fall through case ActionType.DRAG: // fall through case ActionType.ABANDON: // fall through + case ActionType.OTHER: // fall through return true; default: return false; diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index 8e1f2183..f368c74a 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,58 +1,213 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2011 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ package android.view.textservice; +import android.annotation.SystemService; +import android.content.Context; import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceManager.ServiceNotFoundException; +import android.util.Log; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; +import com.android.internal.textservice.ITextServicesManager; + import java.util.Locale; /** - * A stub class of TextServicesManager for Layout-Lib. + * System API to the overall text services, which arbitrates interaction between applications + * and text services. + * + * The user can change the current text services in Settings. And also applications can specify + * the target text services. + * + * <h3>Architecture Overview</h3> + * + * <p>There are three primary parties involved in the text services + * framework (TSF) architecture:</p> + * + * <ul> + * <li> The <strong>text services manager</strong> as expressed by this class + * is the central point of the system that manages interaction between all + * other parts. It is expressed as the client-side API here which exists + * in each application context and communicates with a global system service + * that manages the interaction across all processes. + * <li> A <strong>text service</strong> implements a particular + * interaction model allowing the client application to retrieve information of text. + * The system binds to the current text service that is in use, causing it to be created and run. + * <li> Multiple <strong>client applications</strong> arbitrate with the text service + * manager for connections to text services. + * </ul> + * + * <h3>Text services sessions</h3> + * <ul> + * <li>The <strong>spell checker session</strong> is one of the text services. + * {@link android.view.textservice.SpellCheckerSession}</li> + * </ul> + * */ +@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) public final class TextServicesManager { - private static final TextServicesManager sInstance = new TextServicesManager(); - private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0]; + private static final String TAG = TextServicesManager.class.getSimpleName(); + private static final boolean DBG = false; + + private static TextServicesManager sInstance; + + private final ITextServicesManager mService; + + private TextServicesManager() throws ServiceNotFoundException { + mService = ITextServicesManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE)); + } /** * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist. * @hide */ public static TextServicesManager getInstance() { - return sInstance; + synchronized (TextServicesManager.class) { + if (sInstance == null) { + try { + sInstance = new TextServicesManager(); + } catch (ServiceNotFoundException e) { + throw new IllegalStateException(e); + } + } + return sInstance; + } + } + + /** + * Returns the language component of a given locale string. + */ + private static String parseLanguageFromLocaleString(String locale) { + final int idx = locale.indexOf('_'); + if (idx < 0) { + return locale; + } else { + return locale.substring(0, idx); + } } + /** + * Get a spell checker session for the specified spell checker + * @param locale the locale for the spell checker. If {@code locale} is null and + * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be + * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true, + * the locale specified in Settings will be returned only when it is same as {@code locale}. + * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is + * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be + * selected. + * @param listener a spell checker session lister for getting results from a spell checker. + * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled + * languages in settings will be returned. + * @return the spell checker session of the spell checker + */ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale, SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) { - return null; + if (listener == null) { + throw new NullPointerException(); + } + if (!referToSpellCheckerLanguageSettings && locale == null) { + throw new IllegalArgumentException("Locale should not be null if you don't refer" + + " settings."); + } + + if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) { + return null; + } + + final SpellCheckerInfo sci; + try { + sci = mService.getCurrentSpellChecker(null); + } catch (RemoteException e) { + return null; + } + if (sci == null) { + return null; + } + SpellCheckerSubtype subtypeInUse = null; + if (referToSpellCheckerLanguageSettings) { + subtypeInUse = getCurrentSpellCheckerSubtype(true); + if (subtypeInUse == null) { + return null; + } + if (locale != null) { + final String subtypeLocale = subtypeInUse.getLocale(); + final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); + if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) { + return null; + } + } + } else { + final String localeStr = locale.toString(); + for (int i = 0; i < sci.getSubtypeCount(); ++i) { + final SpellCheckerSubtype subtype = sci.getSubtypeAt(i); + final String tempSubtypeLocale = subtype.getLocale(); + final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale); + if (tempSubtypeLocale.equals(localeStr)) { + subtypeInUse = subtype; + break; + } else if (tempSubtypeLanguage.length() >= 2 && + locale.getLanguage().equals(tempSubtypeLanguage)) { + subtypeInUse = subtype; + } + } + } + if (subtypeInUse == null) { + return null; + } + final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener); + try { + mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), + session.getTextServicesSessionListener(), + session.getSpellCheckerSessionListener(), bundle); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return session; } /** * @hide */ public SpellCheckerInfo[] getEnabledSpellCheckers() { - return EMPTY_SPELL_CHECKER_INFO; + try { + final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers(); + if (DBG) { + Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null")); + } + return retval; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** * @hide */ public SpellCheckerInfo getCurrentSpellChecker() { - return null; + try { + // Passing null as a locale for ICS + return mService.getCurrentSpellChecker(null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -60,13 +215,22 @@ public final class TextServicesManager { */ public SpellCheckerSubtype getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype) { - return null; + try { + // Passing null as a locale until we support multiple enabled spell checker subtypes. + return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** * @hide */ public boolean isSpellCheckerEnabled() { - return false; + try { + return mService.isSpellCheckerEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } |