diff options
Diffstat (limited to 'android/view')
28 files changed, 2911 insertions, 668 deletions
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java index 66a9c6c0..f59c0b50 100644 --- a/android/view/DisplayCutout.java +++ b/android/view/DisplayCutout.java @@ -31,6 +31,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.util.PathParser; import android.util.proto.ProtoOutputStream; @@ -75,15 +76,19 @@ public final class DisplayCutout { false /* copyArguments */); + private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null); private static final Object CACHE_LOCK = new Object(); + @GuardedBy("CACHE_LOCK") private static String sCachedSpec; @GuardedBy("CACHE_LOCK") private static int sCachedDisplayWidth; @GuardedBy("CACHE_LOCK") + private static int sCachedDisplayHeight; + @GuardedBy("CACHE_LOCK") private static float sCachedDensity; @GuardedBy("CACHE_LOCK") - private static DisplayCutout sCachedCutout; + private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR; private final Rect mSafeInsets; private final Region mBounds; @@ -347,7 +352,7 @@ public final class DisplayCutout { } /** - * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. + * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout. * * @hide */ @@ -357,6 +362,16 @@ public final class DisplayCutout { } /** + * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. + * + * @hide + */ + public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) { + return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout), + displayWidth, displayHeight, res.getDisplayMetrics().density).first; + } + + /** * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec. * * @hide @@ -364,11 +379,17 @@ public final class DisplayCutout { @VisibleForTesting(visibility = PRIVATE) public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight, float density) { + return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second; + } + + private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec, + int displayWidth, int displayHeight, float density) { if (TextUtils.isEmpty(spec)) { - return null; + return NULL_PAIR; } synchronized (CACHE_LOCK) { if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth + && sCachedDisplayHeight == displayHeight && sCachedDensity == density) { return sCachedCutout; } @@ -398,7 +419,7 @@ public final class DisplayCutout { p = PathParser.createPathFromPathData(spec); } catch (Throwable e) { Log.wtf(TAG, "Could not inflate cutout: ", e); - return null; + return NULL_PAIR; } final Matrix m = new Matrix(); @@ -414,7 +435,7 @@ public final class DisplayCutout { bottomPath = PathParser.createPathFromPathData(bottomSpec); } catch (Throwable e) { Log.wtf(TAG, "Could not inflate bottom cutout: ", e); - return null; + return NULL_PAIR; } // Keep top transform m.postTranslate(0, displayHeight); @@ -422,10 +443,11 @@ public final class DisplayCutout { p.addPath(bottomPath); } - final DisplayCutout result = fromBounds(p); + final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p)); synchronized (CACHE_LOCK) { sCachedSpec = spec; sCachedDisplayWidth = displayWidth; + sCachedDisplayHeight = displayHeight; sCachedDensity = density; sCachedCutout = result; } diff --git a/android/view/HapticFeedbackConstants.java b/android/view/HapticFeedbackConstants.java index b1479284..db01cea3 100644 --- a/android/view/HapticFeedbackConstants.java +++ b/android/view/HapticFeedbackConstants.java @@ -77,6 +77,55 @@ public class HapticFeedbackConstants { public static final int TEXT_HANDLE_MOVE = 9; /** + * The user unlocked the device + * @hide + */ + public static final int ENTRY_BUMP = 10; + + /** + * The user has moved the dragged object within a droppable area. + * @hide + */ + public static final int DRAG_CROSSING = 11; + + /** + * The user has started a gesture (e.g. on the soft keyboard). + * @hide + */ + public static final int GESTURE_START = 12; + + /** + * The user has finished a gesture (e.g. on the soft keyboard). + * @hide + */ + public static final int GESTURE_END = 13; + + /** + * The user's squeeze crossed the gesture's initiation threshold. + * @hide + */ + public static final int EDGE_SQUEEZE = 14; + + /** + * The user's squeeze crossed the gesture's release threshold. + * @hide + */ + public static final int EDGE_RELEASE = 15; + + /** + * A haptic effect to signal the confirmation or successful completion of a user + * interaction. + * @hide + */ + public static final int CONFIRM = 16; + + /** + * A haptic effect to signal the rejection or failure of a user interaction. + * @hide + */ + public static final int REJECT = 17; + + /** * The phone has booted with safe mode enabled. * This is a private constant. Feel free to renumber as desired. * @hide diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index d4610a56..5deee11b 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -87,6 +87,7 @@ public class SurfaceControl implements Parcelable { private static native void nativeMergeTransaction(long transactionObj, long otherTransactionObj); private static native void nativeSetAnimationTransaction(long transactionObj); + private static native void nativeSetEarlyWakeup(long transactionObj); private static native void nativeSetLayer(long transactionObj, long nativeObject, int zorder); private static native void nativeSetRelativeLayer(long transactionObj, long nativeObject, @@ -1642,6 +1643,19 @@ public class SurfaceControl implements Parcelable { } /** + * Indicate that SurfaceFlinger should wake up earlier than usual as a result of this + * transaction. This should be used when the caller thinks that the scene is complex enough + * that it's likely to hit GL composition, and thus, SurfaceFlinger needs to more time in + * order not to miss frame deadlines. + * <p> + * Corresponds to setting ISurfaceComposer::eEarlyWakeup + */ + public Transaction setEarlyWakeup() { + nativeSetEarlyWakeup(mNativeObject); + return this; + } + + /** * Merge the other transaction into this transaction, clearing the * other transaction as if it had been applied. */ diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index ebb2af45..7e546476 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,115 +16,1237 @@ package android.view; -import com.android.layoutlib.bridge.MockView; +import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER; import android.content.Context; +import android.content.res.CompatibilityInfo.Translator; +import android.content.res.Configuration; import android.graphics.Canvas; +import android.graphics.Color; +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; + + SurfaceControlWithBackground 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; + + private SurfaceControl.Transaction mRtTransaction = new SurfaceControl.Transaction(); 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 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(); + final String name = "SurfaceView - " + viewRoot.getTitle().toString(); + + mSurfaceControl = new SurfaceControlWithBackground( + name, + (mSurfaceFlags & SurfaceControl.OPAQUE) != 0, + new SurfaceControl.Builder(mSurfaceSession) + .setSize(mSurfaceWidth, mSurfaceHeight) + .setFormat(mFormat) + .setFlags(mSurfaceFlags)); + } else if (mSurfaceControl == null) { + return; + } + + 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(); + }); + } + + /** + * A place to over-ride for applying child-surface transactions. + * These can be synchronized with the viewroot surface using deferTransaction. + * + * Called from RenderWorker while UI thread is paused. + * @hide + */ + protected void applyChildSurfaceTransaction_renderWorker(SurfaceControl.Transaction t, + Surface viewRootSurface, long nextViewRootFrameNumber) { + } + + private void applySurfaceTransforms(SurfaceControl surface, Rect position, long frameNumber) { + if (frameNumber > 0) { + final ViewRootImpl viewRoot = getViewRootImpl(); + + mRtTransaction.deferTransactionUntilSurface(surface, viewRoot.mSurface, + frameNumber); + } + + mRtTransaction.setPosition(surface, position.left, position.top); + mRtTransaction.setMatrix(surface, + position.width() / (float) mSurfaceWidth, + 0.0f, 0.0f, + position.height() / (float) mSurfaceHeight); + } + + private void setParentSpaceRectangle(Rect position, long frameNumber) { + final ViewRootImpl viewRoot = getViewRootImpl(); + + applySurfaceTransforms(mSurfaceControl, position, frameNumber); + applySurfaceTransforms(mSurfaceControl.mBackgroundControl, position, frameNumber); + + applyChildSurfaceTransaction_renderWorker(mRtTransaction, viewRoot.mSurface, + frameNumber); + + mRtTransaction.apply(); + } + + 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; } - private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + 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; + } + + /** + * Set an opaque background color to use with this {@link SurfaceView} when it's being resized + * and size of the content hasn't updated yet. This color will fill the expanded area when the + * view becomes larger. + * @param bgColor An opaque color to fill the background. Alpha component will be ignored. + * @hide + */ + public void setResizeBackgroundColor(int bgColor) { + mSurfaceControl.setBackgroundColor(bgColor); + } + + 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 inOutDirty) { + return internalLockCanvas(inOutDirty, false); } @Override - public Canvas lockCanvas(Rect dirty) { + 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 { + SurfaceControl mBackgroundControl; + private boolean mOpaque = true; + public boolean mVisible = false; + + public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b) + throws Exception { + super(b.setName(name).build()); + + mBackgroundControl = b.setName("Background for -" + name) + .setFormat(OPAQUE) + .setColorLayer(true) + .build(); + mOpaque = opaque; + } + + @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); + } + + /** Set the color to fill the background with. */ + private void setBackgroundColor(int bgColor) { + final float[] colorComponents = new float[] { Color.red(bgColor) / 255.f, + Color.green(bgColor) / 255.f, Color.blue(bgColor) / 255.f }; + + SurfaceControl.openTransaction(); + try { + mBackgroundControl.setColor(colorComponents); + } finally { + SurfaceControl.closeTransaction(); + } + } + + void updateBackgroundVisibility() { + if (mOpaque && mVisible) { + mBackgroundControl.show(); + } else { + mBackgroundControl.hide(); + } + } + } +} diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java index 5eb7e9cb..e03f5faa 100644 --- a/android/view/ThreadedRenderer.java +++ b/android/view/ThreadedRenderer.java @@ -190,6 +190,10 @@ public final class ThreadedRenderer { */ public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor"; + public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101; + public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102; + public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103; + static { // Try to check OpenGL support early if possible. isAvailable(); @@ -1140,6 +1144,16 @@ public final class ThreadedRenderer { nHackySetRTAnimationsEnabled(divisor <= 1); } + /** + * Changes the OpenGL context priority if IMG_context_priority extension is available. Must be + * called before any OpenGL context is created. + * + * @param priority The priority to use. Must be one of EGL_CONTEXT_PRIORITY_* values. + */ + public static void setContextPriority(int priority) { + nSetContextPriority(priority); + } + /** Not actually public - internal use only. This doc to make lint happy */ public static native void disableVsync(); @@ -1213,4 +1227,5 @@ public final class ThreadedRenderer { private static native void nHackySetRTAnimationsEnabled(boolean enabled); private static native void nSetDebuggingEnabled(boolean enabled); private static native void nSetIsolatedProcess(boolean enabled); + private static native void nSetContextPriority(int priority); } diff --git a/android/view/View.java b/android/view/View.java index 97e11b15..71b60844 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -697,6 +697,7 @@ import java.util.function.Predicate; * security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}. * </p> * + * @attr ref android.R.styleable#View_accessibilityHeading * @attr ref android.R.styleable#View_alpha * @attr ref android.R.styleable#View_background * @attr ref android.R.styleable#View_clickable @@ -2955,7 +2956,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 1 PFLAG3_SCREEN_READER_FOCUSABLE * 1 PFLAG3_AGGREGATED_VISIBLE * 1 PFLAG3_AUTOFILLID_EXPLICITLY_SET - * 1 available + * 1 PFLAG3_ACCESSIBILITY_HEADING * |-------|-------|-------|-------| */ @@ -3252,6 +3253,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int PFLAG3_AUTOFILLID_EXPLICITLY_SET = 0x40000000; + /** + * Indicates if the View is a heading for accessibility purposes + */ + private static final int PFLAG3_ACCESSIBILITY_HEADING = 0x80000000; + /* End of masks for mPrivateFlags3 */ /** @@ -5475,6 +5481,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_outlineAmbientShadowColor: setOutlineAmbientShadowColor(a.getColor(attr, Color.BLACK)); break; + case com.android.internal.R.styleable.View_accessibilityHeading: + setAccessibilityHeading(a.getBoolean(attr, false)); } } @@ -8795,6 +8803,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN); populateAccessibilityNodeInfoDrawingOrderInParent(info); info.setPaneTitle(mAccessibilityPaneTitle); + info.setHeading(isAccessibilityHeading()); } /** @@ -10398,7 +10407,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param willNotCacheDrawing true if this view does not cache its * drawing, false otherwise + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void setWillNotCacheDrawing(boolean willNotCacheDrawing) { setFlags(willNotCacheDrawing ? WILL_NOT_CACHE_DRAWING : 0, WILL_NOT_CACHE_DRAWING); } @@ -10407,8 +10430,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Returns whether or not this View can cache its drawing or not. * * @return true if this view does not cache its drawing, false otherwise + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ @ViewDebug.ExportedProperty(category = "drawing") + @Deprecated public boolean willNotCacheDrawing() { return (mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING; } @@ -10754,11 +10791,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * accessibility tools. */ public void setScreenReaderFocusable(boolean screenReaderFocusable) { + updatePflags3AndNotifyA11yIfChanged(PFLAG3_SCREEN_READER_FOCUSABLE, screenReaderFocusable); + } + + /** + * Gets whether this view is a heading for accessibility purposes. + * + * @return {@code true} if the view is a heading, {@code false} otherwise. + * + * @attr ref android.R.styleable#View_accessibilityHeading + */ + public boolean isAccessibilityHeading() { + return (mPrivateFlags3 & PFLAG3_ACCESSIBILITY_HEADING) != 0; + } + + /** + * Set if view is a heading for a section of content for accessibility purposes. + * + * @param isHeading {@code true} if the view is a heading, {@code false} otherwise. + * + * @attr ref android.R.styleable#View_accessibilityHeading + */ + public void setAccessibilityHeading(boolean isHeading) { + updatePflags3AndNotifyA11yIfChanged(PFLAG3_ACCESSIBILITY_HEADING, isHeading); + } + + private void updatePflags3AndNotifyA11yIfChanged(int mask, boolean newValue) { int pflags3 = mPrivateFlags3; - if (screenReaderFocusable) { - pflags3 |= PFLAG3_SCREEN_READER_FOCUSABLE; + if (newValue) { + pflags3 |= mask; } else { - pflags3 &= ~PFLAG3_SCREEN_READER_FOCUSABLE; + pflags3 &= ~mask; } if (pflags3 != mPrivateFlags3) { @@ -11763,6 +11826,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } + /** @hide */ + View getSelfOrParentImportantForA11y() { + if (isImportantForAccessibility()) return this; + ViewParent parent = getParentForAccessibility(); + if (parent instanceof View) return (View) parent; + return null; + } + /** * Adds the children of this View relevant for accessibility to the given list * as output. Since some Views are not important for accessibility the added @@ -14978,10 +15049,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { ensureTransformationInfo(); if (mTransformationInfo.mAlpha != alpha) { - // Report visibility changes, which can affect children, to accessibility - if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) { - notifySubtreeAccessibilityStateChangedIfNeeded(); - } + float oldAlpha = mTransformationInfo.mAlpha; mTransformationInfo.mAlpha = alpha; if (onSetAlpha((int) (alpha * 255))) { mPrivateFlags |= PFLAG_ALPHA_SET; @@ -14993,6 +15061,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidateViewProperty(true, false); mRenderNode.setAlpha(getFinalAlpha()); } + // Report visibility changes, which can affect children, to accessibility + if ((alpha == 0) ^ (oldAlpha == 0)) { + notifySubtreeAccessibilityStateChangedIfNeeded(); + } } } diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java index 6002fe51..2ec42c0d 100644 --- a/android/view/ViewGroup.java +++ b/android/view/ViewGroup.java @@ -5692,6 +5692,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown()); + notifySubtreeAccessibilityStateChangedIfNeeded(); } /** diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 433c90b3..730c3729 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -3719,7 +3719,7 @@ public final class ViewRootImpl implements ViewParent, checkThread(); if (mView != null) { if (!mView.hasFocus()) { - if (sAlwaysAssignFocus || !isInTouchMode()) { + if (sAlwaysAssignFocus || !mAttachInfo.mInTouchMode) { v.requestFocus(); } } else { @@ -6482,17 +6482,17 @@ public final class ViewRootImpl implements ViewParent, params.type = mOrigWindowType; } } + } - if (mSurface.isValid()) { - params.frameNumber = mSurface.getNextFrameNumber(); - } + long frameNumber = -1; + if (mSurface.isValid()) { + frameNumber = mSurface.getNextFrameNumber(); } - int relayoutResult = mWindowSession.relayout( - mWindow, mSeq, params, + int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params, (int) (mView.getMeasuredWidth() * appScale + 0.5f), - (int) (mView.getMeasuredHeight() * appScale + 0.5f), - viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, + (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, + insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber, mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout, mPendingMergedConfiguration, mSurface); @@ -8305,6 +8305,12 @@ public final class ViewRootImpl implements ViewParent, public View mSource; public long mLastEventTimeMillis; + /** + * Override for {@link AccessibilityEvent#originStackTrace} to provide the stack trace + * of the original {@link #runOrPost} call instead of one for sending the delayed event + * from a looper. + */ + public StackTraceElement[] mOrigin; @Override public void run() { @@ -8322,6 +8328,7 @@ public final class ViewRootImpl implements ViewParent, AccessibilityEvent event = AccessibilityEvent.obtain(); event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); event.setContentChangeTypes(mChangeTypes); + if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin; source.sendAccessibilityEventUnchecked(event); } else { mLastEventTimeMillis = 0; @@ -8329,6 +8336,7 @@ public final class ViewRootImpl implements ViewParent, // In any case reset to initial state. source.resetSubtreeAccessibilityStateChanged(); mChangeTypes = 0; + if (AccessibilityEvent.DEBUG_ORIGIN) mOrigin = null; } public void runOrPost(View source, int changeType) { @@ -8352,12 +8360,18 @@ public final class ViewRootImpl implements ViewParent, // If there is no common predecessor, then mSource points to // a removed view, hence in this case always prefer the source. View predecessor = getCommonPredecessor(mSource, source); + if (predecessor != null) { + predecessor = predecessor.getSelfOrParentImportantForA11y(); + } mSource = (predecessor != null) ? predecessor : source; mChangeTypes |= changeType; return; } mSource = source; mChangeTypes = changeType; + if (AccessibilityEvent.DEBUG_ORIGIN) { + mOrigin = Thread.currentThread().getStackTrace(); + } final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis; final long minEventIntevalMillis = ViewConfiguration.getSendRecurringAccessibilityEventsInterval(); diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java index f6181d70..0f5c23f7 100644 --- a/android/view/WindowManager.java +++ b/android/view/WindowManager.java @@ -2438,13 +2438,6 @@ public interface WindowManager extends ViewManager { public long hideTimeoutMilliseconds = -1; /** - * A frame number in which changes requested in this layout will be rendered. - * - * @hide - */ - public long frameNumber = -1; - - /** * The color mode requested by this window. The target display may * not be able to honor the request. When the color mode is not set * to {@link ActivityInfo#COLOR_MODE_DEFAULT}, it might override the @@ -2617,7 +2610,6 @@ public interface WindowManager extends ViewManager { TextUtils.writeToParcel(accessibilityTitle, out, parcelableFlags); out.writeInt(mColorMode); out.writeLong(hideTimeoutMilliseconds); - out.writeLong(frameNumber); } public static final Parcelable.Creator<LayoutParams> CREATOR @@ -2674,7 +2666,6 @@ public interface WindowManager extends ViewManager { accessibilityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mColorMode = in.readInt(); hideTimeoutMilliseconds = in.readLong(); - frameNumber = in.readLong(); } @SuppressWarnings({"PointlessBitwiseExpression"}) @@ -2875,10 +2866,6 @@ public interface WindowManager extends ViewManager { changes |= SURFACE_INSETS_CHANGED; } - // The frame number changing is only relevant in the context of other - // changes, and so we don't need to track it with a flag. - frameNumber = o.frameNumber; - if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) { hasManualSurfaceInsets = o.hasManualSurfaceInsets; changes |= SURFACE_INSETS_CHANGED; diff --git a/android/view/WindowManagerGlobal.java b/android/view/WindowManagerGlobal.java index cca66d6b..08c2d0b7 100644 --- a/android/view/WindowManagerGlobal.java +++ b/android/view/WindowManagerGlobal.java @@ -610,6 +610,10 @@ public final class WindowManagerGlobal { ViewRootImpl root = mRoots.get(i); // Client might remove the view by "stopped" event. root.setWindowStopped(stopped); + // Recursively forward stopped state to View's attached + // to this Window rather than the root application token, + // e.g. PopupWindow's. + setStoppedState(root.mAttachInfo.mWindowToken, stopped); } } } diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java index e0f74a7d..7946e9e2 100644 --- a/android/view/accessibility/AccessibilityEvent.java +++ b/android/view/accessibility/AccessibilityEvent.java @@ -201,6 +201,7 @@ import java.util.List; * <em>Properties:</em></br> * <ul> * <li>{@link #getEventType()} - The type of the event.</li> + * <li>{@link #getContentChangeTypes()} - The type of state changes.</li> * <li>{@link #getSource()} - The source info (for registered clients).</li> * <li>{@link #getClassName()} - The class name of the source.</li> * <li>{@link #getPackageName()} - The package name of the source.</li> @@ -388,6 +389,8 @@ import java.util.List; */ public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable { private static final boolean DEBUG = false; + /** @hide */ + public static final boolean DEBUG_ORIGIN = false; /** * Invalid selection/focus position. @@ -748,7 +751,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par private static final int MAX_POOL_SIZE = 10; private static final SynchronizedPool<AccessibilityEvent> sPool = - new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE); + new SynchronizedPool<>(MAX_POOL_SIZE); private @EventType int mEventType; private CharSequence mPackageName; @@ -758,6 +761,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par int mContentChangeTypes; int mWindowChangeTypes; + /** + * The stack trace describing where this event originated from on the app side. + * Only populated if {@link #DEBUG_ORIGIN} is enabled + * Can be inspected(e.g. printed) from an + * {@link android.accessibilityservice.AccessibilityService} to trace where particular events + * are being dispatched from. + * + * @hide + */ + public StackTraceElement[] originStackTrace = null; + private ArrayList<AccessibilityRecord> mRecords; /* @@ -780,6 +794,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mWindowChangeTypes = event.mWindowChangeTypes; mEventTime = event.mEventTime; mPackageName = event.mPackageName; + if (DEBUG_ORIGIN) originStackTrace = event.originStackTrace; } /** @@ -849,16 +864,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } /** - * Gets the bit mask of change types signaled by an - * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent - * multiple change types. + * Gets the bit mask of change types signaled by a + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event or {@link #TYPE_WINDOW_STATE_CHANGED}. A single + * event may represent multiple change types. * * @return The bit mask of change types. One or more of: * <ul> - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + * <li>{@link #CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + * <li>{@link #CONTENT_CHANGE_TYPE_SUBTREE} + * <li>{@link #CONTENT_CHANGE_TYPE_TEXT} + * <li>{@link #CONTENT_CHANGE_TYPE_PANE_TITLE} + * <li>{@link #CONTENT_CHANGE_TYPE_UNDEFINED} * </ul> */ @ContentChangeTypes @@ -877,6 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } case CONTENT_CHANGE_TYPE_SUBTREE: return "CONTENT_CHANGE_TYPE_SUBTREE"; case CONTENT_CHANGE_TYPE_TEXT: return "CONTENT_CHANGE_TYPE_TEXT"; + case CONTENT_CHANGE_TYPE_PANE_TITLE: return "CONTENT_CHANGE_TYPE_PANE_TITLE"; case CONTENT_CHANGE_TYPE_UNDEFINED: return "CONTENT_CHANGE_TYPE_UNDEFINED"; default: return Integer.toHexString(type); } @@ -1104,7 +1121,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public static AccessibilityEvent obtain() { AccessibilityEvent event = sPool.acquire(); - return (event != null) ? event : new AccessibilityEvent(); + if (event == null) event = new AccessibilityEvent(); + if (DEBUG_ORIGIN) event.originStackTrace = Thread.currentThread().getStackTrace(); + return event; } /** @@ -1142,6 +1161,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par record.recycle(); } } + if (DEBUG_ORIGIN) originStackTrace = null; } /** @@ -1164,7 +1184,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par // Read the records. final int recordCount = parcel.readInt(); if (recordCount > 0) { - mRecords = new ArrayList<AccessibilityRecord>(recordCount); + mRecords = new ArrayList<>(recordCount); for (int i = 0; i < recordCount; i++) { AccessibilityRecord record = AccessibilityRecord.obtain(); readAccessibilityRecordFromParcel(record, parcel); @@ -1172,6 +1192,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mRecords.add(record); } } + + if (DEBUG_ORIGIN) { + originStackTrace = new StackTraceElement[parcel.readInt()]; + for (int i = 0; i < originStackTrace.length; i++) { + originStackTrace[i] = new StackTraceElement( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readInt()); + } + } } /** @@ -1227,6 +1258,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par AccessibilityRecord record = mRecords.get(i); writeAccessibilityRecordToParcel(record, parcel, flags); } + + if (DEBUG_ORIGIN) { + if (originStackTrace == null) originStackTrace = Thread.currentThread().getStackTrace(); + parcel.writeInt(originStackTrace.length); + for (StackTraceElement element : originStackTrace) { + parcel.writeString(element.getClassName()); + parcel.writeString(element.getMethodName()); + parcel.writeString(element.getFileName()); + parcel.writeInt(element.getLineNumber()); + } + } } /** @@ -1285,7 +1327,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) { builder.append("; WindowChangeTypes: ").append( - contentChangeTypesToString(mWindowChangeTypes)); + windowChangeTypesToString(mWindowChangeTypes)); } super.appendTo(builder); if (DEBUG || DEBUG_CONCISE_TOSTRING) { diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java index 72af203e..d60c4819 100644 --- a/android/view/accessibility/AccessibilityInteractionClient.java +++ b/android/view/accessibility/AccessibilityInteractionClient.java @@ -326,12 +326,14 @@ public final class AccessibilityInteractionClient accessibilityWindowId, accessibilityNodeId); if (cachedInfo != null) { if (DEBUG) { - Log.i(LOG_TAG, "Node cache hit"); + Log.i(LOG_TAG, "Node cache hit for " + + idToString(accessibilityWindowId, accessibilityNodeId)); } return cachedInfo; } if (DEBUG) { - Log.i(LOG_TAG, "Node cache miss"); + Log.i(LOG_TAG, "Node cache miss for " + + idToString(accessibilityWindowId, accessibilityNodeId)); } } final int interactionId = mInteractionIdCounter.getAndIncrement(); @@ -368,6 +370,11 @@ public final class AccessibilityInteractionClient return null; } + private static String idToString(int accessibilityWindowId, long accessibilityNodeId) { + return accessibilityWindowId + "/" + + AccessibilityNodeInfo.idToString(accessibilityNodeId); + } + /** * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in * the window whose id is specified and starts from the node whose accessibility diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 84b40641..cbb23f1a 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,48 +16,156 @@ package android.view.accessibility; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; + +import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType; 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 android.view.accessibility.AccessibilityEvent.EventType; +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; + + int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + + boolean mIsTouchExplorationEnabled; - private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); + boolean mIsHighTextContrastEnabled; + AccessibilityPolicy mAccessibilityPolicy; + + 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<>(); /** - * Listener for the accessibility state. + * Map from a view's accessibility id to the list of request preparers set for that view + */ + private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists; + + /** + * Listener for the system accessibility state. To listen for changes to the + * accessibility state on the device, implement this interface and register + * it with the system by calling {@link #addAccessibilityStateChangeListener}. */ 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); } /** @@ -73,7 +181,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); } /** @@ -81,6 +206,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 { @@ -89,7 +216,7 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - public void onHighTextContrastStateChanged(boolean enabled); + void onHighTextContrastStateChanged(boolean enabled); } /** @@ -148,21 +275,67 @@ public final class AccessibilityManager { 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 = context.getUserId(); + } + sInstance = new AccessibilityManager(context, null, userId); + } + } return sInstance; } @@ -170,21 +343,65 @@ 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) { + return mIsEnabled || (mAccessibilityPolicy != null + && mAccessibilityPolicy.isEnabled(mIsEnabled)); + } } /** @@ -193,7 +410,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; + } } /** @@ -203,47 +426,188 @@ 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}. - */ - public void sendAccessibilityEvent(AccessibilityEvent event) { - } - - /** - * Returns whether there are observers registered for this event type. If - * this method returns false you shuold not generate events of this type - * to conserve resources. * - * @param type The event type. - * @return Whether the event is being observed. + * @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 boolean isObservedEventType(@AccessibilityEvent.EventType int type) { - return false; + public void sendAccessibilityEvent(AccessibilityEvent event) { + final IAccessibilityManager service; + final int userId; + final AccessibilityEvent dispatchedEvent; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + event.setEventTime(SystemClock.uptimeMillis()); + if (mAccessibilityPolicy != null) { + dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event, + mIsEnabled, mRelevantEventTypes); + if (dispatchedEvent == null) { + return; + } + } else { + dispatchedEvent = event; + } + if (!isEnabled()) { + 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 ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) { + if (DEBUG) { + Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent + + " that is not among " + + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); + } + return; + } + userId = mUserId; + } + try { + // 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(); + try { + service.sendAccessibilityEvent(dispatchedEvent, userId); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (DEBUG) { + Log.i(LOG_TAG, dispatchedEvent + " sent"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re); + } finally { + if (event != dispatchedEvent) { + event.recycle(); + } + dispatchedEvent.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 (!isEnabled()) { + 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 (mAccessibilityPolicy != null) { + services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** @@ -258,21 +622,52 @@ 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 (mAccessibilityPolicy != null) { + services = mAccessibilityPolicy.getEnabledAccessibilityServiceList( + feedbackTypeFlags, services); + } + 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; } @@ -286,22 +681,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; } @@ -315,17 +728,104 @@ 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) { + synchronized (mLock) { + 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); } /** @@ -337,7 +837,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}. @@ -347,7 +852,64 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) {} + @NonNull HighTextContrastChangeListener listener) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners.remove(listener); + } + } + + /** + * Sets the {@link AccessibilityPolicy} controlling this manager. + * + * @param policy The policy. + * + * @hide + */ + public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) { + synchronized (mLock) { + mAccessibilityPolicy = policy; + } + } + + /** + * 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. @@ -355,14 +917,312 @@ 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 = isEnabled(); + final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; + final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; + + // Ensure listeners get current state from isZzzEnabled() calls. + mIsEnabled = enabled; + mIsTouchExplorationEnabled = touchExplorationEnabled; + mIsHighTextContrastEnabled = highTextContrastEnabled; + + if (wasEnabled != isEnabled()) { + 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) { + String packageName, 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, + packageName, 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 = isEnabled(); + listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityStateChangeListener listener = listeners.keyAt(i); + listeners.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); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final TouchExplorationStateChangeListener listener = listeners.keyAt(i); + listeners.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); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final HighTextContrastChangeListener listener = listeners.keyAt(i); + listeners.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/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java index 4c437dd4..03f1c124 100644 --- a/android/view/accessibility/AccessibilityNodeInfo.java +++ b/android/view/accessibility/AccessibilityNodeInfo.java @@ -3874,6 +3874,24 @@ public class AccessibilityNodeInfo implements Parcelable { | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null); } + /** @hide */ + public static String idToString(long accessibilityId) { + int accessibilityViewId = getAccessibilityViewId(accessibilityId); + int virtualDescendantId = getVirtualDescendantId(accessibilityId); + return virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID + ? idItemToString(accessibilityViewId) + : idItemToString(accessibilityViewId) + ":" + idItemToString(virtualDescendantId); + } + + private static String idItemToString(int item) { + switch (item) { + case ROOT_ITEM_ID: return "ROOT"; + case UNDEFINED_ITEM_ID: return "UNDEFINED"; + case AccessibilityNodeProvider.HOST_VIEW_ID: return "HOST"; + default: return "" + item; + } + } + /** * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}. * Each action has a unique id that is mandatory and optional data. diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java index 1da998d0..a6495d15 100644 --- a/android/view/autofill/AutofillPopupWindow.java +++ b/android/view/autofill/AutofillPopupWindow.java @@ -79,6 +79,11 @@ public class AutofillPopupWindow extends PopupWindow { public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) { mWindowPresenter = new WindowPresenter(presenter); + // We want to show the window as system controlled one so it covers app windows, but it has + // to be an application type (so it's contained inside the application area). + // Hence, we set it to the application type with the highest z-order, which currently + // is TYPE_APPLICATION_ABOVE_SUB_PANEL. + setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); setTouchModal(false); setOutsideTouchable(true); setInputMethodMode(INPUT_METHOD_NOT_NEEDED); diff --git a/android/view/inputmethod/BaseInputConnection.java b/android/view/inputmethod/BaseInputConnection.java index 5f7a0f78..090e19f9 100644 --- a/android/view/inputmethod/BaseInputConnection.java +++ b/android/view/inputmethod/BaseInputConnection.java @@ -522,7 +522,7 @@ public class BaseInputConnection implements InputConnection { b = tmp; } - if (a == b) return null; + if (a == b || a < 0) return null; if ((flags&GET_TEXT_WITH_STYLES) != 0) { return content.subSequence(a, b); diff --git a/android/view/textclassifier/GenerateLinksLogger.java b/android/view/textclassifier/GenerateLinksLogger.java index 73cf43b8..067513f1 100644 --- a/android/view/textclassifier/GenerateLinksLogger.java +++ b/android/view/textclassifier/GenerateLinksLogger.java @@ -19,13 +19,13 @@ package android.view.textclassifier; import android.annotation.Nullable; import android.metrics.LogMaker; import android.util.ArrayMap; -import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Random; @@ -39,6 +39,7 @@ import java.util.UUID; public final class GenerateLinksLogger { private static final String LOG_TAG = "GenerateLinksLogger"; + private static final boolean DEBUG_LOG_ENABLED = false; private static final String ZERO = "0"; private final MetricsLogger mMetricsLogger; @@ -127,7 +128,7 @@ public final class GenerateLinksLogger { } private static void debugLog(LogMaker log) { - if (!Logger.DEBUG_LOG_ENABLED) return; + if (!DEBUG_LOG_ENABLED) return; final String callId = Objects.toString( log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), ""); @@ -142,8 +143,9 @@ public final class GenerateLinksLogger { final int latencyMs = Integer.parseInt( Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO)); - Log.d(LOG_TAG, String.format("%s:%s %d links (%d/%d chars) %dms %s", callId, entityType, - numLinks, linkLength, textLength, latencyMs, log.getPackageName())); + Log.d(LOG_TAG, + String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType, + numLinks, linkLength, textLength, latencyMs, log.getPackageName())); } /** Helper class for storing per-entity type statistics. */ diff --git a/android/view/textclassifier/Logger.java b/android/view/textclassifier/Logger.java deleted file mode 100644 index f03906a0..00000000 --- a/android/view/textclassifier/Logger.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view.textclassifier; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; - -import com.android.internal.util.Preconditions; - -import java.text.BreakIterator; -import java.util.Locale; -import java.util.Objects; - -/** - * A helper for logging TextClassifier related events. - * @hide - */ -public abstract class Logger { - - private static final String LOG_TAG = "Logger"; - /* package */ static final boolean DEBUG_LOG_ENABLED = true; - - private @SelectionEvent.InvocationMethod int mInvocationMethod; - private SelectionEvent mPrevEvent; - private SelectionEvent mSmartEvent; - private SelectionEvent mStartEvent; - - /** - * Logger that does not log anything. - * @hide - */ - public static final Logger DISABLED = new Logger() { - @Override - public void writeEvent(SelectionEvent event) {} - }; - - @Nullable - private final Config mConfig; - - public Logger(Config config) { - mConfig = Preconditions.checkNotNull(config); - } - - private Logger() { - mConfig = null; - } - - /** - * Writes the selection event to a log. - */ - public abstract void writeEvent(@NonNull SelectionEvent event); - - /** - * Returns true if the resultId matches that of a smart selection event (i.e. - * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or - * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}). - * Returns false otherwise. - */ - public boolean isSmartSelection(@NonNull String resultId) { - return false; - } - - /** - * Returns a token iterator for tokenizing text for logging purposes. - */ - public BreakIterator getTokenIterator(@NonNull Locale locale) { - return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale)); - } - - /** - * Logs a "selection started" event. - * - * @param invocationMethod the way the selection was triggered - * @param start the token index of the selected token - */ - public final void logSelectionStartedEvent( - @SelectionEvent.InvocationMethod int invocationMethod, int start) { - if (mConfig == null) { - return; - } - - mInvocationMethod = invocationMethod; - logEvent(new SelectionEvent( - start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED, - TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig)); - } - - /** - * Logs a "selection modified" event. - * Use when the user modifies the selection. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - */ - public final void logSelectionModifiedEvent(int start, int end) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - - if (mConfig == null) { - return; - } - - logEvent(new SelectionEvent( - start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig)); - } - - /** - * Logs a "selection modified" event. - * Use when the user modifies the selection and the selection's entity type is known. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param classification the TextClassification object returned by the TextClassifier that - * classified the selected text - */ - public final void logSelectionModifiedEvent( - int start, int end, @NonNull TextClassification classification) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - Preconditions.checkNotNull(classification); - - if (mConfig == null) { - return; - } - - final String entityType = classification.getEntityCount() > 0 - ? classification.getEntity(0) - : TextClassifier.TYPE_UNKNOWN; - logEvent(new SelectionEvent( - start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - entityType, mInvocationMethod, classification.getId(), mConfig)); - } - - /** - * Logs a "selection modified" event. - * Use when a TextClassifier modifies the selection. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param selection the TextSelection object returned by the TextClassifier for the - * specified selection - */ - public final void logSelectionModifiedEvent( - int start, int end, @NonNull TextSelection selection) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - Preconditions.checkNotNull(selection); - - if (mConfig == null) { - return; - } - - final int eventType; - if (isSmartSelection(selection.getId())) { - eventType = end - start > 1 - ? SelectionEvent.EVENT_SMART_SELECTION_MULTI - : SelectionEvent.EVENT_SMART_SELECTION_SINGLE; - - } else { - eventType = SelectionEvent.EVENT_AUTO_SELECTION; - } - final String entityType = selection.getEntityCount() > 0 - ? selection.getEntity(0) - : TextClassifier.TYPE_UNKNOWN; - logEvent(new SelectionEvent(start, end, eventType, entityType, mInvocationMethod, - selection.getId(), mConfig)); - } - - /** - * Logs an event specifying an action taken on a selection. - * Use when the user clicks on an action to act on the selected text. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param actionType the action that was performed on the selection - */ - public final void logSelectionActionEvent( - int start, int end, @SelectionEvent.ActionType int actionType) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - checkActionType(actionType); - - if (mConfig == null) { - return; - } - - logEvent(new SelectionEvent( - start, end, actionType, TextClassifier.TYPE_UNKNOWN, mInvocationMethod, - null, mConfig)); - } - - /** - * Logs an event specifying an action taken on a selection. - * Use when the user clicks on an action to act on the selected text and the selection's - * entity type is known. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param actionType the action that was performed on the selection - * @param classification the TextClassification object returned by the TextClassifier that - * classified the selected text - * - * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType - */ - public final void logSelectionActionEvent( - int start, int end, @SelectionEvent.ActionType int actionType, - @NonNull TextClassification classification) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - Preconditions.checkNotNull(classification); - checkActionType(actionType); - - if (mConfig == null) { - return; - } - - final String entityType = classification.getEntityCount() > 0 - ? classification.getEntity(0) - : TextClassifier.TYPE_UNKNOWN; - logEvent(new SelectionEvent(start, end, actionType, entityType, mInvocationMethod, - classification.getId(), mConfig)); - } - - private void logEvent(@NonNull SelectionEvent event) { - Preconditions.checkNotNull(event); - - if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED - && mStartEvent == null) { - if (DEBUG_LOG_ENABLED) { - Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); - } - return; - } - - final long now = System.currentTimeMillis(); - switch (event.getEventType()) { - case SelectionEvent.EVENT_SELECTION_STARTED: - Preconditions.checkArgument(event.getAbsoluteEnd() == event.getAbsoluteStart() + 1); - event.setSessionId(startNewSession()); - mStartEvent = event; - break; - case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through - case SelectionEvent.EVENT_SMART_SELECTION_MULTI: - mSmartEvent = event; - break; - case SelectionEvent.EVENT_SELECTION_MODIFIED: // fall through - case SelectionEvent.EVENT_AUTO_SELECTION: - if (mPrevEvent != null - && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart() - && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) { - // Selection did not change. Ignore event. - return; - } - break; - default: - // do nothing. - } - - event.setEventTime(now); - if (mStartEvent != null) { - event.setSessionId(mStartEvent.getSessionId()) - .setDurationSinceSessionStart(now - mStartEvent.getEventTime()) - .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) - .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); - } - if (mSmartEvent != null) { - event.setResultId(mSmartEvent.getResultId()) - .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) - .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); - } - if (mPrevEvent != null) { - event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime()) - .setEventIndex(mPrevEvent.getEventIndex() + 1); - } - writeEvent(event); - mPrevEvent = event; - - if (event.isTerminal()) { - endSession(); - } - } - - private TextClassificationSessionId startNewSession() { - endSession(); - return new TextClassificationSessionId(); - } - - private void endSession() { - mPrevEvent = null; - mSmartEvent = null; - mStartEvent = null; - } - - /** - * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType} - */ - private static void checkActionType(@SelectionEvent.EventType int eventType) - throws IllegalArgumentException { - switch (eventType) { - case SelectionEvent.ACTION_OVERTYPE: // fall through - case SelectionEvent.ACTION_COPY: // fall through - case SelectionEvent.ACTION_PASTE: // fall through - case SelectionEvent.ACTION_CUT: // fall through - case SelectionEvent.ACTION_SHARE: // fall through - case SelectionEvent.ACTION_SMART_SHARE: // fall through - case SelectionEvent.ACTION_DRAG: // fall through - case SelectionEvent.ACTION_ABANDON: // fall through - case SelectionEvent.ACTION_SELECT_ALL: // fall through - case SelectionEvent.ACTION_RESET: // fall through - return; - default: - throw new IllegalArgumentException( - String.format(Locale.US, "%d is not an eventType", eventType)); - } - } - - - /** - * A Logger config. - */ - public static final class Config { - - private final String mPackageName; - private final String mWidgetType; - @Nullable private final String mWidgetVersion; - - /** - * @param context Context of the widget the logger logs for - * @param widgetType a name for the widget being logged for. e.g. - * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW} - * @param widgetVersion a string version info for the widget the logger logs for - */ - public Config( - @NonNull Context context, - @TextClassifier.WidgetType String widgetType, - @Nullable String widgetVersion) { - mPackageName = Preconditions.checkNotNull(context).getPackageName(); - mWidgetType = widgetType; - mWidgetVersion = widgetVersion; - } - - /** - * Returns the package name of the application the logger logs for. - */ - public String getPackageName() { - return mPackageName; - } - - /** - * Returns the name for the widget being logged for. e.g. - * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}. - */ - public String getWidgetType() { - return mWidgetType; - } - - /** - * Returns string version info for the logger. This is specific to the text classifier. - */ - @Nullable - public String getWidgetVersion() { - return mWidgetVersion; - } - - @Override - public int hashCode() { - return Objects.hash(mPackageName, mWidgetType, mWidgetVersion); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (!(obj instanceof Config)) { - return false; - } - - final Config other = (Config) obj; - return Objects.equals(mPackageName, other.mPackageName) - && Objects.equals(mWidgetType, other.mWidgetType) - && Objects.equals(mWidgetVersion, other.mWidgetType); - } - } -} diff --git a/android/view/textclassifier/SelectionEvent.java b/android/view/textclassifier/SelectionEvent.java index 1e978ccf..b0735969 100644 --- a/android/view/textclassifier/SelectionEvent.java +++ b/android/view/textclassifier/SelectionEvent.java @@ -150,20 +150,6 @@ public final class SelectionEvent implements Parcelable { mInvocationMethod = invocationMethod; } - SelectionEvent( - int start, int end, - @EventType int eventType, @EntityType String entityType, - @InvocationMethod int invocationMethod, @Nullable String resultId, - Logger.Config config) { - this(start, end, eventType, entityType, invocationMethod, resultId); - Preconditions.checkNotNull(config); - setTextClassificationSessionContext( - new TextClassificationContext.Builder( - config.getPackageName(), config.getWidgetType()) - .setWidgetVersion(config.getWidgetVersion()) - .build()); - } - private SelectionEvent(Parcel in) { mAbsoluteStart = in.readInt(); mAbsoluteEnd = in.readInt(); @@ -362,6 +348,7 @@ public final class SelectionEvent implements Parcelable { case SelectionEvent.ACTION_ABANDON: // fall through case SelectionEvent.ACTION_SELECT_ALL: // fall through case SelectionEvent.ACTION_RESET: // fall through + case SelectionEvent.ACTION_OTHER: // fall through return; default: throw new IllegalArgumentException( @@ -667,4 +654,4 @@ public final class SelectionEvent implements Parcelable { return new SelectionEvent[size]; } }; -}
\ No newline at end of file +} diff --git a/android/view/textclassifier/DefaultLogger.java b/android/view/textclassifier/SelectionSessionLogger.java index 203ca560..f2fb63eb 100644 --- a/android/view/textclassifier/DefaultLogger.java +++ b/android/view/textclassifier/SelectionSessionLogger.java @@ -17,28 +17,29 @@ package android.view.textclassifier; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.metrics.LogMaker; -import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; +import java.text.BreakIterator; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; /** - * Default Logger. - * Used internally by TextClassifierImpl. + * A helper for logging selection session events. * @hide */ -public final class DefaultLogger extends Logger { +public final class SelectionSessionLogger { - private static final String LOG_TAG = "DefaultLogger"; + private static final String LOG_TAG = "SelectionSessionLogger"; + private static final boolean DEBUG_LOG_ENABLED = false; static final String CLASSIFIER_ID = "androidtc"; private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; @@ -59,23 +60,16 @@ public final class DefaultLogger extends Logger { private final MetricsLogger mMetricsLogger; - public DefaultLogger(@NonNull Config config) { - super(config); + public SelectionSessionLogger() { mMetricsLogger = new MetricsLogger(); } @VisibleForTesting - public DefaultLogger(@NonNull Config config, @NonNull MetricsLogger metricsLogger) { - super(config); + public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) { mMetricsLogger = Preconditions.checkNotNull(metricsLogger); } - @Override - public boolean isSmartSelection(@NonNull String signature) { - return CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature)); - } - - @Override + /** Emits a selection event to the logs. */ public void writeEvent(@NonNull SelectionEvent event) { Preconditions.checkNotNull(event); final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) @@ -93,7 +87,7 @@ public final class DefaultLogger extends Logger { .addTaggedData(SMART_END, event.getSmartEnd()) .addTaggedData(EVENT_START, event.getStart()) .addTaggedData(EVENT_END, event.getEnd()) - .addTaggedData(SESSION_ID, event.getSessionId()); + .addTaggedData(SESSION_ID, event.getSessionId().flattenToString()); mMetricsLogger.write(log); debugLog(log); } @@ -225,9 +219,17 @@ public final class DefaultLogger extends Logger { final int eventEnd = Integer.parseInt( Objects.toString(log.getTaggedData(EVENT_END), ZERO)); - Log.d(LOG_TAG, String.format("%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", - index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, widget, - model)); + Log.d(LOG_TAG, + String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", + index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, + widget, model)); + } + + /** + * Returns a token iterator for tokenizing text for logging purposes. + */ + public static BreakIterator getTokenIterator(@NonNull Locale locale) { + return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale)); } /** @@ -260,8 +262,10 @@ public final class DefaultLogger extends Logger { return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash); } - static String getClassifierId(String signature) { - Preconditions.checkNotNull(signature); + static String getClassifierId(@Nullable String signature) { + if (signature == null) { + return ""; + } final int end = signature.indexOf("|"); if (end >= 0) { return signature.substring(0, end); @@ -269,8 +273,10 @@ public final class DefaultLogger extends Logger { return ""; } - static String getModelName(String signature) { - Preconditions.checkNotNull(signature); + static String getModelName(@Nullable String signature) { + if (signature == null) { + return ""; + } final int start = signature.indexOf("|") + 1; final int end = signature.indexOf("|", start); if (start >= 1 && end >= start) { @@ -279,8 +285,10 @@ public final class DefaultLogger extends Logger { return ""; } - static int getHash(String signature) { - Preconditions.checkNotNull(signature); + static int getHash(@Nullable String signature) { + if (signature == null) { + return 0; + } final int index1 = signature.indexOf("|"); final int index2 = signature.indexOf("|", index1); if (index2 > 0) { diff --git a/android/view/textclassifier/SystemTextClassifier.java b/android/view/textclassifier/SystemTextClassifier.java index 45fd6bfb..490c3890 100644 --- a/android/view/textclassifier/SystemTextClassifier.java +++ b/android/view/textclassifier/SystemTextClassifier.java @@ -28,7 +28,6 @@ import android.service.textclassifier.ITextClassifierService; import android.service.textclassifier.ITextLinksCallback; import android.service.textclassifier.ITextSelectionCallback; -import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.util.Preconditions; @@ -49,13 +48,6 @@ public final class SystemTextClassifier implements TextClassifier { private final TextClassificationConstants mSettings; private final TextClassifier mFallback; private final String mPackageName; - - private final Object mLoggerLock = new Object(); - @GuardedBy("mLoggerLock") - private Logger.Config mLoggerConfig; - @GuardedBy("mLoggerLock") - private Logger mLogger; - @GuardedBy("mLoggerLock") private TextClassificationSessionId mSessionId; public SystemTextClassifier(Context context, TextClassificationConstants settings) @@ -147,27 +139,6 @@ public final class SystemTextClassifier implements TextClassifier { } @Override - public Logger getLogger(@NonNull Logger.Config config) { - Preconditions.checkNotNull(config); - synchronized (mLoggerLock) { - if (mLogger == null || !config.equals(mLoggerConfig)) { - mLoggerConfig = config; - mLogger = new Logger(config) { - @Override - public void writeEvent(SelectionEvent event) { - try { - mManagerService.onSelectionEvent(mSessionId, event); - } catch (RemoteException e) { - Log.e(LOG_TAG, "Error reporting selection event.", e); - } - } - }; - } - } - return mLogger; - } - - @Override public void destroy() { try { if (mSessionId != null) { diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java index 37a5d9a1..96016b44 100644 --- a/android/view/textclassifier/TextClassification.java +++ b/android/view/textclassifier/TextClassification.java @@ -375,13 +375,13 @@ public final class TextClassification implements Parcelable { */ public static final class Builder { - @NonNull private String mText; @NonNull private List<RemoteAction> mActions = new ArrayList<>(); @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); - @Nullable Drawable mLegacyIcon; - @Nullable String mLegacyLabel; - @Nullable Intent mLegacyIntent; - @Nullable OnClickListener mLegacyOnClickListener; + @Nullable private String mText; + @Nullable private Drawable mLegacyIcon; + @Nullable private String mLegacyLabel; + @Nullable private Intent mLegacyIntent; + @Nullable private OnClickListener mLegacyOnClickListener; @Nullable private String mId; /** @@ -721,4 +721,67 @@ public final class TextClassification implements Parcelable { mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); mId = in.readString(); } + + // TODO: Remove once apps can build against the latest sdk. + /** + * Optional input parameters for generating TextClassification. + * @hide + */ + public static final class Options { + + @Nullable private final TextClassificationSessionId mSessionId; + @Nullable private final Request mRequest; + @Nullable private LocaleList mDefaultLocales; + @Nullable private ZonedDateTime mReferenceTime; + + public Options() { + this(null, null); + } + + private Options( + @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { + mSessionId = sessionId; + mRequest = request; + } + + /** Helper to create Options from a Request. */ + public static Options from(TextClassificationSessionId sessionId, Request request) { + final Options options = new Options(sessionId, request); + options.setDefaultLocales(request.getDefaultLocales()); + options.setReferenceTime(request.getReferenceTime()); + return options; + } + + /** @param defaultLocales ordered list of locale preferences. */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** @param referenceTime refrence time used for interpreting relatives dates */ + public Options setReferenceTime(@Nullable ZonedDateTime referenceTime) { + mReferenceTime = referenceTime; + return this; + } + + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + @Nullable + public ZonedDateTime getReferenceTime() { + return mReferenceTime; + } + + @Nullable + public Request getRequest() { + return mRequest; + } + + @Nullable + public TextClassificationSessionId getSessionId() { + return mSessionId; + } + } } diff --git a/android/view/textclassifier/TextClassificationSession.java b/android/view/textclassifier/TextClassificationSession.java index e8e300a9..4c641985 100644 --- a/android/view/textclassifier/TextClassificationSession.java +++ b/android/view/textclassifier/TextClassificationSession.java @@ -17,7 +17,6 @@ package android.view.textclassifier; import android.annotation.WorkerThread; -import android.view.textclassifier.DefaultLogger.SignatureParser; import android.view.textclassifier.SelectionEvent.InvocationMethod; import com.android.internal.util.Preconditions; @@ -222,7 +221,8 @@ final class TextClassificationSession implements TextClassifier { } private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) { - return DefaultLogger.CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature)); + return SelectionSessionLogger.CLASSIFIER_ID.equals( + SelectionSessionLogger.SignatureParser.getClassifierId(signature)); } } } diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java index 54261be3..da47bcb1 100644 --- a/android/view/textclassifier/TextClassifier.java +++ b/android/view/textclassifier/TextClassifier.java @@ -41,8 +41,9 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * Interface for providing text classification related features. @@ -208,6 +209,26 @@ public interface TextClassifier { return suggestSelection(request); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + default TextSelection suggestSelection( + @NonNull CharSequence text, + @IntRange(from = 0) int selectionStartIndex, + @IntRange(from = 0) int selectionEndIndex, + @Nullable TextSelection.Options options) { + if (options == null) { + return suggestSelection(new TextSelection.Request.Builder( + text, selectionStartIndex, selectionEndIndex).build()); + } else if (options.getRequest() != null) { + return suggestSelection(options.getRequest()); + } else { + return suggestSelection( + new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex) + .setDefaultLocales(options.getDefaultLocales()) + .build()); + } + } + /** * Classifies the specified text and returns a {@link TextClassification} object that can be * used to generate a widget for handling the classified text. @@ -267,6 +288,26 @@ public interface TextClassifier { return classifyText(request); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + default TextClassification classifyText( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex, + @Nullable TextClassification.Options options) { + if (options == null) { + return classifyText( + new TextClassification.Request.Builder(text, startIndex, endIndex).build()); + } else if (options.getRequest() != null) { + return classifyText(options.getRequest()); + } else { + return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex) + .setDefaultLocales(options.getDefaultLocales()) + .setReferenceTime(options.getReferenceTime()) + .build()); + } + } + /** * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. @@ -288,6 +329,22 @@ public interface TextClassifier { return new TextLinks.Builder(request.getText().toString()).build(); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + default TextLinks generateLinks( + @NonNull CharSequence text, @Nullable TextLinks.Options options) { + if (options == null) { + return generateLinks(new TextLinks.Request.Builder(text).build()); + } else if (options.getRequest() != null) { + return generateLinks(options.getRequest()); + } else { + return generateLinks(new TextLinks.Request.Builder(text) + .setDefaultLocales(options.getDefaultLocales()) + .setEntityConfig(options.getEntityConfig()) + .build()); + } + } + /** * Returns the maximal length of text that can be processed by generateLinks. * @@ -302,18 +359,6 @@ public interface TextClassifier { } /** - * Returns a helper for logging TextClassifier related events. - * - * @param config logger configuration - * @hide - */ - @WorkerThread - default Logger getLogger(@NonNull Logger.Config config) { - Preconditions.checkNotNull(config); - return Logger.DISABLED; - } - - /** * Reports a selection event. * * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should @@ -377,6 +422,12 @@ public interface TextClassifier { /* includedEntityTypes */null, /* excludedEntityTypes */ null); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public static EntityConfig create(@Nullable Collection<String> hints) { + return createWithHints(hints); + } + /** * Creates an EntityConfig. * @@ -406,6 +457,12 @@ public interface TextClassifier { /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) { + return createWithExplicitEntityList(entityTypes); + } + /** * Returns a list of the final set of entities to find. * @@ -413,21 +470,15 @@ public interface TextClassifier { * * This method is intended for use by TextClassifier implementations. */ - public List<String> resolveEntityListModifications(@NonNull Collection<String> entities) { - final ArrayList<String> finalList = new ArrayList<>(); + public Collection<String> resolveEntityListModifications( + @NonNull Collection<String> entities) { + final Set<String> finalSet = new HashSet(); if (mUseHints) { - for (String entity : entities) { - if (!mExcludedEntityTypes.contains(entity)) { - finalList.add(entity); - } - } - } - for (String entity : mIncludedEntityTypes) { - if (!mExcludedEntityTypes.contains(entity) && !finalList.contains(entity)) { - finalList.add(entity); - } + finalSet.addAll(entities); } - return finalList; + finalSet.addAll(mIncludedEntityTypes); + finalSet.removeAll(mExcludedEntityTypes); + return finalSet; } /** @@ -508,7 +559,7 @@ public interface TextClassifier { final String string = request.getText().toString(); final TextLinks.Builder links = new TextLinks.Builder(string); - final List<String> entities = request.getEntityConfig() + final Collection<String> entities = request.getEntityConfig() .resolveEntityListModifications(Collections.emptyList()); if (entities.contains(TextClassifier.TYPE_URL)) { addLinks(links, string, TextClassifier.TYPE_URL); diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index 7e3748ae..22133558 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -94,11 +94,7 @@ public final class TextClassifierImpl implements TextClassifier { private final Object mLoggerLock = new Object(); @GuardedBy("mLoggerLock") // Do not access outside this lock. - private Logger.Config mLoggerConfig; - @GuardedBy("mLoggerLock") // Do not access outside this lock. - private Logger mLogger; - @GuardedBy("mLoggerLock") // Do not access outside this lock. - private Logger mLogger2; // This is the new logger. Will replace mLogger. + private SelectionSessionLogger mSessionLogger; private final TextClassificationConstants mSettings; @@ -283,28 +279,14 @@ public final class TextClassifierImpl implements TextClassifier { } } - /** @inheritDoc */ - @Override - public Logger getLogger(@NonNull Logger.Config config) { - Preconditions.checkNotNull(config); - synchronized (mLoggerLock) { - if (mLogger == null || !config.equals(mLoggerConfig)) { - mLoggerConfig = config; - mLogger = new DefaultLogger(config); - } - } - return mLogger; - } - @Override public void onSelectionEvent(SelectionEvent event) { Preconditions.checkNotNull(event); synchronized (mLoggerLock) { - if (mLogger2 == null) { - mLogger2 = new DefaultLogger( - new Logger.Config(mContext, WIDGET_TYPE_UNKNOWN, null)); + if (mSessionLogger == null) { + mSessionLogger = new SelectionSessionLogger(); } - mLogger2.writeEvent(event); + mSessionLogger.writeEvent(event); } } @@ -331,7 +313,7 @@ public final class TextClassifierImpl implements TextClassifier { private String createId(String text, int start, int end) { synchronized (mLock) { - return DefaultLogger.createId(text, start, end, mContext, mModel.getVersion(), + return SelectionSessionLogger.createId(text, start, end, mContext, mModel.getVersion(), mModel.getSupportedLocales()); } } diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java index 17c7b13c..851b2c9b 100644 --- a/android/view/textclassifier/TextLinks.java +++ b/android/view/textclassifier/TextLinks.java @@ -28,6 +28,8 @@ import android.text.Spannable; import android.text.method.MovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.text.util.Linkify.LinkifyMask; import android.view.View; import android.view.textclassifier.TextClassifier.EntityType; import android.widget.TextView; @@ -337,7 +339,7 @@ public final class TextLinks implements Parcelable { /** * @return The config representing the set of entities to look for - * @see #setEntityConfig(TextClassifier.EntityConfig) + * @see Builder#setEntityConfig(TextClassifier.EntityConfig) */ @Nullable public TextClassifier.EntityConfig getEntityConfig() { @@ -607,4 +609,124 @@ public final class TextLinks implements Parcelable { return new TextLinks(mFullText, mLinks); } } + + // TODO: Remove once apps can build against the latest sdk. + /** + * Optional input parameters for generating TextLinks. + * @hide + */ + public static final class Options { + + @Nullable private final TextClassificationSessionId mSessionId; + @Nullable private final Request mRequest; + @Nullable private LocaleList mDefaultLocales; + @Nullable private TextClassifier.EntityConfig mEntityConfig; + private boolean mLegacyFallback; + + private @ApplyStrategy int mApplyStrategy; + private Function<TextLink, TextLinkSpan> mSpanFactory; + + private String mCallingPackageName; + + public Options() { + this(null, null); + } + + private Options( + @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { + mSessionId = sessionId; + mRequest = request; + } + + /** Helper to create Options from a Request. */ + public static Options from(TextClassificationSessionId sessionId, Request request) { + final Options options = new Options(sessionId, request); + options.setDefaultLocales(request.getDefaultLocales()); + options.setEntityConfig(request.getEntityConfig()); + return options; + } + + /** Returns a new options object based on the specified link mask. */ + public static Options fromLinkMask(@LinkifyMask int mask) { + final List<String> entitiesToFind = new ArrayList<>(); + + if ((mask & Linkify.WEB_URLS) != 0) { + entitiesToFind.add(TextClassifier.TYPE_URL); + } + if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { + entitiesToFind.add(TextClassifier.TYPE_EMAIL); + } + if ((mask & Linkify.PHONE_NUMBERS) != 0) { + entitiesToFind.add(TextClassifier.TYPE_PHONE); + } + if ((mask & Linkify.MAP_ADDRESSES) != 0) { + entitiesToFind.add(TextClassifier.TYPE_ADDRESS); + } + + return new Options().setEntityConfig( + TextClassifier.EntityConfig.createWithEntityList(entitiesToFind)); + } + + /** @param defaultLocales ordered list of locale preferences. */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** @param entityConfig definition of which entity types to look for. */ + public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { + mEntityConfig = entityConfig; + return this; + } + + /** @param applyStrategy strategy to use when resolving conflicts. */ + public Options setApplyStrategy(@ApplyStrategy int applyStrategy) { + checkValidApplyStrategy(applyStrategy); + mApplyStrategy = applyStrategy; + return this; + } + + /** @param spanFactory factory for converting TextLink to TextLinkSpan. */ + public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { + mSpanFactory = spanFactory; + return this; + } + + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + @Nullable + public TextClassifier.EntityConfig getEntityConfig() { + return mEntityConfig; + } + + @ApplyStrategy + public int getApplyStrategy() { + return mApplyStrategy; + } + + @Nullable + public Function<TextLink, TextLinkSpan> getSpanFactory() { + return mSpanFactory; + } + + @Nullable + public Request getRequest() { + return mRequest; + } + + @Nullable + public TextClassificationSessionId getSessionId() { + return mSessionId; + } + + private static void checkValidApplyStrategy(int applyStrategy) { + if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) { + throw new IllegalArgumentException( + "Invalid apply strategy. See TextLinks.ApplyStrategy for options."); + } + } + } } diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java index 939e7176..17687c9e 100644 --- a/android/view/textclassifier/TextSelection.java +++ b/android/view/textclassifier/TextSelection.java @@ -375,4 +375,56 @@ public final class TextSelection implements Parcelable { mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); mId = in.readString(); } + + + // TODO: Remove once apps can build against the latest sdk. + /** + * Optional input parameters for generating TextSelection. + * @hide + */ + public static final class Options { + + @Nullable private final TextClassificationSessionId mSessionId; + @Nullable private final Request mRequest; + @Nullable private LocaleList mDefaultLocales; + private boolean mDarkLaunchAllowed; + + public Options() { + this(null, null); + } + + private Options( + @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { + mSessionId = sessionId; + mRequest = request; + } + + /** Helper to create Options from a Request. */ + public static Options from(TextClassificationSessionId sessionId, Request request) { + final Options options = new Options(sessionId, request); + options.setDefaultLocales(request.getDefaultLocales()); + return options; + } + + /** @param defaultLocales ordered list of locale preferences. */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + @Nullable + public Request getRequest() { + return mRequest; + } + + @Nullable + public TextClassificationSessionId getSessionId() { + return mSessionId; + } + } } diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index 8e1f2183..21ec42b1 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,58 +1,219 @@ /* - * 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; + + /** + * A compile time switch to control per-profile spell checker, which is not yet ready. + * @hide + */ + public static final boolean DISABLE_PER_PROFILE_SPELL_CHECKER = true; + + 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 +221,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(); + } } } |