diff options
Diffstat (limited to 'core/java/android')
90 files changed, 5411 insertions, 1497 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index ddd7f7c..3da35d3 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -19,17 +19,22 @@ package android.accessibilityservice; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.util.LocaleUtil; import android.util.Log; +import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.os.HandlerCaller; +import java.util.Locale; + /** * An accessibility service runs in the background and receives callbacks by the system * when {@link AccessibilityEvent}s are fired. Such events denote some state transition @@ -202,12 +207,65 @@ import com.android.internal.os.HandlerCaller; * @see android.view.accessibility.AccessibilityManager */ public abstract class AccessibilityService extends Service { + + /** + * The user has performed a swipe up gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_UP = 1; + + /** + * The user has performed a swipe down gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_DOWN = 2; + + /** + * The user has performed a swipe left gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_LEFT = 3; + + /** + * The user has performed a swipe right gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_RIGHT = 4; + + /** + * The user has performed a swipe left and right gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_LEFT_AND_RIGHT = 5; + + /** + * The user has performed a swipe right and left gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_RIGHT_AND_LEFT = 6; + + /** + * The user has performed a swipe up and down gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_UP_AND_DOWN = 7; + + /** + * The user has performed a swipe down and up gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_DOWN_AND_UP = 8; + + /** + * The user has performed a clockwise circle gesture on the touch screen. + */ + public static final int GESTURE_CLOCKWISE_CIRCLE = 9; + + /** + * The user has performed a counter clockwise circle gesture on the touch screen. + */ + public static final int GESTURE_COUNTER_CLOCKWISE_CIRCLE = 10; + /** * The {@link Intent} that must be declared as handled by the service. */ public static final String SERVICE_INTERFACE = "android.accessibilityservice.AccessibilityService"; + private static final int UNDEFINED = -1; + /** * Name under which an AccessibilityService component publishes information * about itself. This meta-data must reference an XML resource containing an @@ -233,12 +291,15 @@ public abstract class AccessibilityService extends Service { public void onInterrupt(); public void onServiceConnected(); public void onSetConnectionId(int connectionId); + public void onGesture(int gestureId); } private int mConnectionId; private AccessibilityServiceInfo mInfo; + private int mLayoutDirection; + /** * Callback for {@link android.view.accessibility.AccessibilityEvent}s. * @@ -264,6 +325,106 @@ public abstract class AccessibilityService extends Service { } /** + * Called by the system when the user performs a specific gesture on the + * touch screen. + * + * @param gestureId The unique id of the performed gesture. + * + * @see #GESTURE_SWIPE_UP + * @see #GESTURE_SWIPE_DOWN + * @see #GESTURE_SWIPE_LEFT + * @see #GESTURE_SWIPE_RIGHT + * @see #GESTURE_SWIPE_UP_AND_DOWN + * @see #GESTURE_SWIPE_DOWN_AND_UP + * @see #GESTURE_SWIPE_LEFT_AND_RIGHT + * @see #GESTURE_SWIPE_RIGHT_AND_LEFT + * @see #GESTURE_CLOCKWISE_CIRCLE + * @see #GESTURE_COUNTER_CLOCKWISE_CIRCLE + */ + protected void onGesture(int gestureId) { + // TODO: Describe the default gesture processing in the javaDoc once it is finalized. + + // Cache the id to avoid locking + final int connectionId = mConnectionId; + if (connectionId == UNDEFINED) { + throw new IllegalStateException("AccessibilityService not connected." + + " Did you receive a call of onServiceConnected()?"); + } + AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfoByAccessibilityId(connectionId, + AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + if (root == null) { + return; + } + AccessibilityNodeInfo current = root.findFocus(View.FOCUS_ACCESSIBILITY); + if (current == null) { + current = root; + } + AccessibilityNodeInfo next = null; + switch (gestureId) { + case GESTURE_SWIPE_UP: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_OUT); + } break; + case GESTURE_SWIPE_DOWN: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_IN); + } break; + case GESTURE_SWIPE_LEFT: { + if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD); + } else { // LAYOUT_DIRECTION_RTL + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD); + } + } break; + case GESTURE_SWIPE_RIGHT: { + if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD); + } else { // LAYOUT_DIRECTION_RTL + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD); + } + } break; + case GESTURE_SWIPE_UP_AND_DOWN: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_UP); + } break; + case GESTURE_SWIPE_DOWN_AND_UP: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_DOWN); + } break; + case GESTURE_SWIPE_LEFT_AND_RIGHT: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_LEFT); + } break; + case GESTURE_SWIPE_RIGHT_AND_LEFT: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_RIGHT); + } break; + } + if (next != null && !next.equals(current)) { + next.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + } + + /** + * Gets the an {@link AccessibilityServiceInfo} describing this + * {@link AccessibilityService}. This method is useful if one wants + * to change some of the dynamically configurable properties at + * runtime. + * + * @return The accessibility service info. + * + * @see AccessibilityNodeInfo + */ + public final AccessibilityServiceInfo getServiceInfo() { + IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); + if (connection != null) { + try { + return connection.getServiceInfo(); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re); + } + } + return null; + } + + /** * Sets the {@link AccessibilityServiceInfo} that describes this service. * <p> * Note: You can call this method any time but the info will be picked up after @@ -287,19 +448,33 @@ public abstract class AccessibilityService extends Service { if (mInfo != null && connection != null) { try { connection.setServiceInfo(mInfo); + mInfo = null; + AccessibilityInteractionClient.getInstance().clearCache(); } catch (RemoteException re) { Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re); } } } + @Override + public void onCreate() { + Locale locale = getResources().getConfiguration().locale; + mLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(locale); + } + + @Override + public void onConfigurationChanged(Configuration configuration) { + super.onConfigurationChanged(configuration); + mLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(configuration.locale); + } + /** * Implement to return the implementation of the internal accessibility * service interface. */ @Override public final IBinder onBind(Intent intent) { - return new IEventListenerWrapper(this, getMainLooper(), new Callbacks() { + return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() { @Override public void onServiceConnected() { AccessibilityService.this.onServiceConnected(); @@ -319,14 +494,19 @@ public abstract class AccessibilityService extends Service { public void onSetConnectionId( int connectionId) { mConnectionId = connectionId; } + + @Override + public void onGesture(int gestureId) { + AccessibilityService.this.onGesture(gestureId); + } }); } /** - * Implements the internal {@link IEventListener} interface to convert + * Implements the internal {@link IAccessibilityServiceClient} interface to convert * incoming calls to it back to calls on an {@link AccessibilityService}. */ - static class IEventListenerWrapper extends IEventListener.Stub + static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback { static final int NO_ID = -1; @@ -334,12 +514,14 @@ public abstract class AccessibilityService extends Service { private static final int DO_SET_SET_CONNECTION = 10; private static final int DO_ON_INTERRUPT = 20; private static final int DO_ON_ACCESSIBILITY_EVENT = 30; + private static final int DO_ON_GESTURE = 40; private final HandlerCaller mCaller; private final Callbacks mCallback; - public IEventListenerWrapper(Context context, Looper looper, Callbacks callback) { + public IAccessibilityServiceClientWrapper(Context context, Looper looper, + Callbacks callback) { mCallback = callback; mCaller = new HandlerCaller(context, looper, this); } @@ -360,6 +542,11 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(message); } + public void onGesture(int gestureId) { + Message message = mCaller.obtainMessageI(DO_ON_GESTURE, gestureId); + mCaller.sendMessage(message); + } + public void executeMessage(Message message) { switch (message.what) { case DO_ON_ACCESSIBILITY_EVENT : @@ -387,6 +574,10 @@ public abstract class AccessibilityService extends Service { mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); } return; + case DO_ON_GESTURE : + final int gestureId = message.arg1; + mCallback.onGesture(gestureId); + return; default : Log.w(LOG_TAG, "Unknown message type " + message.what); } diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 8e53431..e77ed9a 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -25,11 +25,13 @@ import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.TypedValue; import android.util.Xml; +import android.view.View; import android.view.accessibility.AccessibilityEvent; import org.xmlpull.v1.XmlPullParser; @@ -101,6 +103,37 @@ public class AccessibilityServiceInfo implements Parcelable { public static final int DEFAULT = 0x0000001; /** + * If this flag is set the system will regard views that are not important + * for accessibility in addition to the ones that are important for accessibility. + * That is, views that are marked as not important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO} and views that are marked as + * potentially important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_AUTO} for which the system has determined + * that are not important for accessibility, are both reported while querying the + * window content and also the accessibility service will receive accessibility events + * from them. + * <p> + * <strong>Note:</strong> For accessibility services targeting API version + * {@link Build.VERSION_CODES#JELLY_BEAN} or higher this flag has to be explicitly + * set for the system to regard views that are not important for accessibility. For + * accessibility services targeting API version lower than + * {@link Build.VERSION_CODES#JELLY_BEAN} this flag is ignored and all views are + * regarded for accessibility purposes. + * </p> + * <p> + * Usually views not important for accessibility are layout managers that do not + * react to user actions, do not draw any content, and do not have any special + * semantics in the context of the screen content. For example, a three by three + * grid can be implemented as three horizontal linear layouts and one vertical, + * or three vertical linear layouts and one horizontal, or one grid layout, etc. + * In this context the actual layout mangers used to achieve the grid configuration + * are not important, rather it is important that there are nine evenly distributed + * elements. + * </p> + */ + public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 0x0000002; + + /** * The event types an {@link AccessibilityService} is interested in. * <p> * <strong>Can be dynamically set at runtime.</strong> @@ -165,6 +198,7 @@ public class AccessibilityServiceInfo implements Parcelable { * <strong>Can be dynamically set at runtime.</strong> * </p> * @see #DEFAULT + * @see #INCLUDE_NOT_IMPORTANT_VIEWS */ public int flags; @@ -561,6 +595,8 @@ public class AccessibilityServiceInfo implements Parcelable { switch (flag) { case DEFAULT: return "DEFAULT"; + case INCLUDE_NOT_IMPORTANT_VIEWS: + return "REGARD_VIEWS_NOT_IMPORTANT_FOR_ACCESSIBILITY"; default: return null; } diff --git a/core/java/android/accessibilityservice/IEventListener.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 5536b3c..588728c 100644 --- a/core/java/android/accessibilityservice/IEventListener.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -20,15 +20,17 @@ import android.accessibilityservice.IAccessibilityServiceConnection; import android.view.accessibility.AccessibilityEvent; /** - * Top-level interface to accessibility service component (implemented in Service). + * Top-level interface to an accessibility service component. * * @hide */ - oneway interface IEventListener { + oneway interface IAccessibilityServiceClient { void setConnection(in IAccessibilityServiceConnection connection, int connectionId); void onAccessibilityEvent(in AccessibilityEvent event); void onInterrupt(); + + void onGesture(int gestureId); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 8d17325..30da9db 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -41,13 +41,13 @@ interface IAccessibilityServiceConnection { * to start from the root. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. + * @param flags Additional flags. * @param threadId The id of the calling thread. - * @param prefetchFlags flags to guide prefetching. * @return The current window scale, where zero means a failure. */ float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, long threadId, int prefetchFlags); + IAccessibilityInteractionConnectionCallback callback, int flags, long threadId); /** * Finds {@link android.view.accessibility.AccessibilityNodeInfo}s by View text. @@ -94,6 +94,48 @@ interface IAccessibilityServiceConnection { long threadId); /** + * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the specified + * focus type. The search is performed in the window whose id is specified and starts from + * the node whose accessibility id is specified. + * + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param focusType The type of focus to find. + * @param interactionId The id of the interaction for matching with the callback result. + * @param callback Callback which to receive the result. + * @param threadId The id of the calling thread. + * @return The current window scale, where zero means a failure. + */ + float findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, + int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + + /** + * Finds an {@link android.view.accessibility.AccessibilityNodeInfo} to take accessibility + * focus in the given direction. The search is performed in the window whose id is + * specified and starts from the node whose accessibility id is specified. + * + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param direction The direction in which to search for focusable. + * @param interactionId The id of the interaction for matching with the callback result. + * @param callback Callback which to receive the result. + * @param threadId The id of the calling thread. + * @return The current window scale, where zero means a failure. + */ + float focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, + int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + + /** * Performs an accessibility action on an * {@link android.view.accessibility.AccessibilityNodeInfo}. * @@ -113,4 +155,9 @@ interface IAccessibilityServiceConnection { boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + + /** + * @return The associated accessibility service info. + */ + AccessibilityServiceInfo getServiceInfo(); } diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java index a898c3f..c840bd6 100644 --- a/core/java/android/accessibilityservice/UiTestAutomationBridge.java +++ b/core/java/android/accessibilityservice/UiTestAutomationBridge.java @@ -17,7 +17,7 @@ package android.accessibilityservice; import android.accessibilityservice.AccessibilityService.Callbacks; -import android.accessibilityservice.AccessibilityService.IEventListenerWrapper; +import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; import android.content.Context; import android.os.HandlerThread; import android.os.Looper; @@ -66,7 +66,7 @@ public class UiTestAutomationBridge { private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; - private IEventListenerWrapper mListener; + private IAccessibilityServiceClientWrapper mListener; private AccessibilityEvent mLastEvent; @@ -133,7 +133,7 @@ public class UiTestAutomationBridge { mHandlerThread.start(); Looper looper = mHandlerThread.getLooper(); - mListener = new IEventListenerWrapper(null, looper, new Callbacks() { + mListener = new IAccessibilityServiceClientWrapper(null, looper, new Callbacks() { @Override public void onServiceConnected() { /* do nothing */ @@ -175,6 +175,11 @@ public class UiTestAutomationBridge { mLock.notifyAll(); } } + + @Override + public void onGesture(int gestureId) { + /* do nothing */ + } }); final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( @@ -252,6 +257,7 @@ public class UiTestAutomationBridge { public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, Predicate<AccessibilityEvent> predicate, long timeoutMillis) throws TimeoutException, Exception { + // TODO: This is broken - remove from here when finalizing this as public APIs. synchronized (mLock) { // Prepare to wait for an event. mWaitingForEventDelivery = true; diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index a3fdf3e..7e1589f 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -1000,7 +1000,7 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM } return true; } - + case GOING_TO_SLEEP_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); goingToSleep(); @@ -1015,6 +1015,13 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case SET_LOCK_SCREEN_SHOWN_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + setLockScreenShown(data.readInt() != 0); + reply.writeNoException(); + return true; + } + case SET_DEBUG_APP_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); String pn = data.readString(); @@ -2912,6 +2919,17 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } + public void setLockScreenShown(boolean shown) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(shown ? 1 : 0); + mRemote.transact(SET_LOCK_SCREEN_SHOWN_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } public void setDebugApp( String packageName, boolean waitForDebugger, boolean persistent) throws RemoteException diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 138a88f..0645aa9 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -82,7 +82,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserId; -import android.os.Vibrator; +import android.os.SystemVibrator; import android.os.storage.StorageManager; import android.telephony.TelephonyManager; import android.content.ClipboardManager; @@ -455,7 +455,7 @@ class ContextImpl extends Context { registerService(VIBRATOR_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { - return new Vibrator(); + return new SystemVibrator(); }}); registerService(WALLPAPER_SERVICE, WALLPAPER_FETCHER); diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index dd58397..55f29e6 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -349,6 +349,7 @@ public class DownloadManager { private String mMimeType; private boolean mRoamingAllowed = true; private int mAllowedNetworkTypes = ~0; // default to all network types allowed + private boolean mAllowedOverMetered = true; private boolean mIsVisibleInDownloadsUi = true; private boolean mScannable = false; private boolean mUseSystemCache = false; @@ -609,8 +610,11 @@ public class DownloadManager { } /** - * Restrict the types of networks over which this download may proceed. By default, all - * network types are allowed. + * Restrict the types of networks over which this download may proceed. + * By default, all network types are allowed. Consider using + * {@link #setAllowedOverMetered(boolean)} instead, since it's more + * flexible. + * * @param flags any combination of the NETWORK_* bit flags. * @return this object */ @@ -620,6 +624,17 @@ public class DownloadManager { } /** + * Set whether this download may proceed over a metered network + * connection. By default, metered networks are allowed. + * + * @see ConnectivityManager#isActiveNetworkMetered() + */ + public Request setAllowedOverMetered(boolean allow) { + mAllowedOverMetered = allow; + return this; + } + + /** * Set whether this download may proceed over a roaming connection. By default, roaming is * allowed. * @param allowed whether to allow a roaming connection to be used @@ -672,6 +687,7 @@ public class DownloadManager { putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription); putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType); + // TODO: add COLUMN_ALLOW_METERED and persist values.put(Downloads.Impl.COLUMN_VISIBILITY, mNotificationVisibility); values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes); values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed); diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index c71b186..3fc2280 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -205,7 +205,8 @@ public interface IActivityManager extends IInterface { // Note: probably don't want to allow applications access to these. public void goingToSleep() throws RemoteException; public void wakingUp() throws RemoteException; - + public void setLockScreenShown(boolean shown) throws RemoteException; + public void unhandledBack() throws RemoteException; public ParcelFileDescriptor openContentUri(Uri uri) throws RemoteException; public void setDebugApp( @@ -588,4 +589,5 @@ public interface IActivityManager extends IInterface { int GET_CURRENT_USER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+144; int TARGET_TASK_AFFINITY_MATCHES_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+145; int NAVIGATE_UP_TO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+146; + int SET_LOCK_SCREEN_SHOWN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+147; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 04c64a0..5cce25f 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -196,10 +196,9 @@ public class Notification implements Parcelable public RemoteViews intruderView; /** - * A larger version of {@link #contentView}, giving the Notification an + * A large-format version of {@link #contentView}, giving the Notification an * opportunity to show more detail. The system UI may choose to show this * instead of the normal content view at its discretion. - * @hide */ public RemoteViews bigContentView; @@ -987,8 +986,6 @@ public class Notification implements Parcelable } /** - * @hide - * * Show the {@link Notification#when} field as a countdown (or count-up) timer instead of a timestamp. * * @see Notification#when @@ -1609,23 +1606,21 @@ public class Notification implements Parcelable } /** - * @hide because this API is still very rough + * Helper class for generating large-format notifications that include a large image attachment. * - * This is a "rebuilder": It consumes a Builder object and modifies its output. - * - * This represents the "big picture" style notification, with a large Bitmap atop the usual notification. - * - * Usage: + * This class is a "rebuilder": It consumes a Builder object and modifies its behavior, like so: * <pre class="prettyprint"> * Notification noti = new Notification.BigPictureStyle( * new Notification.Builder() - * .setContentTitle("New mail from " + sender.toString()) + * .setContentTitle("New photo from " + sender.toString()) * .setContentText(subject) - * .setSmallIcon(R.drawable.new_mail) + * .setSmallIcon(R.drawable.new_post) * .setLargeIcon(aBitmap)) * .bigPicture(aBigBitmap) * .build(); * </pre> + * + * @see Notification#bigContentView */ public static class BigPictureStyle { private Builder mBuilder; @@ -1656,13 +1651,9 @@ public class Notification implements Parcelable } /** - * @hide because this API is still very rough - * - * This is a "rebuilder": It consumes a Builder object and modifies its output. + * Helper class for generating large-format notifications that include a lot of text. * - * This represents the "big text" style notification, with more area for the main content text to be read in its entirety. - * - * Usage: + * This class is a "rebuilder": It consumes a Builder object and modifies its behavior, like so: * <pre class="prettyprint"> * Notification noti = new Notification.BigPictureStyle( * new Notification.Builder() @@ -1673,6 +1664,8 @@ public class Notification implements Parcelable * .bigText(aVeryLongString) * .build(); * </pre> + * + * @see Notification#bigContentView */ public static class BigTextStyle { private Builder mBuilder; diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index a655dd4..1866830 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -21,7 +21,12 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.text.Html; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; +import android.text.style.URLSpan; import android.util.Log; import java.io.FileInputStream; @@ -144,6 +149,8 @@ import java.util.ArrayList; public class ClipData implements Parcelable { static final String[] MIMETYPES_TEXT_PLAIN = new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN }; + static final String[] MIMETYPES_TEXT_HTML = new String[] { + ClipDescription.MIMETYPE_TEXT_HTML }; static final String[] MIMETYPES_TEXT_URILIST = new String[] { ClipDescription.MIMETYPE_TEXT_URILIST }; static final String[] MIMETYPES_TEXT_INTENT = new String[] { @@ -176,6 +183,7 @@ public class ClipData implements Parcelable { */ public static class Item { final CharSequence mText; + final String mHtmlText; final Intent mIntent; final Uri mUri; @@ -184,6 +192,20 @@ public class ClipData implements Parcelable { */ public Item(CharSequence text) { mText = text; + mHtmlText = null; + mIntent = null; + mUri = null; + } + + /** + * Create an Item consisting of a single block of (possibly styled) text, + * with an alternative HTML formatted representation. You <em>must</em> + * supply a plain text representation in addition to HTML text; coercion + * will not be done from HTML formated text into plain text. + */ + public Item(CharSequence text, String htmlText) { + mText = text; + mHtmlText = htmlText; mIntent = null; mUri = null; } @@ -193,6 +215,7 @@ public class ClipData implements Parcelable { */ public Item(Intent intent) { mText = null; + mHtmlText = null; mIntent = intent; mUri = null; } @@ -202,16 +225,35 @@ public class ClipData implements Parcelable { */ public Item(Uri uri) { mText = null; + mHtmlText = null; mIntent = null; mUri = uri; } /** * Create a complex Item, containing multiple representations of - * text, intent, and/or URI. + * text, Intent, and/or URI. */ public Item(CharSequence text, Intent intent, Uri uri) { mText = text; + mHtmlText = null; + mIntent = intent; + mUri = uri; + } + + /** + * Create a complex Item, containing multiple representations of + * text, HTML text, Intent, and/or URI. If providing HTML text, you + * <em>must</em> supply a plain text representation as well; coercion + * will not be done from HTML formated text into plain text. + */ + public Item(CharSequence text, String htmlText, Intent intent, Uri uri) { + if (htmlText != null && text == null) { + throw new IllegalArgumentException( + "Plain text must be supplied if HTML text is supplied"); + } + mText = text; + mHtmlText = htmlText; mIntent = intent; mUri = uri; } @@ -224,6 +266,13 @@ public class ClipData implements Parcelable { } /** + * Retrieve the raw HTML text contained in this Item. + */ + public String getHtmlText() { + return mHtmlText; + } + + /** * Retrieve the raw Intent contained in this Item. */ public Intent getIntent() { @@ -250,7 +299,7 @@ public class ClipData implements Parcelable { * the content provider does not supply a text representation, return * the raw URI as a string. * <li> If {@link #getIntent} is non-null, convert that to an intent: - * URI and returnit. + * URI and return it. * <li> Otherwise, return an empty string. * </ul> * @@ -261,12 +310,14 @@ public class ClipData implements Parcelable { //BEGIN_INCLUDE(coerceToText) public CharSequence coerceToText(Context context) { // If this Item has an explicit textual value, simply return that. - if (mText != null) { - return mText; + CharSequence text = getText(); + if (text != null) { + return text; } // If this Item has a URI value, try using that. - if (mUri != null) { + Uri uri = getUri(); + if (uri != null) { // First see if the URI can be opened as a plain text stream // (of any sub-type). If so, this is the best textual @@ -275,7 +326,7 @@ public class ClipData implements Parcelable { try { // Ask for a stream of the desired type. AssetFileDescriptor descr = context.getContentResolver() - .openTypedAssetFileDescriptor(mUri, "text/*", null); + .openTypedAssetFileDescriptor(uri, "text/*", null); stream = descr.createInputStream(); InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); @@ -308,13 +359,14 @@ public class ClipData implements Parcelable { // If we couldn't open the URI as a stream, then the URI itself // probably serves fairly well as a textual representation. - return mUri.toString(); + return uri.toString(); } // Finally, if all we have is an Intent, then we can just turn that // into text. Not the most user-friendly thing, but it's something. - if (mIntent != null) { - return mIntent.toUri(Intent.URI_INTENT_SCHEME); + Intent intent = getIntent(); + if (intent != null) { + return intent.toUri(Intent.URI_INTENT_SCHEME); } // Shouldn't get here, but just in case... @@ -322,6 +374,210 @@ public class ClipData implements Parcelable { } //END_INCLUDE(coerceToText) + /** + * Like {@link #coerceToHtmlText(Context)}, but any text that would + * be returned as HTML formatting will be returned as text with + * style spans. + * @param context The caller's Context, from which its ContentResolver + * and other things can be retrieved. + * @return Returns the item's textual representation. + */ + public CharSequence coerceToStyledText(Context context) { + CharSequence text = getText(); + if (text instanceof Spanned) { + return text; + } + String htmlText = getHtmlText(); + if (htmlText != null) { + try { + CharSequence newText = Html.fromHtml(htmlText); + if (newText != null) { + return newText; + } + } catch (RuntimeException e) { + // If anything bad happens, we'll fall back on the plain text. + } + } + + if (text != null) { + return text; + } + return coerceToHtmlOrStyledText(context, true); + } + + /** + * Turn this item into HTML text, regardless of the type of data it + * actually contains. + * + * <p>The algorithm for deciding what text to return is: + * <ul> + * <li> If {@link #getHtmlText} is non-null, return that. + * <li> If {@link #getText} is non-null, return that, converting to + * valid HTML text. If this text contains style spans, + * {@link Html#toHtml(Spanned) Html.toHtml(Spanned)} is used to + * convert them to HTML formatting. + * <li> If {@link #getUri} is non-null, try to retrieve its data + * as a text stream from its content provider. If the provider can + * supply text/html data, that will be preferred and returned as-is. + * Otherwise, any text/* data will be returned and escaped to HTML. + * If it is not a content: URI or the content provider does not supply + * a text representation, HTML text containing a link to the URI + * will be returned. + * <li> If {@link #getIntent} is non-null, convert that to an intent: + * URI and return as an HTML link. + * <li> Otherwise, return an empty string. + * </ul> + * + * @param context The caller's Context, from which its ContentResolver + * and other things can be retrieved. + * @return Returns the item's representation as HTML text. + */ + public String coerceToHtmlText(Context context) { + // If the item has an explicit HTML value, simply return that. + String htmlText = getHtmlText(); + if (htmlText != null) { + return htmlText; + } + + // If this Item has a plain text value, return it as HTML. + CharSequence text = getText(); + if (text != null) { + if (text instanceof Spanned) { + return Html.toHtml((Spanned)text); + } + return Html.escapeHtml(text); + } + + text = coerceToHtmlOrStyledText(context, false); + return text != null ? text.toString() : null; + } + + private CharSequence coerceToHtmlOrStyledText(Context context, boolean styled) { + // If this Item has a URI value, try using that. + if (mUri != null) { + + // Check to see what data representations the content + // provider supports. We would like HTML text, but if that + // is not possible we'll live with plan text. + String[] types = context.getContentResolver().getStreamTypes(mUri, "text/*"); + boolean hasHtml = false; + boolean hasText = false; + if (types != null) { + for (String type : types) { + if ("text/html".equals(type)) { + hasHtml = true; + } else if (type.startsWith("text/")) { + hasText = true; + } + } + } + + // If the provider can serve data we can use, open and load it. + if (hasHtml || hasText) { + FileInputStream stream = null; + try { + // Ask for a stream of the desired type. + AssetFileDescriptor descr = context.getContentResolver() + .openTypedAssetFileDescriptor(mUri, + hasHtml ? "text/html" : "text/plain", null); + stream = descr.createInputStream(); + InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); + + // Got it... copy the stream into a local string and return it. + StringBuilder builder = new StringBuilder(128); + char[] buffer = new char[8192]; + int len; + while ((len=reader.read(buffer)) > 0) { + builder.append(buffer, 0, len); + } + String text = builder.toString(); + if (hasHtml) { + if (styled) { + // We loaded HTML formatted text and the caller + // want styled text, convert it. + try { + CharSequence newText = Html.fromHtml(text); + return newText != null ? newText : text; + } catch (RuntimeException e) { + return text; + } + } else { + // We loaded HTML formatted text and that is what + // the caller wants, just return it. + return text.toString(); + } + } + if (styled) { + // We loaded plain text and the caller wants styled + // text, that is all we have so return it. + return text; + } else { + // We loaded plain text and the caller wants HTML + // text, escape it for HTML. + return Html.escapeHtml(text); + } + + } catch (FileNotFoundException e) { + // Unable to open content URI as text... not really an + // error, just something to ignore. + + } catch (IOException e) { + // Something bad has happened. + Log.w("ClippedData", "Failure loading text", e); + return Html.escapeHtml(e.toString()); + + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + } + + // If we couldn't open the URI as a stream, then we can build + // some HTML text with the URI itself. + // probably serves fairly well as a textual representation. + if (styled) { + return uriToStyledText(mUri.toString()); + } else { + return uriToHtml(mUri.toString()); + } + } + + // Finally, if all we have is an Intent, then we can just turn that + // into text. Not the most user-friendly thing, but it's something. + if (mIntent != null) { + if (styled) { + return uriToStyledText(mIntent.toUri(Intent.URI_INTENT_SCHEME)); + } else { + return uriToHtml(mIntent.toUri(Intent.URI_INTENT_SCHEME)); + } + } + + // Shouldn't get here, but just in case... + return ""; + } + + private String uriToHtml(String uri) { + StringBuilder builder = new StringBuilder(256); + builder.append("<a href=\""); + builder.append(uri); + builder.append("\">"); + builder.append(Html.escapeHtml(uri)); + builder.append("</a>"); + return builder.toString(); + } + + private CharSequence uriToStyledText(String uri) { + SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(uri); + builder.setSpan(new URLSpan(uri), 0, builder.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return builder; + } + @Override public String toString() { StringBuilder b = new StringBuilder(128); @@ -335,7 +591,10 @@ public class ClipData implements Parcelable { /** @hide */ public void toShortString(StringBuilder b) { - if (mText != null) { + if (mHtmlText != null) { + b.append("H:"); + b.append(mHtmlText); + } else if (mText != null) { b.append("T:"); b.append(mText); } else if (mUri != null) { @@ -409,6 +668,22 @@ public class ClipData implements Parcelable { } /** + * Create a new ClipData holding data of the type + * {@link ClipDescription#MIMETYPE_TEXT_HTML}. + * + * @param label User-visible label for the clip data. + * @param text The text of clip as plain text, for receivers that don't + * handle HTML. This is required. + * @param htmlText The actual HTML text in the clip. + * @return Returns a new ClipData containing the specified data. + */ + static public ClipData newHtmlText(CharSequence label, CharSequence text, + String htmlText) { + Item item = new Item(text, htmlText); + return new ClipData(label, MIMETYPES_TEXT_HTML, item); + } + + /** * Create a new ClipData holding an Intent with MIME type * {@link ClipDescription#MIMETYPE_TEXT_INTENT}. * @@ -574,6 +849,7 @@ public class ClipData implements Parcelable { for (int i=0; i<N; i++) { Item item = mItems.get(i); TextUtils.writeToParcel(item.mText, dest, flags); + dest.writeString(item.mHtmlText); if (item.mIntent != null) { dest.writeInt(1); item.mIntent.writeToParcel(dest, flags); @@ -600,9 +876,10 @@ public class ClipData implements Parcelable { final int N = in.readInt(); for (int i=0; i<N; i++) { CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + String htmlText = in.readString(); Intent intent = in.readInt() != 0 ? Intent.CREATOR.createFromParcel(in) : null; Uri uri = in.readInt() != 0 ? Uri.CREATOR.createFromParcel(in) : null; - mItems.add(new Item(text, intent, uri)); + mItems.add(new Item(text, htmlText, intent, uri)); } } diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java index c6b51ef..5cb6e77 100644 --- a/core/java/android/content/ClipDescription.java +++ b/core/java/android/content/ClipDescription.java @@ -41,6 +41,11 @@ public class ClipDescription implements Parcelable { public static final String MIMETYPE_TEXT_PLAIN = "text/plain"; /** + * The MIME type for a clip holding HTML text. + */ + public static final String MIMETYPE_TEXT_HTML = "text/html"; + + /** * The MIME type for a clip holding one or more URIs. This should be * used for URIs that are meaningful to a user (such as an http: URI). * It should <em>not</em> be used for a content: URI that references some diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 2930998..722fdc6 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -248,7 +248,7 @@ public abstract class ContentResolver { * @param mimeTypeFilter The desired MIME type. This may be a pattern, * such as *\/*, to query for all available MIME types that match the * pattern. - * @return Returns an array of MIME type strings for all availablle + * @return Returns an array of MIME type strings for all available * data streams that match the given mimeTypeFilter. If there are none, * null is returned. */ diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 18d682d..19e4372 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -954,7 +954,18 @@ public class Intent implements Parcelable, Cloneable { * using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it * should be the MIME type of the data in EXTRA_STREAM. Use {@literal *}/* * if the MIME type is unknown (this will only allow senders that can - * handle generic data streams). + * handle generic data streams). If using {@link #EXTRA_TEXT}, you can + * also optionally supply {@link #EXTRA_HTML_TEXT} for clients to retrieve + * your text with HTML formatting. + * <p> + * As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, the data + * being sent can be supplied through {@link #setClipData(ClipData)}. This + * allows you to use {@link #FLAG_GRANT_READ_URI_PERMISSION} when sharing + * content: URIs and other advanced features of {@link ClipData}. If + * using this approach, you still must supply the same data through the + * {@link #EXTRA_TEXT} or {@link #EXTRA_STREAM} fields described below + * for compatibility with old applications. If you don't set a ClipData, + * it will be copied there for you when calling {@link Context#startActivity(Intent)}. * <p> * Optional standard extras, which may be interpreted by some recipients as * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC}, @@ -967,11 +978,13 @@ public class Intent implements Parcelable, Cloneable { /** * Activity Action: Deliver multiple data to someone else. * <p> - * Like ACTION_SEND, except the data is multiple. + * Like {@link #ACTION_SEND}, except the data is multiple. * <p> * Input: {@link #getType} is the MIME type of the data being sent. * get*ArrayListExtra can have either a {@link #EXTRA_TEXT} or {@link - * #EXTRA_STREAM} field, containing the data to be sent. + * #EXTRA_STREAM} field, containing the data to be sent. If using + * {@link #EXTRA_TEXT}, you can also optionally supply {@link #EXTRA_HTML_TEXT} + * for clients to retrieve your text with HTML formatting. * <p> * Multiple types are supported, and receivers should handle mixed types * whenever possible. The right way for the receiver to check them is to @@ -983,6 +996,15 @@ public class Intent implements Parcelable, Cloneable { * be image/jpg, but if you are sending image/jpg and image/png, then the * intent's type should be image/*. * <p> + * As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, the data + * being sent can be supplied through {@link #setClipData(ClipData)}. This + * allows you to use {@link #FLAG_GRANT_READ_URI_PERMISSION} when sharing + * content: URIs and other advanced features of {@link ClipData}. If + * using this approach, you still must supply the same data through the + * {@link #EXTRA_TEXT} or {@link #EXTRA_STREAM} fields described below + * for compatibility with old applications. If you don't set a ClipData, + * it will be copied there for you when calling {@link Context#startActivity(Intent)}. + * <p> * Optional standard extras, which may be interpreted by some recipients as * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC}, * {@link #EXTRA_BCC}, {@link #EXTRA_SUBJECT}. @@ -2501,6 +2523,14 @@ public class Intent implements Parcelable, Cloneable { public static final String EXTRA_TEXT = "android.intent.extra.TEXT"; /** + * A constant String that is associated with the Intent, used with + * {@link #ACTION_SEND} to supply an alternative to {@link #EXTRA_TEXT} + * as HTML formatted text. Note that you <em>must</em> also supply + * {@link #EXTRA_TEXT}. + */ + public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT"; + + /** * A content: URI holding a stream of data associated with the Intent, * used with {@link #ACTION_SEND} to supply the data being sent. */ diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 56fd5f8..9b8454a 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -373,6 +373,6 @@ interface IPackageManager { List<UserInfo> getUsers(); UserInfo getUser(int userId); - void setPermissionEnforcement(String permission, int enforcement); - int getPermissionEnforcement(String permission); + void setPermissionEnforced(String permission, boolean enforced); + boolean isPermissionEnforced(String permission); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 5d890d4..675f77e 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -28,6 +28,7 @@ import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.util.AndroidException; import android.util.DisplayMetrics; @@ -1091,21 +1092,7 @@ public abstract class PackageManager { = "android.content.pm.extra.VERIFICATION_INSTALL_FLAGS"; /** {@hide} */ - public static final int ENFORCEMENT_DEFAULT = 0; - /** {@hide} */ - public static final int ENFORCEMENT_YES = 1; - - /** {@hide} */ - public static String enforcementToString(int enforcement) { - switch (enforcement) { - case ENFORCEMENT_DEFAULT: - return "DEFAULT"; - case ENFORCEMENT_YES: - return "YES"; - default: - return Integer.toString(enforcement); - } - } + public static final boolean DEFAULT_ENFORCE_READ_EXTERNAL_STORAGE = !"user".equals(Build.TYPE); /** * Retrieve overall information about an application package that is diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 2af58be..c682852 100755 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -32,7 +32,6 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; -import android.util.SparseArray; import android.util.TypedValue; import android.util.LongSparseArray; @@ -86,8 +85,8 @@ public class Resources { // single-threaded, and after that these are immutable. private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables = new LongSparseArray<Drawable.ConstantState>(); - private static final SparseArray<ColorStateList> mPreloadedColorStateLists - = new SparseArray<ColorStateList>(); + private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists + = new LongSparseArray<ColorStateList>(); private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<Drawable.ConstantState>(); private static boolean mPreloaded; @@ -98,8 +97,8 @@ public class Resources { // These are protected by the mTmpValue lock. private final LongSparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache = new LongSparseArray<WeakReference<Drawable.ConstantState> >(); - private final SparseArray<WeakReference<ColorStateList> > mColorStateListCache - = new SparseArray<WeakReference<ColorStateList> >(); + private final LongSparseArray<WeakReference<ColorStateList> > mColorStateListCache + = new LongSparseArray<WeakReference<ColorStateList> >(); private final LongSparseArray<WeakReference<Drawable.ConstantState> > mColorDrawableCache = new LongSparseArray<WeakReference<Drawable.ConstantState> >(); private boolean mPreloading; @@ -118,22 +117,6 @@ public class Resources { private CompatibilityInfo mCompatibilityInfo; - private static final LongSparseArray<Object> EMPTY_ARRAY = new LongSparseArray<Object>(0) { - @Override - public void put(long k, Object o) { - throw new UnsupportedOperationException(); - } - @Override - public void append(long k, Object o) { - throw new UnsupportedOperationException(); - } - }; - - @SuppressWarnings("unchecked") - private static <T> LongSparseArray<T> emptySparseArray() { - return (LongSparseArray<T>) EMPTY_ARRAY; - } - /** @hide */ public static int selectDefaultTheme(int curTheme, int targetSdkVersion) { return selectSystemTheme(curTheme, targetSdkVersion, @@ -180,9 +163,8 @@ public class Resources { * @param config Desired device configuration to consider when * selecting/computing resource values (optional). */ - public Resources(AssetManager assets, DisplayMetrics metrics, - Configuration config) { - this(assets, metrics, config, (CompatibilityInfo) null); + public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { + this(assets, metrics, config, null); } /** @@ -1883,7 +1865,8 @@ public class Resources { return dr; } - Drawable.ConstantState cs = isColorDrawable ? sPreloadedColorDrawables.get(key) : sPreloadedDrawables.get(key); + Drawable.ConstantState cs = isColorDrawable ? + sPreloadedColorDrawables.get(key) : sPreloadedDrawables.get(key); if (cs != null) { dr = cs.newDrawable(this); } else { @@ -2005,21 +1988,21 @@ public class Resources { } } - final int key = (value.assetCookie << 24) | value.data; + final long key = (((long) value.assetCookie) << 32) | value.data; ColorStateList csl; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { - csl = mPreloadedColorStateLists.get(key); + csl = sPreloadedColorStateLists.get(key); if (csl != null) { return csl; } csl = ColorStateList.valueOf(value.data); if (mPreloading) { - mPreloadedColorStateLists.put(key, csl); + sPreloadedColorStateLists.put(key, csl); } return csl; @@ -2030,7 +2013,7 @@ public class Resources { return csl; } - csl = mPreloadedColorStateLists.get(key); + csl = sPreloadedColorStateLists.get(key); if (csl != null) { return csl; } @@ -2063,14 +2046,13 @@ public class Resources { if (csl != null) { if (mPreloading) { - mPreloadedColorStateLists.put(key, csl); + sPreloadedColorStateLists.put(key, csl); } else { synchronized (mTmpValue) { //Log.i(TAG, "Saving cached color state list @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + csl); - mColorStateListCache.put( - key, new WeakReference<ColorStateList>(csl)); + mColorStateListCache.put(key, new WeakReference<ColorStateList>(csl)); } } } @@ -2078,7 +2060,7 @@ public class Resources { return csl; } - private ColorStateList getCachedColorStateList(int key) { + private ColorStateList getCachedColorStateList(long key) { synchronized (mTmpValue) { WeakReference<ColorStateList> wr = mColorStateListCache.get(key); if (wr != null) { // we have the key @@ -2088,8 +2070,7 @@ public class Resources { // Integer.toHexString(((Integer)key).intValue()) // + " in " + this + ": " + entry); return entry; - } - else { // our entry has been purged + } else { // our entry has been purged mColorStateListCache.delete(key); } } diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 83b6986..640b47b 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -950,11 +950,10 @@ public class Camera { /** * Callback interface used to notify on auto focus start and stop. * - * <p>This is useful for continuous autofocus -- {@link Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} - * and {@link Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can - * show autofocus animation.</p> - * - * @hide + * <p>This is only supported in continuous autofocus modes -- {@link + * Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} and {@link + * Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can show + * autofocus animation based on this.</p> */ public interface AutoFocusMoveCallback { @@ -962,7 +961,7 @@ public class Camera { * Called when the camera auto focus starts or stops. * * @param start true if focus starts to move, false if focus stops to move - * @param camera the Camera service object + * @param camera the Camera service object */ void onAutoFocusMoving(boolean start, Camera camera); } @@ -971,7 +970,6 @@ public class Camera { * Sets camera auto-focus move callback. * * @param cb the callback to run - * @hide */ public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) { mAutoFocusMoveCallback = cb; diff --git a/core/java/android/hardware/input/IInputDevicesChangedListener.aidl b/core/java/android/hardware/input/IInputDevicesChangedListener.aidl new file mode 100644 index 0000000..5d8ada1 --- /dev/null +++ b/core/java/android/hardware/input/IInputDevicesChangedListener.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** @hide */ +interface IInputDevicesChangedListener { + /* Called when input devices changed, such as a device being added, + * removed or changing configuration. + * + * The parameter is an array of pairs (deviceId, generation) indicating the current + * device id and generation of all input devices. The client can determine what + * has happened by comparing the result to its prior observations. + */ + oneway void onInputDevicesChanged(in int[] deviceIdAndGeneration); +} diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 47e0d1e..3137947 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -17,6 +17,8 @@ package android.hardware.input; import android.hardware.input.KeyboardLayout; +import android.hardware.input.IInputDevicesChangedListener; +import android.os.IBinder; import android.view.InputDevice; import android.view.InputEvent; @@ -42,4 +44,11 @@ interface IInputManager { String getKeyboardLayoutForInputDevice(String inputDeviceDescriptor); void setKeyboardLayoutForInputDevice(String inputDeviceDescriptor, String keyboardLayoutDescriptor); + + // Registers an input devices changed listener. + void registerInputDevicesChangedListener(IInputDevicesChangedListener listener); + + // Input device vibrator control. + void vibrate(int deviceId, in long[] pattern, int repeat, IBinder token); + void cancelVibrate(int deviceId, IBinder token); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 3b3c237..b39b823 100755 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -19,9 +19,14 @@ package android.hardware.input; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; +import android.os.Binder; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.Vibrator; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.util.Log; @@ -29,6 +34,8 @@ import android.util.SparseArray; import android.view.InputDevice; import android.view.InputEvent; +import java.util.ArrayList; + /** * Provides information about input devices and available key layouts. * <p> @@ -40,11 +47,22 @@ import android.view.InputEvent; */ public final class InputManager { private static final String TAG = "InputManager"; + private static final boolean DEBUG = false; + + private static final int MSG_DEVICE_ADDED = 1; + private static final int MSG_DEVICE_REMOVED = 2; + private static final int MSG_DEVICE_CHANGED = 3; private static InputManager sInstance; private final IInputManager mIm; - private final SparseArray<InputDevice> mInputDevices = new SparseArray<InputDevice>(); + + // Guarded by mInputDevicesLock + private final Object mInputDevicesLock = new Object(); + private SparseArray<InputDevice> mInputDevices; + private InputDevicesChangedListener mInputDevicesChangedListener; + private final ArrayList<InputDeviceListenerDelegate> mInputDeviceListeners = + new ArrayList<InputDeviceListenerDelegate>(); /** * Broadcast Action: Query available keyboard layouts. @@ -169,6 +187,103 @@ public final class InputManager { } /** + * Gets information about the input device with the specified id. + * @param id The device id. + * @return The input device or null if not found. + */ + public InputDevice getInputDevice(int id) { + synchronized (mInputDevicesLock) { + populateInputDevicesLocked(); + + int index = mInputDevices.indexOfKey(id); + if (index < 0) { + return null; + } + + InputDevice inputDevice = mInputDevices.valueAt(index); + if (inputDevice == null) { + try { + inputDevice = mIm.getInputDevice(id); + } catch (RemoteException ex) { + throw new RuntimeException("Could not get input device information.", ex); + } + } + mInputDevices.setValueAt(index, inputDevice); + return inputDevice; + } + } + + /** + * Gets the ids of all input devices in the system. + * @return The input device ids. + */ + public int[] getInputDeviceIds() { + synchronized (mInputDevicesLock) { + populateInputDevicesLocked(); + + final int count = mInputDevices.size(); + final int[] ids = new int[count]; + for (int i = 0; i < count; i++) { + ids[i] = mInputDevices.keyAt(i); + } + return ids; + } + } + + /** + * Registers an input device listener to receive notifications about when + * input devices are added, removed or changed. + * + * @param listener The listener to register. + * @param handler The handler on which the listener should be invoked, or null + * if the listener should be invoked on the calling thread's looper. + * + * @see #unregisterInputDeviceListener + */ + public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + synchronized (mInputDevicesLock) { + int index = findInputDeviceListenerLocked(listener); + if (index < 0) { + mInputDeviceListeners.add(new InputDeviceListenerDelegate(listener, handler)); + } + } + } + + /** + * Unregisters an input device listener. + * + * @param listener The listener to unregister. + * + * @see #registerInputDeviceListener + */ + public void unregisterInputDeviceListener(InputDeviceListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + synchronized (mInputDevicesLock) { + int index = findInputDeviceListenerLocked(listener); + if (index >= 0) { + mInputDeviceListeners.remove(index); + } + } + } + + private int findInputDeviceListenerLocked(InputDeviceListener listener) { + final int numListeners = mInputDeviceListeners.size(); + for (int i = 0; i < numListeners; i++) { + if (mInputDeviceListeners.get(i).mListener == listener) { + return i; + } + } + return -1; + } + + /** * Gets information about all supported keyboard layouts. * <p> * The input manager consults the built-in keyboard layouts as well @@ -327,50 +442,6 @@ public final class InputManager { } /** - * Gets information about the input device with the specified id. - * @param id The device id. - * @return The input device or null if not found. - * - * @hide - */ - public InputDevice getInputDevice(int id) { - synchronized (mInputDevices) { - InputDevice inputDevice = mInputDevices.get(id); - if (inputDevice != null) { - return inputDevice; - } - } - final InputDevice newInputDevice; - try { - newInputDevice = mIm.getInputDevice(id); - } catch (RemoteException ex) { - throw new RuntimeException("Could not get input device information.", ex); - } - synchronized (mInputDevices) { - InputDevice inputDevice = mInputDevices.get(id); - if (inputDevice != null) { - return inputDevice; - } - mInputDevices.put(id, newInputDevice); - return newInputDevice; - } - } - - /** - * Gets the ids of all input devices in the system. - * @return The input device ids. - * - * @hide - */ - public int[] getInputDeviceIds() { - try { - return mIm.getInputDeviceIds(); - } catch (RemoteException ex) { - throw new RuntimeException("Could not get input device ids.", ex); - } - } - - /** * Queries the framework about whether any physical keys exist on the * any keyboard attached to the device that are capable of producing the given * array of key codes. @@ -429,4 +500,201 @@ public final class InputManager { return false; } } + + private void populateInputDevicesLocked() { + if (mInputDevicesChangedListener == null) { + final InputDevicesChangedListener listener = new InputDevicesChangedListener(); + try { + mIm.registerInputDevicesChangedListener(listener); + } catch (RemoteException ex) { + throw new RuntimeException( + "Could not get register input device changed listener", ex); + } + mInputDevicesChangedListener = listener; + } + + if (mInputDevices == null) { + final int[] ids; + try { + ids = mIm.getInputDeviceIds(); + } catch (RemoteException ex) { + throw new RuntimeException("Could not get input device ids.", ex); + } + + mInputDevices = new SparseArray<InputDevice>(); + for (int i = 0; i < ids.length; i++) { + mInputDevices.put(ids[i], null); + } + } + } + + private void onInputDevicesChanged(int[] deviceIdAndGeneration) { + if (DEBUG) { + Log.d(TAG, "Received input devices changed."); + } + + synchronized (mInputDevicesLock) { + for (int i = mInputDevices.size(); --i > 0; ) { + final int deviceId = mInputDevices.keyAt(i); + if (!containsDeviceId(deviceIdAndGeneration, deviceId)) { + if (DEBUG) { + Log.d(TAG, "Device removed: " + deviceId); + } + mInputDevices.removeAt(i); + sendMessageToInputDeviceListenersLocked(MSG_DEVICE_REMOVED, deviceId); + } + } + + for (int i = 0; i < deviceIdAndGeneration.length; i += 2) { + final int deviceId = deviceIdAndGeneration[i]; + int index = mInputDevices.indexOfKey(deviceId); + if (index >= 0) { + final InputDevice device = mInputDevices.valueAt(index); + if (device != null) { + final int generation = deviceIdAndGeneration[i + 1]; + if (device.getGeneration() != generation) { + if (DEBUG) { + Log.d(TAG, "Device changed: " + deviceId); + } + mInputDevices.setValueAt(index, null); + sendMessageToInputDeviceListenersLocked(MSG_DEVICE_CHANGED, deviceId); + } + } + } else { + if (DEBUG) { + Log.d(TAG, "Device added: " + deviceId); + } + mInputDevices.put(deviceId, null); + sendMessageToInputDeviceListenersLocked(MSG_DEVICE_ADDED, deviceId); + } + } + } + } + + private void sendMessageToInputDeviceListenersLocked(int what, int deviceId) { + final int numListeners = mInputDeviceListeners.size(); + for (int i = 0; i < numListeners; i++) { + InputDeviceListenerDelegate listener = mInputDeviceListeners.get(i); + listener.sendMessage(listener.obtainMessage(what, deviceId, 0)); + } + } + + private static boolean containsDeviceId(int[] deviceIdAndGeneration, int deviceId) { + for (int i = 0; i < deviceIdAndGeneration.length; i += 2) { + if (deviceIdAndGeneration[i] == deviceId) { + return true; + } + } + return false; + } + + /** + * Gets a vibrator service associated with an input device, assuming it has one. + * @return The vibrator, never null. + * @hide + */ + public Vibrator getInputDeviceVibrator(int deviceId) { + return new InputDeviceVibrator(deviceId); + } + + /** + * Listens for changes in input devices. + */ + public interface InputDeviceListener { + /** + * Called whenever an input device has been added to the system. + * Use {@link InputManager#getInputDevice} to get more information about the device. + * + * @param deviceId The id of the input device that was added. + */ + void onInputDeviceAdded(int deviceId); + + /** + * Called whenever an input device has been removed from the system. + * + * @param deviceId The id of the input device that was removed. + */ + void onInputDeviceRemoved(int deviceId); + + /** + * Called whenever the properties of an input device have changed since they + * were last queried. Use {@link InputManager#getInputDevice} to get + * a fresh {@link InputDevice} object with the new properties. + * + * @param deviceId The id of the input device that changed. + */ + void onInputDeviceChanged(int deviceId); + } + + private final class InputDevicesChangedListener extends IInputDevicesChangedListener.Stub { + @Override + public void onInputDevicesChanged(int[] deviceIdAndGeneration) throws RemoteException { + InputManager.this.onInputDevicesChanged(deviceIdAndGeneration); + } + } + + private static final class InputDeviceListenerDelegate extends Handler { + public final InputDeviceListener mListener; + + public InputDeviceListenerDelegate(InputDeviceListener listener, Handler handler) { + super(handler != null ? handler.getLooper() : Looper.myLooper()); + mListener = listener; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DEVICE_ADDED: + mListener.onInputDeviceAdded(msg.arg1); + break; + case MSG_DEVICE_REMOVED: + mListener.onInputDeviceRemoved(msg.arg1); + break; + case MSG_DEVICE_CHANGED: + mListener.onInputDeviceChanged(msg.arg1); + break; + } + } + } + + private final class InputDeviceVibrator extends Vibrator { + private final int mDeviceId; + private final Binder mToken; + + public InputDeviceVibrator(int deviceId) { + mDeviceId = deviceId; + mToken = new Binder(); + } + + @Override + public boolean hasVibrator() { + return true; + } + + @Override + public void vibrate(long milliseconds) { + vibrate(new long[] { 0, milliseconds}, -1); + } + + @Override + public void vibrate(long[] pattern, int repeat) { + if (repeat >= pattern.length) { + throw new ArrayIndexOutOfBoundsException(); + } + try { + mIm.vibrate(mDeviceId, pattern, repeat, mToken); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to vibrate.", ex); + } + } + + @Override + public void cancel() { + try { + mIm.cancelVibrate(mDeviceId, mToken); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to cancel vibration.", ex); + } + } + } } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index de16985..ef4209f 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -373,10 +373,11 @@ public class ConnectivityManager { } /** - * Gets you info about the current data network. - * Call {@link NetworkInfo#isConnected()} on the returned {@link NetworkInfo} - * to check if the device has a data connection. - */ + * Returns details about the currently active data network. When connected, + * this network is the default route for outgoing connections. You should + * always check {@link NetworkInfo#isConnected()} before initiating network + * traffic. This may return {@code null} when no networks are available. + */ public NetworkInfo getActiveNetworkInfo() { try { return mService.getActiveNetworkInfo(); @@ -856,4 +857,19 @@ public class ConnectivityManager { } catch (RemoteException e) {} return false; } + + /** + * Returns if the currently active data network is metered. A network is + * classified as metered when the user is sensitive to heavy data usage on + * that connection. You should check this before doing large data transfers, + * and warn the user or delay the operation until another network is + * available. + */ + public boolean isActiveNetworkMetered() { + try { + return mService.isActiveNetworkMetered(); + } catch (RemoteException e) { + return false; + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 7046008..92aeff2 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -51,6 +51,7 @@ interface IConnectivityManager NetworkState[] getAllNetworkState(); NetworkQuotaInfo getActiveNetworkQuotaInfo(); + boolean isActiveNetworkMetered(); boolean setRadios(boolean onOff); diff --git a/core/java/android/net/INetworkPolicyManager.aidl b/core/java/android/net/INetworkPolicyManager.aidl index 89c9c36..3250ae7 100644 --- a/core/java/android/net/INetworkPolicyManager.aidl +++ b/core/java/android/net/INetworkPolicyManager.aidl @@ -32,6 +32,7 @@ interface INetworkPolicyManager { /** Control UID policies. */ void setAppPolicy(int appId, int policy); int getAppPolicy(int appId); + int[] getAppsWithPolicy(int policy); boolean isUidForeground(int uid); @@ -50,5 +51,6 @@ interface INetworkPolicyManager { boolean getRestrictBackground(); NetworkQuotaInfo getNetworkQuotaInfo(in NetworkState state); + boolean isNetworkMetered(in NetworkState state); } diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java index 2b36131..07bfd4b 100644 --- a/core/java/android/net/NetworkPolicyManager.java +++ b/core/java/android/net/NetworkPolicyManager.java @@ -92,6 +92,14 @@ public class NetworkPolicyManager { } } + public int[] getAppsWithPolicy(int policy) { + try { + return mService.getAppsWithPolicy(policy); + } catch (RemoteException e) { + return new int[0]; + } + } + public void registerListener(INetworkPolicyListener listener) { try { mService.registerListener(listener); diff --git a/core/java/android/net/nsd/DnsSdServiceInfo.java b/core/java/android/net/nsd/DnsSdServiceInfo.java index 47d6ec6..33c3eb9 100644 --- a/core/java/android/net/nsd/DnsSdServiceInfo.java +++ b/core/java/android/net/nsd/DnsSdServiceInfo.java @@ -19,6 +19,8 @@ package android.net.nsd; import android.os.Parcelable; import android.os.Parcel; +import java.net.InetAddress; + /** * Defines a service based on DNS service discovery * {@hide} @@ -27,20 +29,20 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { private String mServiceName; - private String mRegistrationType; + private String mServiceType; private DnsSdTxtRecord mTxtRecord; - private String mHostname; + private InetAddress mHost; private int mPort; - DnsSdServiceInfo() { + public DnsSdServiceInfo() { } - DnsSdServiceInfo(String sn, String rt, DnsSdTxtRecord tr) { + public DnsSdServiceInfo(String sn, String rt, DnsSdTxtRecord tr) { mServiceName = sn; - mRegistrationType = rt; + mServiceType = rt; mTxtRecord = tr; } @@ -59,13 +61,13 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { @Override /** @hide */ public String getServiceType() { - return mRegistrationType; + return mServiceType; } @Override /** @hide */ public void setServiceType(String s) { - mRegistrationType = s; + mServiceType = s; } public DnsSdTxtRecord getTxtRecord() { @@ -76,12 +78,12 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { mTxtRecord = new DnsSdTxtRecord(t); } - public String getHostName() { - return mHostname; + public InetAddress getHost() { + return mHost; } - public void setHostName(String s) { - mHostname = s; + public void setHost(InetAddress s) { + mHost = s; } public int getPort() { @@ -96,7 +98,9 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { StringBuffer sb = new StringBuffer(); sb.append("name: ").append(mServiceName). - append("type: ").append(mRegistrationType). + append("type: ").append(mServiceType). + append("host: ").append(mHost). + append("port: ").append(mPort). append("txtRecord: ").append(mTxtRecord); return sb.toString(); } @@ -109,9 +113,14 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { /** Implement the Parcelable interface */ public void writeToParcel(Parcel dest, int flags) { dest.writeString(mServiceName); - dest.writeString(mRegistrationType); + dest.writeString(mServiceType); dest.writeParcelable(mTxtRecord, flags); - dest.writeString(mHostname); + if (mHost != null) { + dest.writeByte((byte)1); + dest.writeByteArray(mHost.getAddress()); + } else { + dest.writeByte((byte)0); + } dest.writeInt(mPort); } @@ -121,9 +130,15 @@ public class DnsSdServiceInfo implements NetworkServiceInfo, Parcelable { public DnsSdServiceInfo createFromParcel(Parcel in) { DnsSdServiceInfo info = new DnsSdServiceInfo(); info.mServiceName = in.readString(); - info.mRegistrationType = in.readString(); + info.mServiceType = in.readString(); info.mTxtRecord = in.readParcelable(null); - info.mHostname = in.readString(); + + if (in.readByte() == 1) { + try { + info.mHost = InetAddress.getByAddress(in.createByteArray()); + } catch (java.net.UnknownHostException e) {} + } + info.mPort = in.readInt(); return info; } diff --git a/core/java/android/net/nsd/DnsSdTxtRecord.java b/core/java/android/net/nsd/DnsSdTxtRecord.java index 6d4342c..952e02f 100644 --- a/core/java/android/net/nsd/DnsSdTxtRecord.java +++ b/core/java/android/net/nsd/DnsSdTxtRecord.java @@ -24,6 +24,8 @@ package android.net.nsd; import android.os.Parcelable; import android.os.Parcel; +import java.util.Arrays; + /** * This class handles TXT record data for DNS based service discovery as specified at * http://tools.ietf.org/html/draft-cheshire-dnsext-dns-sd-11 @@ -160,7 +162,7 @@ public class DnsSdTxtRecord implements Parcelable { /* Gets the raw data in bytes */ public byte[] getRawData() { - return mData; + return (byte[]) mData.clone(); } private void insert(byte[] keyBytes, byte[] value, int index) { @@ -279,6 +281,24 @@ public class DnsSdTxtRecord implements Parcelable { return result != null ? result : ""; } + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof DnsSdTxtRecord)) { + return false; + } + + DnsSdTxtRecord record = (DnsSdTxtRecord)o; + return Arrays.equals(record.mData, mData); + } + + @Override + public int hashCode() { + return Arrays.hashCode(mData); + } + /** Implement the Parcelable interface */ public int describeContents() { return 0; diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index a109a98..505f11b 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -93,6 +93,15 @@ public class NsdManager { /** @hide */ public static final int RESOLVE_SERVICE_SUCCEEDED = BASE + 17; + /** @hide */ + public static final int STOP_RESOLVE = BASE + 18; + /** @hide */ + public static final int STOP_RESOLVE_FAILED = BASE + 19; + /** @hide */ + public static final int STOP_RESOLVE_SUCCEEDED = BASE + 20; + + + /** * Create a new Nsd instance. Applications use * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve @@ -117,10 +126,23 @@ public class NsdManager { /** * Indicates that the operation failed because the framework is busy and - * unable to service the request + * unable to service the request. */ public static final int BUSY = 2; + /** + * Indicates that the operation failed because it is already active. + */ + public static final int ALREADY_ACTIVE = 3; + + /** + * Indicates that the operation failed because maximum limit on + * service registrations has reached. + */ + public static final int MAX_REGS_REACHED = 4; + + + /** Interface for callback invocation when framework channel is connected or lost */ public interface ChannelListener { public void onChannelConnected(Channel c); @@ -188,6 +210,7 @@ public class NsdManager { private DnsSdRegisterListener mDnsSdRegisterListener; private DnsSdUpdateRegistrationListener mDnsSdUpdateListener; private DnsSdResolveListener mDnsSdResolveListener; + private ActionListener mDnsSdStopResolveListener; AsyncChannel mAsyncChannel; ServiceHandler mHandler; @@ -278,6 +301,16 @@ public class NsdManager { (DnsSdServiceInfo) message.obj); } break; + case STOP_RESOLVE_FAILED: + if (mDnsSdStopResolveListener!= null) { + mDnsSdStopResolveListener.onFailure(message.arg1); + } + break; + case STOP_RESOLVE_SUCCEEDED: + if (mDnsSdStopResolveListener != null) { + mDnsSdStopResolveListener.onSuccess(); + } + break; default: Log.d(TAG, "Ignored " + message); break; @@ -345,6 +378,14 @@ public class NsdManager { c.mDnsSdResolveListener = b; } + /** + * Set the listener for stopping service resolution. Can be null. + */ + public void setStopResolveListener(Channel c, ActionListener b) { + if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); + c.mDnsSdStopResolveListener = b; + } + public void registerService(Channel c, DnsSdServiceInfo serviceInfo) { if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); if (serviceInfo == null) throw new IllegalArgumentException("Null serviceInfo"); @@ -378,6 +419,13 @@ public class NsdManager { c.mAsyncChannel.sendMessage(RESOLVE_SERVICE, serviceInfo); } + public void stopServiceResolve(Channel c) { + if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); + if (c.mDnsSdResolveListener == null) throw new + IllegalStateException("Resolve listener needs to be set first"); + c.mAsyncChannel.sendMessage(STOP_RESOLVE); + } + /** * Get a reference to NetworkService handler. This is used to establish * an AsyncChannel communication with the service diff --git a/core/java/android/nfc/INdefPushCallback.aidl b/core/java/android/nfc/INdefPushCallback.aidl index 4e79822..1c6d5d0 100644 --- a/core/java/android/nfc/INdefPushCallback.aidl +++ b/core/java/android/nfc/INdefPushCallback.aidl @@ -25,7 +25,6 @@ import android.net.Uri; interface INdefPushCallback { NdefMessage createMessage(); - Uri getUri(); - String getMimeType(); + Uri[] getUris(); void onNdefPushComplete(); } diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index f80dae4..7ffa575 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -108,8 +108,8 @@ public final class NfcActivityManager extends INdefPushCallback.Stub NdefMessage ndefMessage = null; // static NDEF message NfcAdapter.CreateNdefMessageCallback ndefMessageCallback = null; NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback = null; - Uri uri = null; - String mimeType = null; + NfcAdapter.CreateBeamUrisCallback uriCallback = null; + Uri[] uris = null; public NfcActivityState(Activity activity) { if (activity.getWindow().isDestroyed()) { throw new IllegalStateException("activity is already destroyed"); @@ -128,14 +128,19 @@ public final class NfcActivityManager extends INdefPushCallback.Stub ndefMessage = null; ndefMessageCallback = null; onNdefPushCompleteCallback = null; - uri = null; - mimeType = null; + uriCallback = null; + uris = null; } @Override public String toString() { StringBuilder s = new StringBuilder("[").append(" "); s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" "); - s.append(onNdefPushCompleteCallback).append(" ").append(uri).append("]"); + s.append(uriCallback).append(" "); + if (uris != null) { + for (Uri uri : uris) { + s.append(onNdefPushCompleteCallback).append(" ").append(uri).append("]"); + } + } return s.toString(); } } @@ -184,12 +189,25 @@ public final class NfcActivityManager extends INdefPushCallback.Stub mDefaultEvent = new NfcEvent(mAdapter); } - public void setNdefPushContentUri(Activity activity, String mimeType, Uri uri) { + public void setNdefPushContentUri(Activity activity, Uri[] uris) { + boolean isResumed; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + state.uris = uris; + isResumed = state.resumed; + } + if (isResumed) { + requestNfcServiceCallback(true); + } + } + + + public void setNdefPushContentUriCallback(Activity activity, + NfcAdapter.CreateBeamUrisCallback callback) { boolean isResumed; synchronized (NfcActivityManager.this) { NfcActivityState state = getActivityState(activity); - state.uri = uri; - state.mimeType = mimeType; + state.uriCallback = callback; isResumed = state.resumed; } if (isResumed) { @@ -271,24 +289,22 @@ public final class NfcActivityManager extends INdefPushCallback.Stub /** Callback from NFC service, usually on binder thread */ @Override - public Uri getUri() { + public Uri[] getUris() { + Uri[] uris; + NfcAdapter.CreateBeamUrisCallback callback; synchronized (NfcActivityManager.this) { NfcActivityState state = findResumedActivityState(); if (state == null) return null; - - return state.uri; + uris = state.uris; + callback = state.uriCallback; } - } - /** Callback from NFC service, usually on binder thread */ - @Override - public String getMimeType() { - synchronized (NfcActivityManager.this) { - NfcActivityState state = findResumedActivityState(); - if (state == null) return null; - - return state.mimeType; + if (callback != null) { + return callback.createBeamUris(mDefaultEvent); + } else { + return uris; } } + /** Callback from NFC service, usually on binder thread */ @Override public void onNdefPushComplete() { @@ -358,4 +374,5 @@ public final class NfcActivityManager extends INdefPushCallback.Stub } } } + } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 917751c..90f5bef 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -203,6 +203,27 @@ public final class NfcAdapter { /** @hide */ public static final int STATE_TURNING_OFF = 4; + /** @hide */ + public static final String ACTION_HANDOVER_TRANSFER_STARTED = + "android.nfc.action.HANDOVER_TRANSFER_STARTED"; + + /** @hide */ + public static final String ACTION_HANDOVER_TRANSFER_DONE = + "android.nfc.action.HANDOVER_TRANSFER_DONE"; + + /** @hide */ + public static final String EXTRA_HANDOVER_TRANSFER_STATUS = + "android.nfc.extra.HANDOVER_TRANSFER_STATUS"; + + /** @hide */ + public static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; + /** @hide */ + public static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; + + /** @hide */ + public static final String EXTRA_HANDOVER_TRANSFER_URI = + "android.nfc.extra.HANDOVER_TRANSFER_URI"; + // Guarded by NfcAdapter.class static boolean sIsInitialized = false; @@ -281,6 +302,12 @@ public final class NfcAdapter { public NdefMessage createNdefMessage(NfcEvent event); } + + // TODO javadoc + public interface CreateBeamUrisCallback { + public Uri[] createBeamUris(NfcEvent event); + } + /** * Helper to check if this device has FEATURE_NFC, but without using * a context. @@ -556,16 +583,22 @@ public final class NfcAdapter { } } - //TODO: Consider a callback alternative - //TOOD: See if we get rid of mimeType //TODO: make sure NFC service has permission for URI + //TODO: see if we will eventually support multiple URIs //TODO: javadoc - /** @hide */ - public void setBeamPushUri(String mimeType, Uri uri, Activity activity) { + public void setBeamPushUris(Uri[] uris, Activity activity) { + if (activity == null) { + throw new NullPointerException("activity cannot be null"); + } + mNfcActivityManager.setNdefPushContentUri(activity, uris); + } + + // TODO javadoc + public void setBeamPushUrisCallback(CreateBeamUrisCallback callback, Activity activity) { if (activity == null) { throw new NullPointerException("activity cannot be null"); } - mNfcActivityManager.setNdefPushContentUri(activity, mimeType, uri); + mNfcActivityManager.setNdefPushContentUriCallback(activity, callback); } /** diff --git a/core/java/android/os/NullVibrator.java b/core/java/android/os/NullVibrator.java new file mode 100644 index 0000000..8de4e06 --- /dev/null +++ b/core/java/android/os/NullVibrator.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.util.Log; + +/** + * Vibrator implementation that does nothing. + * + * @hide + */ +public class NullVibrator extends Vibrator { + private static final NullVibrator sInstance = new NullVibrator(); + + private NullVibrator() { + } + + public static NullVibrator getInstance() { + return sInstance; + } + + @Override + public boolean hasVibrator() { + return false; + } + + @Override + public void vibrate(long milliseconds) { + } + + @Override + public void vibrate(long[] pattern, int repeat) { + if (repeat >= pattern.length) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + @Override + public void cancel() { + } +} diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java new file mode 100644 index 0000000..7c5a47e --- /dev/null +++ b/core/java/android/os/SystemVibrator.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.util.Log; + +/** + * Vibrator implementation that controls the main system vibrator. + * + * @hide + */ +public class SystemVibrator extends Vibrator { + private static final String TAG = "Vibrator"; + + private final IVibratorService mService; + private final Binder mToken = new Binder(); + + public SystemVibrator() { + mService = IVibratorService.Stub.asInterface( + ServiceManager.getService("vibrator")); + } + + @Override + public boolean hasVibrator() { + if (mService == null) { + Log.w(TAG, "Failed to vibrate; no vibrator service."); + return false; + } + try { + return mService.hasVibrator(); + } catch (RemoteException e) { + } + return false; + } + + @Override + public void vibrate(long milliseconds) { + if (mService == null) { + Log.w(TAG, "Failed to vibrate; no vibrator service."); + return; + } + try { + mService.vibrate(milliseconds, mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate.", e); + } + } + + @Override + public void vibrate(long[] pattern, int repeat) { + if (mService == null) { + Log.w(TAG, "Failed to vibrate; no vibrator service."); + return; + } + // catch this here because the server will do nothing. pattern may + // not be null, let that be checked, because the server will drop it + // anyway + if (repeat < pattern.length) { + try { + mService.vibratePattern(pattern, repeat, mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate.", e); + } + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + + @Override + public void cancel() { + if (mService == null) { + return; + } + try { + mService.cancelVibrate(mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to cancel vibration.", e); + } + } +} diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 3769cfe..3f783c9 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -16,61 +16,37 @@ package android.os; -import android.util.Log; +import android.content.Context; /** * Class that operates the vibrator on the device. * <p> * If your process exits, any vibration you started with will stop. * </p> + * + * To obtain an instance of the system vibrator, call + * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as argument. */ -public class Vibrator -{ - private static final String TAG = "Vibrator"; - - IVibratorService mService; - private final Binder mToken = new Binder(); - - /** @hide */ - public Vibrator() - { - mService = IVibratorService.Stub.asInterface( - ServiceManager.getService("vibrator")); +public abstract class Vibrator { + /** + * @hide to prevent subclassing from outside of the framework + */ + public Vibrator() { } /** - * Check whether the hardware has a vibrator. Returns true if a vibrator - * exists, else false. + * Check whether the hardware has a vibrator. + * + * @return True if the hardware has a vibrator, else false. */ - public boolean hasVibrator() { - if (mService == null) { - Log.w(TAG, "Failed to vibrate; no vibrator service."); - return false; - } - try { - return mService.hasVibrator(); - } catch (RemoteException e) { - } - return false; - } + public abstract boolean hasVibrator(); /** - * Turn the vibrator on. + * Vibrate constantly for the specified period of time. * * @param milliseconds The number of milliseconds to vibrate. */ - public void vibrate(long milliseconds) - { - if (mService == null) { - Log.w(TAG, "Failed to vibrate; no vibrator service."); - return; - } - try { - mService.vibrate(milliseconds, mToken); - } catch (RemoteException e) { - Log.w(TAG, "Failed to vibrate.", e); - } - } + public abstract void vibrate(long milliseconds); /** * Vibrate with a given pattern. @@ -90,38 +66,10 @@ public class Vibrator * @param repeat the index into pattern at which to repeat, or -1 if * you don't want to repeat. */ - public void vibrate(long[] pattern, int repeat) - { - if (mService == null) { - Log.w(TAG, "Failed to vibrate; no vibrator service."); - return; - } - // catch this here because the server will do nothing. pattern may - // not be null, let that be checked, because the server will drop it - // anyway - if (repeat < pattern.length) { - try { - mService.vibratePattern(pattern, repeat, mToken); - } catch (RemoteException e) { - Log.w(TAG, "Failed to vibrate.", e); - } - } else { - throw new ArrayIndexOutOfBoundsException(); - } - } + public abstract void vibrate(long[] pattern, int repeat); /** * Turn the vibrator off. */ - public void cancel() - { - if (mService == null) { - return; - } - try { - mService.cancelVibrate(mToken); - } catch (RemoteException e) { - Log.w(TAG, "Failed to cancel vibration.", e); - } - } + public abstract void cancel(); } diff --git a/core/java/android/provider/BrowserContract.java b/core/java/android/provider/BrowserContract.java index d678205..118b5eb 100644 --- a/core/java/android/provider/BrowserContract.java +++ b/core/java/android/provider/BrowserContract.java @@ -30,6 +30,15 @@ import android.os.RemoteException; import android.util.Pair; /** + * <p> + * The contract between the browser provider and applications. Contains the definition + * for the supported URIS and columns. + * </p> + * <h3>Overview</h3> + * <p> + * BrowserContract defines an database of browser-related information which are bookmarks, + * history, images and the mapping between the image and URL. + * </p> * @hide */ public class BrowserContract { @@ -45,12 +54,14 @@ public class BrowserContract { * the dirty flag is not automatically set and the "syncToNetwork" parameter * is set to false when calling * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}. + * @hide */ public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; /** * A parameter for use when querying any table that allows specifying a limit on the number * of rows returned. + * @hide */ public static final String PARAM_LIMIT = "limit"; @@ -58,6 +69,8 @@ public class BrowserContract { * Generic columns for use by sync adapters. The specific functions of * these columns are private to the sync adapter. Other clients of the API * should not attempt to either read or write these columns. + * + * @hide */ interface BaseSyncColumns { /** Generic column for use by sync adapters. */ @@ -74,6 +87,7 @@ public class BrowserContract { /** * Convenience definitions for use in implementing chrome bookmarks sync in the Bookmarks table. + * @hide */ public static final class ChromeSyncColumns { private ChromeSyncColumns() {} @@ -93,6 +107,7 @@ public class BrowserContract { /** * Columns that appear when each row of a table belongs to a specific * account, including sync information that an account may need. + * @hide */ interface SyncColumns extends BaseSyncColumns { /** @@ -144,13 +159,14 @@ public class BrowserContract { public static final String _ID = "_id"; /** - * The URL of the bookmark. + * This column is valid when the row is a URL. The history table's URL + * can not be updated. * <P>Type: TEXT (URL)</P> */ public static final String URL = "url"; /** - * The user visible title of the bookmark. + * The user visible title. * <P>Type: TEXT</P> */ public static final String TITLE = "title"; @@ -159,10 +175,14 @@ public class BrowserContract { * The time that this row was created on its originating client (msecs * since the epoch). * <P>Type: INTEGER</P> + * @hide */ public static final String DATE_CREATED = "created"; } + /** + * @hide + */ interface ImageColumns { /** * The favicon of the bookmark, may be NULL. @@ -182,7 +202,6 @@ public class BrowserContract { * The touch icon for the web page, may be NULL. * Must decode via {@link BitmapFactory#decodeByteArray}. * <p>Type: BLOB (image)</p> - * @hide */ public static final String TOUCH_ICON = "touch_icon"; } @@ -200,9 +219,26 @@ public class BrowserContract { */ public static final String VISITS = "visits"; + /** + * @hide + */ public static final String USER_ENTERED = "user_entered"; } + interface ImageMappingColumns { + /** + * The ID of the image in Images. One image can map onto the multiple URLs. + * <P>Type: INTEGER (long)</P> + */ + public static final String IMAGE_ID = "image_id"; + + /** + * The URL. The URL can map onto the different type of images. + * <P>Type: TEXT (URL)</P> + */ + public static final String URL = "url"; + } + /** * The bookmarks table, which holds the user's browser bookmarks. */ @@ -218,24 +254,71 @@ public class BrowserContract { public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks"); /** + * Used in {@link Bookmarks#TYPE} column and indicats the row is a bookmark. + */ + public static final int BOOKMARK_TYPE_BOOKMARK = 1; + + /** + * Used in {@link Bookmarks#TYPE} column and indicats the row is a folder. + */ + public static final int BOOKMARK_TYPE_FOLDER = 2; + + /** + * Used in {@link Bookmarks#TYPE} column and indicats the row is the bookmark bar folder. + */ + public static final int BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER = 3; + + /** + * Used in {@link Bookmarks#TYPE} column and indicats the row is other folder and + */ + public static final int BOOKMARK_TYPE_OTHER_FOLDER = 4; + + /** + * Used in {@link Bookmarks#TYPE} column and indicats the row is other folder, . + */ + public static final int BOOKMARK_TYPE_MOBILE_FOLDER = 5; + + /** + * The type of the item. + * <P>Type: INTEGER</P> + * <p>Allowed values are:</p> + * <p> + * <ul> + * <li>{@link #BOOKMARK_TYPE_BOOKMARK}</li> + * <li>{@link #BOOKMARK_TYPE_FOLDER}</li> + * <li>{@link #BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER}</li> + * <li>{@link #BOOKMARK_TYPE_OTHER_FOLDER}</li> + * <li>{@link #BOOKMARK_TYPE_MOBILE_FOLDER}</li> + * </ul> + * </p> + * <p> The TYPE_BOOKMARK_BAR_FOLDER, TYPE_OTHER_FOLDER and TYPE_MOBILE_FOLDER + * can not be updated or deleted.</p> + */ + public static final String TYPE = "type"; + + /** * The content:// style URI for the default folder + * @hide */ public static final Uri CONTENT_URI_DEFAULT_FOLDER = Uri.withAppendedPath(CONTENT_URI, "folder"); /** * Query parameter used to specify an account name + * @hide */ public static final String PARAM_ACCOUNT_NAME = "acct_name"; /** * Query parameter used to specify an account type + * @hide */ public static final String PARAM_ACCOUNT_TYPE = "acct_type"; /** * Builds a URI that points to a specific folder. * @param folderId the ID of the folder to point to + * @hide */ public static final Uri buildFolderUri(long folderId) { return ContentUris.withAppendedId(CONTENT_URI_DEFAULT_FOLDER, folderId); @@ -255,6 +338,7 @@ public class BrowserContract { * Query parameter to use if you want to see deleted bookmarks that are still * around on the device and haven't been synced yet. * @see #IS_DELETED + * @hide */ public static final String QUERY_PARAMETER_SHOW_DELETED = "show_deleted"; @@ -262,6 +346,7 @@ public class BrowserContract { * Flag indicating if an item is a folder or bookmark. Non-zero values indicate * a folder and zero indicates a bookmark. * <P>Type: INTEGER (boolean)</P> + * @hide */ public static final String IS_FOLDER = "folder"; @@ -274,6 +359,7 @@ public class BrowserContract { /** * The source ID for an item's parent. Read-only. * @see #PARENT + * @hide */ public static final String PARENT_SOURCE_ID = "parent_source"; @@ -281,6 +367,7 @@ public class BrowserContract { * The position of the bookmark in relation to it's siblings that share the same * {@link #PARENT}. May be negative. * <P>Type: INTEGER</P> + * @hide */ public static final String POSITION = "position"; @@ -288,6 +375,7 @@ public class BrowserContract { * The item that the bookmark should be inserted after. * May be negative. * <P>Type: INTEGER</P> + * @hide */ public static final String INSERT_AFTER = "insert_after"; @@ -296,6 +384,7 @@ public class BrowserContract { * May be negative. * <P>Type: INTEGER</P> * @see #INSERT_AFTER + * @hide */ public static final String INSERT_AFTER_SOURCE_ID = "insert_after_source"; @@ -305,12 +394,14 @@ public class BrowserContract { * to the URI when performing your query. * <p>Type: INTEGER (non-zero if the item has been deleted, zero if it hasn't) * @see #QUERY_PARAMETER_SHOW_DELETED + * @hide */ public static final String IS_DELETED = "deleted"; } /** * Read-only table that lists all the accounts that are used to provide bookmarks. + * @hide */ public static final class Accounts { /** @@ -410,6 +501,7 @@ public class BrowserContract { * A table provided for sync adapters to use for storing private sync state data. * * @see SyncStateContract + * @hide */ public static final class SyncState implements SyncStateContract.Columns { /** @@ -459,8 +551,18 @@ public class BrowserContract { } /** - * Stores images for URLs. Only support query() and update(). - * @hide + * <p> + * Stores images for URLs. + * </p> + * <p> + * The rows in this table can not be updated since there might have multiple URLs mapping onto + * the same image. If you want to update a URL's image, you need to add the new image in this + * table, then update the mapping onto the added image. + * </p> + * <p> + * Every image should be at least associated with one URL, otherwise it will be removed after a + * while. + * </p> */ public static final class Images implements ImageColumns { /** @@ -474,15 +576,93 @@ public class BrowserContract { public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "images"); /** + * The MIME type of {@link #CONTENT_URI} providing a directory of images. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/images"; + + /** + * The MIME type of a {@link #CONTENT_URI} of a single image. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/images"; + + /** + * Used in {@link Images#TYPE} column and indicats the row is a favicon. + */ + public static final int IMAGE_TYPE_FAVICON = 1; + + /** + * Used in {@link Images#TYPE} column and indicats the row is a precomposed touch icon. + */ + public static final int IMAGE_TYPE_PRECOMPOSED_TOUCH_ICON = 2; + + /** + * Used in {@link Images#TYPE} column and indicats the row is a touch icon. + */ + public static final int IMAGE_TYPE_TOUCH_ICON = 4; + + /** + * The type of item in the table. + * <P>Type: INTEGER</P> + * <p>Allowed values are:</p> + * <p> + * <ul> + * <li>{@link #IMAGE_TYPE_FAVICON}</li> + * <li>{@link #IMAGE_TYPE_PRECOMPOSED_TOUCH_ICON}</li> + * <li>{@link #IMAGE_TYPE_TOUCH_ICON}</li> + * </ul> + * </p> + */ + public static final String TYPE = "type"; + + /** + * The image data. + * <p>Type: BLOB (image)</p> + */ + public static final String DATA = "data"; + + /** * The URL the images came from. * <P>Type: TEXT (URL)</P> + * @hide */ public static final String URL = "url_key"; } /** + * <p> + * A table that stores the mappings between the image and the URL. + * </p> + * <p> + * Deleting or Updating a mapping might also deletes the mapped image if there is no other URL + * maps onto it. + * </p> + */ + public static final class ImageMappings implements ImageMappingColumns { + /** + * This utility class cannot be instantiated + */ + private ImageMappings() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "image_mappings"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of image mappings. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image_mappings"; + + /** + * The MIME type of a {@link #CONTENT_URI} of a single image mapping. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/image_mappings"; + } + + /** * A combined view of bookmarks and history. All bookmarks in all folders are included and * no folders are included. + * @hide */ public static final class Combined implements CommonColumns, HistoryColumns, ImageColumns { /** @@ -505,6 +685,7 @@ public class BrowserContract { /** * A table that stores settings specific to the browser. Only support query and insert. + * @hide */ public static final class Settings { /** diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 0e9306b..035d8c4 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -1536,7 +1536,11 @@ public final class ContactsContract { * * @param resolver the ContentResolver to use * @param contactId the person who was contacted + * + * @deprecated The class DataUsageStatUpdater of the Android support library should + * be used instead. */ + @Deprecated public static void markAsContacted(ContentResolver resolver, long contactId) { Uri uri = ContentUris.withAppendedId(CONTENT_URI, contactId); ContentValues values = new ContentValues(); @@ -7452,7 +7456,7 @@ public final class ContactsContract { /** * <p> * API allowing applications to send usage information for each {@link Data} row to the - * Contacts Provider. + * Contacts Provider. Applications can also clear all usage information. * </p> * <p> * With the feedback, Contacts Provider may return more contextually appropriate results for @@ -7497,6 +7501,12 @@ public final class ContactsContract { * boolean successful = resolver.update(uri, new ContentValues(), null, null) > 0; * </pre> * </p> + * <p> + * Applications can also clear all usage information with: + * <pre> + * boolean successful = resolver.delete(DataUsageFeedback.DELETE_USAGE_URI, null, null) > 0; + * </pre> + * </p> */ public static final class DataUsageFeedback { @@ -7508,6 +7518,14 @@ public final class ContactsContract { Uri.withAppendedPath(Data.CONTENT_URI, "usagefeedback"); /** + * The content:// style URI for deleting all usage information. + * Must be used with {@link ContentResolver#delete(Uri, String, String[])}. + * The {@code where} and {@code selectionArgs} parameters are ignored. + */ + public static final Uri DELETE_USAGE_URI = + Uri.withAppendedPath(Contacts.CONTENT_URI, "delete_usage"); + + /** * <p> * Name for query parameter specifying the type of data usage. * </p> diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java index bd6170b..cd8d51f 100644 --- a/core/java/android/provider/Downloads.java +++ b/core/java/android/provider/Downloads.java @@ -704,6 +704,37 @@ public final class Downloads { */ public static final int STATUS_BLOCKED = 498; + /** {@hide} */ + public static String statusToString(int status) { + switch (status) { + case STATUS_PENDING: return "PENDING"; + case STATUS_RUNNING: return "RUNNING"; + case STATUS_PAUSED_BY_APP: return "PAUSED_BY_APP"; + case STATUS_WAITING_TO_RETRY: return "WAITING_TO_RETRY"; + case STATUS_WAITING_FOR_NETWORK: return "WAITING_FOR_NETWORK"; + case STATUS_QUEUED_FOR_WIFI: return "QUEUED_FOR_WIFI"; + case STATUS_INSUFFICIENT_SPACE_ERROR: return "INSUFFICIENT_SPACE_ERROR"; + case STATUS_DEVICE_NOT_FOUND_ERROR: return "DEVICE_NOT_FOUND_ERROR"; + case STATUS_SUCCESS: return "SUCCESS"; + case STATUS_BAD_REQUEST: return "BAD_REQUEST"; + case STATUS_NOT_ACCEPTABLE: return "NOT_ACCEPTABLE"; + case STATUS_LENGTH_REQUIRED: return "LENGTH_REQUIRED"; + case STATUS_PRECONDITION_FAILED: return "PRECONDITION_FAILED"; + case STATUS_FILE_ALREADY_EXISTS_ERROR: return "FILE_ALREADY_EXISTS_ERROR"; + case STATUS_CANNOT_RESUME: return "CANNOT_RESUME"; + case STATUS_CANCELED: return "CANCELED"; + case STATUS_UNKNOWN_ERROR: return "UNKNOWN_ERROR"; + case STATUS_FILE_ERROR: return "FILE_ERROR"; + case STATUS_UNHANDLED_REDIRECT: return "UNHANDLED_REDIRECT"; + case STATUS_UNHANDLED_HTTP_CODE: return "UNHANDLED_HTTP_CODE"; + case STATUS_HTTP_DATA_ERROR: return "HTTP_DATA_ERROR"; + case STATUS_HTTP_EXCEPTION: return "HTTP_EXCEPTION"; + case STATUS_TOO_MANY_REDIRECTS: return "TOO_MANY_REDIRECTS"; + case STATUS_BLOCKED: return "BLOCKED"; + default: return Integer.toString(status); + } + } + /** * This download is visible but only shows in the notifications * while it's in progress. diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 2aaf548..6dfbb2f 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1464,6 +1464,20 @@ public final class Settings { public static final String VIBRATE_ON = "vibrate_on"; /** + * If 1, redirects the system vibrator to all currently attached input devices + * that support vibration. If there are no such input devices, then the system + * vibrator is used instead. + * If 0, does not register the system vibrator. + * + * This setting is mainly intended to provide a compatibility mechanism for + * applications that only know about the system vibrator and do not use the + * input device vibrator API. + * + * @hide + */ + public static final String VIBRATE_INPUT_DEVICES = "vibrate_input_devices"; + + /** * Ringer volume. This is used internally, changing this value will not * change the volume. See AudioManager. */ @@ -1970,6 +1984,7 @@ public final class Settings { SCREEN_BRIGHTNESS_MODE, SCREEN_AUTO_BRIGHTNESS_ADJ, VIBRATE_ON, + VIBRATE_INPUT_DEVICES, MODE_RINGER, MODE_RINGER_STREAMS_AFFECTED, MUTE_STREAMS_AFFECTED, diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java index 8c97293..35e2e4a 100644 --- a/core/java/android/text/Html.java +++ b/core/java/android/text/Html.java @@ -147,6 +147,15 @@ public class Html { return out.toString(); } + /** + * Returns an HTML escaped representation of the given plain text. + */ + public static String escapeHtml(CharSequence text) { + StringBuilder out = new StringBuilder(); + withinStyle(out, text, 0, text.length()); + return out.toString(); + } + private static void withinHtml(StringBuilder out, Spanned text) { int len = text.length(); @@ -370,7 +379,7 @@ public class Html { } } - private static void withinStyle(StringBuilder out, Spanned text, + private static void withinStyle(StringBuilder out, CharSequence text, int start, int end) { for (int i = start; i < end; i++) { char c = text.charAt(i); diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index 6056c75..11c169e 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -75,7 +75,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (spans[i] instanceof NoCopySpan) { continue; } - + int st = sp.getSpanStart(spans[i]) - start; int en = sp.getSpanEnd(spans[i]) - start; int fl = sp.getSpanFlags(spans[i]); @@ -212,7 +212,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (mGapLength > 2 * length()) resizeFor(length()); - + return ret; // == this } @@ -220,7 +220,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable public void clear() { replace(0, length(), "", 0, 0); } - + // Documentation from interface public void clearSpans() { for (int i = mSpanCount - 1; i >= 0; i--) { @@ -257,45 +257,50 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return append(String.valueOf(text)); } - private void change(int start, int end, CharSequence tb, int tbstart, int tbend) { - checkRange("replace", start, end); + private void change(int start, int end, CharSequence cs, int csStart, int csEnd) { + // Can be negative + final int nbNewChars = (csEnd - csStart) - (end - start); for (int i = mSpanCount - 1; i >= 0; i--) { - if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { - int st = mSpanStarts[i]; - if (st > mGapStart) - st -= mGapLength; + int spanStart = mSpanStarts[i]; + if (spanStart > mGapStart) + spanStart -= mGapLength; - int en = mSpanEnds[i]; - if (en > mGapStart) - en -= mGapLength; + int spanEnd = mSpanEnds[i]; + if (spanEnd > mGapStart) + spanEnd -= mGapLength; - int ost = st; - int oen = en; + if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { + int ost = spanStart; + int oen = spanEnd; int clen = length(); - if (st > start && st <= end) { - for (st = end; st < clen; st++) - if (st > end && charAt(st - 1) == '\n') + if (spanStart > start && spanStart <= end) { + for (spanStart = end; spanStart < clen; spanStart++) + if (spanStart > end && charAt(spanStart - 1) == '\n') break; } - if (en > start && en <= end) { - for (en = end; en < clen; en++) - if (en > end && charAt(en - 1) == '\n') + if (spanEnd > start && spanEnd <= end) { + for (spanEnd = end; spanEnd < clen; spanEnd++) + if (spanEnd > end && charAt(spanEnd - 1) == '\n') break; } - if (st != ost || en != oen) - setSpan(false, mSpans[i], st, en, mSpanFlags[i]); + if (spanStart != ost || spanEnd != oen) + setSpan(false, mSpans[i], spanStart, spanEnd, mSpanFlags[i]); } + + int flags = 0; + if (spanStart == start) flags |= SPAN_START_AT_START; + else if (spanStart == end + nbNewChars) flags |= SPAN_START_AT_END; + if (spanEnd == start) flags |= SPAN_END_AT_START; + else if (spanEnd == end + nbNewChars) flags |= SPAN_END_AT_END; + mSpanFlags[i] |= flags; } moveGapTo(end); - // Can be negative - final int nbNewChars = (tbend - tbstart) - (end - start); - if (nbNewChars >= mGapLength) { resizeFor(mText.length + nbNewChars - mGapLength); } @@ -306,7 +311,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (mGapLength < 1) new Exception("mGapLength < 1").printStackTrace(); - TextUtils.getChars(tb, tbstart, tbend, mText, start); + TextUtils.getChars(cs, csStart, csEnd, mText, start); if (end > start) { // no need for span fixup on pure insertion @@ -340,21 +345,23 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } } - if (tb instanceof Spanned) { - Spanned sp = (Spanned) tb; - Object[] spans = sp.getSpans(tbstart, tbend, Object.class); + mSpanCountBeforeAdd = mSpanCount; + + if (cs instanceof Spanned) { + Spanned sp = (Spanned) cs; + Object[] spans = sp.getSpans(csStart, csEnd, Object.class); for (int i = 0; i < spans.length; i++) { int st = sp.getSpanStart(spans[i]); int en = sp.getSpanEnd(spans[i]); - if (st < tbstart) st = tbstart; - if (en > tbend) en = tbend; + if (st < csStart) st = csStart; + if (en > csEnd) en = csEnd; // Add span only if this object is not yet used as a span in this string - if (getSpanStart(spans[i]) < 0) { - setSpan(false, spans[i], st - tbstart + start, en - tbstart + start, - sp.getSpanFlags(spans[i])); + if (getSpanStart(spans[i]) < 0 && !(spans[i] instanceof SpanWatcher)) { + setSpan(false, spans[i], st - csStart + start, en - csStart + start, + sp.getSpanFlags(spans[i])); } } } @@ -390,6 +397,8 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // Documentation from interface public SpannableStringBuilder replace(final int start, final int end, CharSequence tb, int tbstart, int tbend) { + checkRange("replace", start, end); + int filtercount = mFilters.length; for (int i = 0; i < filtercount; i++) { CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end); @@ -404,10 +413,6 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable final int origLen = end - start; final int newLen = tbend - tbstart; - if (origLen == 0 && newLen == 0) { - return this; - } - TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class); sendBeforeTextChanged(textWatchers, start, origLen, newLen); @@ -415,43 +420,101 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // a text replacement. If replaced or replacement text length is zero, this // is already taken care of. boolean adjustSelection = origLen != 0 && newLen != 0; - int selstart = 0; - int selend = 0; + int selectionStart = 0; + int selectionEnd = 0; if (adjustSelection) { - selstart = Selection.getSelectionStart(this); - selend = Selection.getSelectionEnd(this); + selectionStart = Selection.getSelectionStart(this); + selectionEnd = Selection.getSelectionEnd(this); } - checkRange("replace", start, end); - change(start, end, tb, tbstart, tbend); if (adjustSelection) { - if (selstart > start && selstart < end) { - long off = selstart - start; - - off = off * newLen / origLen; - selstart = (int) off + start; + if (selectionStart > start && selectionStart < end) { + final int offset = (selectionStart - start) * newLen / origLen; + selectionStart = start + offset; - setSpan(false, Selection.SELECTION_START, selstart, selstart, + setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart, Spanned.SPAN_POINT_POINT); } - if (selend > start && selend < end) { - long off = selend - start; + if (selectionEnd > start && selectionEnd < end) { + final int offset = (selectionEnd - start) * newLen / origLen; + selectionEnd = start + offset; - off = off * newLen / origLen; - selend = (int) off + start; - - setSpan(false, Selection.SELECTION_END, selend, selend, Spanned.SPAN_POINT_POINT); + setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd, + Spanned.SPAN_POINT_POINT); } } sendTextChanged(textWatchers, start, origLen, newLen); sendAfterTextChanged(textWatchers); + // Span watchers need to be called after text watchers, which may update the layout + sendToSpanWatchers(start, end, newLen - origLen); + return this; } + private void sendToSpanWatchers(int replaceStart, int replaceEnd, int nbNewChars) { + for (int i = 0; i < mSpanCountBeforeAdd; i++) { + int spanStart = mSpanStarts[i]; + int spanEnd = mSpanEnds[i]; + if (spanStart > mGapStart) spanStart -= mGapLength; + if (spanEnd > mGapStart) spanEnd -= mGapLength; + int spanFlags = mSpanFlags[i]; + + int newReplaceEnd = replaceEnd + nbNewChars; + boolean spanChanged = false; + int previousSpanStart = spanStart; + if (spanStart > newReplaceEnd) { + if (nbNewChars != 0) { + previousSpanStart -= nbNewChars; + spanChanged = true; + } + } else if (spanStart >= replaceStart) { + // No change if span start was already at replace interval boundaries before replace + if ((spanStart != replaceStart || + ((spanFlags & SPAN_START_AT_START) != SPAN_START_AT_START)) && + (spanStart != newReplaceEnd || + ((spanFlags & SPAN_START_AT_END) != SPAN_START_AT_END))) { + // TODO previousSpanStart is incorrect, but we would need to save all the + // previous spans' positions before replace to provide it + spanChanged = true; + } + } + int previousSpanEnd = spanEnd; + if (spanEnd > newReplaceEnd) { + if (nbNewChars != 0) { + previousSpanEnd -= nbNewChars; + spanChanged = true; + } + } else if (spanEnd >= replaceStart) { + // No change if span start was already at replace interval boundaries before replace + if ((spanEnd != replaceStart || + ((spanFlags & SPAN_END_AT_START) != SPAN_END_AT_START)) && + (spanEnd != newReplaceEnd || + ((spanFlags & SPAN_END_AT_END) != SPAN_END_AT_END))) { + // TODO same as above for previousSpanEnd + spanChanged = true; + } + } + + if (spanChanged) { + sendSpanChanged(mSpans[i], previousSpanStart, previousSpanEnd, spanStart, spanEnd); + } + mSpanFlags[i] &= ~SPAN_START_END_MASK; + } + + // The spans starting at mIntermediateSpanCount were added from the replacement text + for (int i = mSpanCountBeforeAdd; i < mSpanCount; i++) { + int spanStart = mSpanStarts[i]; + int spanEnd = mSpanEnds[i]; + if (spanStart > mGapStart) spanStart -= mGapLength; + if (spanEnd > mGapStart) spanEnd -= mGapLength; + sendSpanAdded(mSpans[i], spanStart, spanEnd); + } + } + /** * Mark the specified range of text with the specified object. * The flags determine how the span will behave when text is @@ -788,13 +851,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (end <= mGapStart) { System.arraycopy(mText, start, dest, destoff, end - start); } else if (start >= mGapStart) { - System.arraycopy(mText, start + mGapLength, - dest, destoff, end - start); + System.arraycopy(mText, start + mGapLength, dest, destoff, end - start); } else { System.arraycopy(mText, start, dest, destoff, mGapStart - start); System.arraycopy(mText, mGapStart + mGapLength, - dest, destoff + (mGapStart - start), - end - mGapStart); + dest, destoff + (mGapStart - start), + end - mGapStart); } } @@ -863,12 +925,14 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } } - private void sendSpanChanged(Object what, int s, int e, int st, int en) { - SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en), SpanWatcher.class); - int n = recip.length; - + private void sendSpanChanged(Object what, int oldStart, int oldEnd, int start, int end) { + // The bounds of a possible SpanWatcher are guaranteed to be set before this method is + // called, so that the order of the span does not affect this broadcast. + SpanWatcher[] spanWatchers = getSpans(Math.min(oldStart, start), + Math.min(Math.max(oldEnd, end), length()), SpanWatcher.class); + int n = spanWatchers.length; for (int i = 0; i < n; i++) { - recip[i].onSpanChanged(this, what, s, e, st, en); + spanWatchers[i].onSpanChanged(this, what, oldStart, oldEnd, start, end); } } @@ -879,26 +943,23 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private void checkRange(final String operation, int start, int end) { if (end < start) { throw new IndexOutOfBoundsException(operation + " " + - region(start, end) + - " has end before start"); + region(start, end) + " has end before start"); } int len = length(); if (start > len || end > len) { throw new IndexOutOfBoundsException(operation + " " + - region(start, end) + - " ends beyond length " + len); + region(start, end) + " ends beyond length " + len); } if (start < 0 || end < 0) { throw new IndexOutOfBoundsException(operation + " " + - region(start, end) + - " starts before 0"); + region(start, end) + " starts before 0"); } } -/* + /* private boolean isprint(char c) { // XXX if (c >= ' ' && c <= '~') return true; @@ -977,7 +1038,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable System.out.print("\n"); } -*/ + */ /** * Don't call this yourself -- exists for Canvas to use internally. @@ -1023,7 +1084,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } } - /** + /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ @@ -1059,8 +1120,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (end <= mGapStart) { ret = p.getTextWidths(mText, start, end - start, widths); } else if (start >= mGapStart) { - ret = p.getTextWidths(mText, start + mGapLength, end - start, - widths); + ret = p.getTextWidths(mText, start + mGapLength, end - start, widths); } else { char[] buf = TextUtils.obtain(end - start); @@ -1205,6 +1265,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private int[] mSpanEnds; private int[] mSpanFlags; private int mSpanCount; + private int mSpanCountBeforeAdd; // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned} private static final int MARK = 1; @@ -1214,4 +1275,11 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private static final int START_MASK = 0xF0; private static final int END_MASK = 0x0F; private static final int START_SHIFT = 4; + + // These bits are not (currently) used by SPANNED flags + private static final int SPAN_START_AT_START = 0x1000; + private static final int SPAN_START_AT_END = 0x2000; + private static final int SPAN_END_AT_START = 0x4000; + private static final int SPAN_END_AT_END = 0x8000; + private static final int SPAN_START_END_MASK = 0xF000; } diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java new file mode 100644 index 0000000..ab21b32 --- /dev/null +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -0,0 +1,900 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.accessibility.AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.util.Pool; +import android.util.Poolable; +import android.util.PoolableManager; +import android.util.Pools; +import android.util.SparseLongArray; +import android.view.ViewGroup.ChildListForAccessibility; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class for managing accessibility interactions initiated from the system + * and targeting the view hierarchy. A *ClientThread method is to be + * called from the interaction connection ViewAncestor gives the system to + * talk to it and a corresponding *UiThread method that is executed on the + * UI thread. + */ +final class AccessibilityInteractionController { + private static final int POOL_SIZE = 5; + + private ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = + new ArrayList<AccessibilityNodeInfo>(); + + private final Handler mHandler = new PrivateHandler(); + + private final ViewRootImpl mViewRootImpl; + + private final AccessibilityNodePrefetcher mPrefetcher; + + public AccessibilityInteractionController(ViewRootImpl viewRootImpl) { + mViewRootImpl = viewRootImpl; + mPrefetcher = new AccessibilityNodePrefetcher(); + } + + // Reusable poolable arguments for interacting with the view hierarchy + // to fit more arguments than Message and to avoid sharing objects between + // two messages since several threads can send messages concurrently. + private final Pool<SomeArgs> mPool = Pools.synchronizedPool(Pools.finitePool( + new PoolableManager<SomeArgs>() { + public SomeArgs newInstance() { + return new SomeArgs(); + } + + public void onAcquired(SomeArgs info) { + /* do nothing */ + } + + public void onReleased(SomeArgs info) { + info.clear(); + } + }, POOL_SIZE) + ); + + private class SomeArgs implements Poolable<SomeArgs> { + private SomeArgs mNext; + private boolean mIsPooled; + + public Object arg1; + public Object arg2; + public int argi1; + public int argi2; + public int argi3; + + public SomeArgs getNextPoolable() { + return mNext; + } + + public boolean isPooled() { + return mIsPooled; + } + + public void setNextPoolable(SomeArgs args) { + mNext = args; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + + private void clear() { + arg1 = null; + arg2 = null; + argi1 = 0; + argi2 = 0; + argi3 = 0; + } + } + + public void findAccessibilityNodeInfoByAccessibilityIdClientThread( + long accessibilityNodeId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; + message.arg1 = flags; + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interrogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { + final int flags = message.arg1; + SomeArgs args = (SomeArgs) message.obj; + final int accessibilityViewId = args.argi1; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; + infos.clear(); + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { + root = mViewRootImpl.mView; + } else { + root = findViewByAccessibilityId(accessibilityViewId); + } + if (root != null && isDisplayedOnScreen(root)) { + mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos); + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + infos.clear(); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, + int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + int flags, int interrogatingPid, long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; + message.arg1 = flags; + message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + SomeArgs args = mPool.acquire(); + args.argi1 = viewId; + args.argi2 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interrogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findAccessibilityNodeInfoByViewIdUiThread(Message message) { + final int flags = message.arg1; + final int accessibilityViewId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int viewId = args.argi1; + final int interactionId = args.argi2; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + AccessibilityNodeInfo info = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null) { + View target = root.findViewById(viewId); + if (target != null && isDisplayedOnScreen(target)) { + info = target.createAccessibilityNodeInfo(); + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfoResult(info, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId, + String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, + int flags, int interrogatingPid, long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; + message.arg1 = flags; + SomeArgs args = mPool.acquire(); + args.arg1 = text; + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; + args.arg2 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interrogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findAccessibilityNodeInfosByTextUiThread(Message message) { + final int flags = message.arg1; + SomeArgs args = (SomeArgs) message.obj; + final String text = (String) args.arg1; + final int accessibilityViewId = args.argi1; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg2; + mPool.release(args); + List<AccessibilityNodeInfo> infos = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null && isDisplayedOnScreen(root)) { + AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider(); + if (provider != null) { + infos = provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { + ArrayList<View> foundViews = mViewRootImpl.mAttachInfo.mTempArrayList; + foundViews.clear(); + root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT + | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION + | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS); + if (!foundViews.isEmpty()) { + infos = mTempAccessibilityNodeInfoList; + infos.clear(); + final int viewCount = foundViews.size(); + for (int i = 0; i < viewCount; i++) { + View foundView = foundViews.get(i); + if (isDisplayedOnScreen(foundView)) { + provider = foundView.getAccessibilityNodeProvider(); + if (provider != null) { + List<AccessibilityNodeInfo> infosFromProvider = + provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + if (infosFromProvider != null) { + infos.addAll(infosFromProvider); + } + } else { + infos.add(foundView.createAccessibilityNodeInfo()); + } + } + } + } + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findFocusClientThread(long accessibilityNodeId, int interactionId, int focusType, + IAccessibilityInteractionConnectionCallback callback, int flags, int interogatingPid, + long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_FOCUS; + message.arg1 = flags; + message.arg2 = focusType; + SomeArgs args = mPool.acquire(); + args.argi1 = interactionId; + args.argi2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi3 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findFocusUiThread(Message message) { + final int flags = message.arg1; + final int focusType = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int interactionId = args.argi1; + final int accessibilityViewId = args.argi2; + final int virtualDescendantId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + AccessibilityNodeInfo focused = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null && isDisplayedOnScreen(root)) { + switch (focusType) { + case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: { + View host = mViewRootImpl.mAccessibilityFocusedHost; + // If there is no accessibility focus host or it is not a descendant + // of the root from which to start the search, then the search failed. + if (host == null || !ViewRootImpl.isViewDescendantOf(host, root)) { + break; + } + // If the host has a provider ask this provider to search for the + // focus instead fetching all provider nodes to do the search here. + AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider(); + if (provider != null) { + focused = provider.findAccessibilitiyFocus(virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + focused = host.createAccessibilityNodeInfo(); + } + } break; + case AccessibilityNodeInfo.FOCUS_INPUT: { + // Input focus cannot go to virtual views. + View target = root.findFocus(); + if (target != null && isDisplayedOnScreen(target)) { + focused = target.createAccessibilityNodeInfo(); + } + } break; + default: + throw new IllegalArgumentException("Unknown focus type: " + focusType); + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfoResult(focused, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void focusSearchClientThread(long accessibilityNodeId, int interactionId, int direction, + IAccessibilityInteractionConnectionCallback callback, int flags, int interogatingPid, + long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FOCUS_SEARCH; + message.arg1 = flags; + message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi2 = direction; + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void focusSearchUiThread(Message message) { + final int flags = message.arg1; + final int accessibilityViewId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int virtualDescendantId = args.argi1; + final int direction = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + AccessibilityNodeInfo next = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null && isDisplayedOnScreen(root)) { + if ((direction & View.FOCUS_ACCESSIBILITY) == View.FOCUS_ACCESSIBILITY) { + AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider(); + if (provider != null) { + next = provider.accessibilityFocusSearch(direction, + virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + View nextView = root.focusSearch(direction); + if (nextView != null) { + // If the focus search reached a node with a provider + // we delegate to the provider to find the next one. + provider = nextView.getAccessibilityNodeProvider(); + if (provider != null) { + next = provider.accessibilityFocusSearch(direction, + virtualDescendantId); + } else { + next = nextView.createAccessibilityNodeInfo(); + } + } + } + } else { + View nextView = root.focusSearch(direction); + if (nextView != null) { + next = nextView.createAccessibilityNodeInfo(); + } + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfoResult(next, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void performAccessibilityActionClientThread(long accessibilityNodeId, int action, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interogatingPid, long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_PERFORM_ACCESSIBILITY_ACTION; + message.arg1 = flags; + message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi2 = action; + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void perfromAccessibilityActionUiThread(Message message) { + final int flags = message.arg1; + final int accessibilityViewId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int virtualDescendantId = args.argi1; + final int action = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + boolean succeeded = false; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View target = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + target = findViewByAccessibilityId(accessibilityViewId); + } else { + target = mViewRootImpl.mView; + } + if (target != null && isDisplayedOnScreen(target)) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + succeeded = provider.performAccessibilityAction(action, virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + succeeded = target.performAccessibilityAction(action); + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setPerformAccessibilityActionResult(succeeded, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + private View findViewByAccessibilityId(int accessibilityId) { + View root = mViewRootImpl.mView; + if (root == null) { + return null; + } + View foundView = root.findViewByAccessibilityId(accessibilityId); + if (foundView != null && !isDisplayedOnScreen(foundView)) { + return null; + } + return foundView; + } + + /** + * Computes whether a view is visible on the screen. + * + * @param view The view to check. + * @return Whether the view is visible on the screen. + */ + private boolean isDisplayedOnScreen(View view) { + // The first two checks are made also made by isShown() which + // however traverses the tree up to the parent to catch that. + // Therefore, we do some fail fast check to minimize the up + // tree traversal. + return (view.mAttachInfo != null + && view.mAttachInfo.mWindowVisibility == View.VISIBLE + && view.isShown() + && view.getGlobalVisibleRect(mViewRootImpl.mTempRect)); + } + + /** + * This class encapsulates a prefetching strategy for the accessibility APIs for + * querying window content. It is responsible to prefetch a batch of + * AccessibilityNodeInfos in addition to the one for a requested node. + */ + private class AccessibilityNodePrefetcher { + + private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50; + + public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int prefetchFlags, + List<AccessibilityNodeInfo> outInfos) { + AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); + if (provider == null) { + AccessibilityNodeInfo root = view.createAccessibilityNodeInfo(); + if (root != null) { + outInfos.add(root); + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + prefetchPredecessorsOfRealNode(view, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + prefetchSiblingsOfRealNode(view, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + prefetchDescendantsOfRealNode(view, outInfos); + } + } + } else { + AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId); + if (root != null) { + outInfos.add(root); + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + prefetchSiblingsOfVirtualNode(root, view, provider, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + prefetchDescendantsOfVirtualNode(root, provider, outInfos); + } + } + } + } + + private void prefetchPredecessorsOfRealNode(View view, + List<AccessibilityNodeInfo> outInfos) { + ViewParent parent = view.getParentForAccessibility(); + while (parent instanceof View + && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + View parentView = (View) parent; + final long parentNodeId = AccessibilityNodeInfo.makeNodeId( + parentView.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); + AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo(); + if (info != null) { + outInfos.add(info); + } + parent = parent.getParentForAccessibility(); + } + } + + private void prefetchSiblingsOfRealNode(View current, + List<AccessibilityNodeInfo> outInfos) { + ViewParent parent = current.getParentForAccessibility(); + if (parent instanceof ViewGroup) { + ViewGroup parentGroup = (ViewGroup) parent; + ChildListForAccessibility children = ChildListForAccessibility.obtain(parentGroup, + false); + final int childCount = children.getChildCount(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + children.recycle(); + return; + } + View child = children.getChildAt(i); + if (child.getAccessibilityViewId() != current.getAccessibilityViewId() + && isDisplayedOnScreen(child)) { + AccessibilityNodeInfo info = null; + AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); + if (provider == null) { + info = child.createAccessibilityNodeInfo(); + } else { + info = provider.createAccessibilityNodeInfo( + AccessibilityNodeInfo.UNDEFINED); + } + if (info != null) { + outInfos.add(info); + } + } + } + children.recycle(); + } + } + + private void prefetchDescendantsOfRealNode(View root, + List<AccessibilityNodeInfo> outInfos) { + if (!(root instanceof ViewGroup)) { + return; + } + ViewGroup rootGroup = (ViewGroup) root; + HashMap<View, AccessibilityNodeInfo> addedChildren = + new HashMap<View, AccessibilityNodeInfo>(); + ChildListForAccessibility children = ChildListForAccessibility.obtain(rootGroup, false); + final int childCount = children.getChildCount(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + children.recycle(); + return; + } + View child = children.getChildAt(i); + if ( isDisplayedOnScreen(child)) { + AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); + if (provider == null) { + AccessibilityNodeInfo info = child.createAccessibilityNodeInfo(); + if (info != null) { + outInfos.add(info); + addedChildren.put(child, null); + } + } else { + AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo( + AccessibilityNodeInfo.UNDEFINED); + if (info != null) { + outInfos.add(info); + addedChildren.put(child, info); + } + } + } + } + children.recycle(); + if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) { + View addedChild = entry.getKey(); + AccessibilityNodeInfo virtualRoot = entry.getValue(); + if (virtualRoot == null) { + prefetchDescendantsOfRealNode(addedChild, outInfos); + } else { + AccessibilityNodeProvider provider = + addedChild.getAccessibilityNodeProvider(); + prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos); + } + } + } + } + + private void prefetchPredecessorsOfVirtualNode(AccessibilityNodeInfo root, + View providerHost, AccessibilityNodeProvider provider, + List<AccessibilityNodeInfo> outInfos) { + long parentNodeId = root.getParentNodeId(); + int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); + while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + return; + } + final int virtualDescendantId = + AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); + if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED + || accessibilityViewId == providerHost.getAccessibilityViewId()) { + AccessibilityNodeInfo parent = provider.createAccessibilityNodeInfo( + virtualDescendantId); + if (parent != null) { + outInfos.add(parent); + } + parentNodeId = parent.getParentNodeId(); + accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId( + parentNodeId); + } else { + prefetchPredecessorsOfRealNode(providerHost, outInfos); + return; + } + } + } + + private void prefetchSiblingsOfVirtualNode(AccessibilityNodeInfo current, View providerHost, + AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { + final long parentNodeId = current.getParentNodeId(); + final int parentAccessibilityViewId = + AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); + final int parentVirtualDescendantId = + AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); + if (parentVirtualDescendantId != AccessibilityNodeInfo.UNDEFINED + || parentAccessibilityViewId == providerHost.getAccessibilityViewId()) { + AccessibilityNodeInfo parent = + provider.createAccessibilityNodeInfo(parentVirtualDescendantId); + if (parent != null) { + SparseLongArray childNodeIds = parent.getChildNodeIds(); + final int childCount = childNodeIds.size(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + return; + } + final long childNodeId = childNodeIds.get(i); + if (childNodeId != current.getSourceNodeId()) { + final int childVirtualDescendantId = + AccessibilityNodeInfo.getVirtualDescendantId(childNodeId); + AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( + childVirtualDescendantId); + if (child != null) { + outInfos.add(child); + } + } + } + } + } else { + prefetchSiblingsOfRealNode(providerHost, outInfos); + } + } + + private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root, + AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { + SparseLongArray childNodeIds = root.getChildNodeIds(); + final int initialOutInfosSize = outInfos.size(); + final int childCount = childNodeIds.size(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + return; + } + final long childNodeId = childNodeIds.get(i); + AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( + AccessibilityNodeInfo.getVirtualDescendantId(childNodeId)); + if (child != null) { + outInfos.add(child); + } + } + if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + final int addedChildCount = outInfos.size() - initialOutInfosSize; + for (int i = 0; i < addedChildCount; i++) { + AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i); + prefetchDescendantsOfVirtualNode(child, provider, outInfos); + } + } + } + } + + private class PrivateHandler extends Handler { + private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 3; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4; + private final static int MSG_FIND_FOCUS = 5; + private final static int MSG_FOCUS_SEARCH = 6; + + public PrivateHandler() { + super(Looper.getMainLooper()); + } + + @Override + public String getMessageName(Message message) { + final int type = message.what; + switch (type) { + case MSG_PERFORM_ACCESSIBILITY_ACTION: + return "MSG_PERFORM_ACCESSIBILITY_ACTION"; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: + return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: + return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: + return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; + case MSG_FIND_FOCUS: + return "MSG_FIND_FOCUS"; + case MSG_FOCUS_SEARCH: + return "MSG_FOCUS_SEARCH"; + default: + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: { + findAccessibilityNodeInfoByAccessibilityIdUiThread(message); + } break; + case MSG_PERFORM_ACCESSIBILITY_ACTION: { + perfromAccessibilityActionUiThread(message); + } break; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { + findAccessibilityNodeInfoByViewIdUiThread(message); + } break; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { + findAccessibilityNodeInfosByTextUiThread(message); + } break; + case MSG_FIND_FOCUS: { + findFocusUiThread(message); + } break; + case MSG_FOCUS_SEARCH: { + focusSearchUiThread(message); + } break; + default: + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + } +} diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 3529b8e..8a01c15 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -17,10 +17,12 @@ package android.view; import android.graphics.Rect; +import android.view.ViewGroup.ChildListForAccessibility; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Stack; /** * The algorithm used for finding the next focusable view in a given direction @@ -30,7 +32,7 @@ public class FocusFinder { private static ThreadLocal<FocusFinder> tlFocusFinder = new ThreadLocal<FocusFinder>() { - + @Override protected FocusFinder initialValue() { return new FocusFinder(); } @@ -48,6 +50,10 @@ public class FocusFinder { Rect mBestCandidateRect = new Rect(); SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator(); + private final ArrayList<View> mTempList = new ArrayList<View>(); + + private Stack<View> mTempStack; + // enforce thread local access private FocusFinder() {} @@ -60,7 +66,30 @@ public class FocusFinder { * @return The next focusable view, or null if none exists. */ public final View findNextFocus(ViewGroup root, View focused, int direction) { + return findNextFocus(root, focused, mFocusedRect, direction); + } + + /** + * Find the next view to take focus in root's descendants, searching from + * a particular rectangle in root's coordinates. + * @param root Contains focusedRect. Cannot be null. + * @param focusedRect The starting point of the search. + * @param direction Direction to look. + * @return The next focusable view, or null if none exists. + */ + public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { + return findNextFocus(root, null, focusedRect, direction); + } + + private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { + if ((direction & View.FOCUS_ACCESSIBILITY) != View.FOCUS_ACCESSIBILITY) { + return findNextInputFocus(root, focused, focusedRect, direction); + } else { + return findNextAccessibilityFocus(root, focused, direction); + } + } + private View findNextInputFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { if (focused != null) { // check for user specified next focus View userSetNextFocus = focused.findUserSetNextFocus(root, direction); @@ -79,90 +108,154 @@ public class FocusFinder { switch (direction) { case View.FOCUS_RIGHT: case View.FOCUS_DOWN: - setFocusBottomRight(root); + setFocusTopLeft(root); break; case View.FOCUS_FORWARD: if (root.isLayoutRtl()) { - setFocusTopLeft(root); - } else { setFocusBottomRight(root); + } else { + setFocusTopLeft(root); } break; case View.FOCUS_LEFT: case View.FOCUS_UP: - setFocusTopLeft(root); + setFocusBottomRight(root); break; case View.FOCUS_BACKWARD: if (root.isLayoutRtl()) { - setFocusBottomRight(root); - } else { setFocusTopLeft(root); + } else { + setFocusBottomRight(root); break; } } } - return findNextFocus(root, focused, mFocusedRect, direction); - } - private void setFocusTopLeft(ViewGroup root) { - final int rootBottom = root.getScrollY() + root.getHeight(); - final int rootRight = root.getScrollX() + root.getWidth(); - mFocusedRect.set(rootRight, rootBottom, - rootRight, rootBottom); - } + ArrayList<View> focusables = mTempList; + focusables.clear(); + root.addFocusables(focusables, direction); + if (focusables.isEmpty()) { + // The focus cannot change. + return null; + } - private void setFocusBottomRight(ViewGroup root) { - final int rootTop = root.getScrollY(); - final int rootLeft = root.getScrollX(); - mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + try { + switch (direction) { + case View.FOCUS_FORWARD: + case View.FOCUS_BACKWARD: + return findNextInputFocusInRelativeDirection(focusables, root, focused, + focusedRect, direction); + case View.FOCUS_UP: + case View.FOCUS_DOWN: + case View.FOCUS_LEFT: + case View.FOCUS_RIGHT: + return findNextInputFocusInAbsoluteDirection(focusables, root, focused, + focusedRect, direction); + default: + throw new IllegalArgumentException("Unknown direction: " + direction); + } + } finally { + focusables.clear(); + } } /** - * Find the next view to take focus in root's descendants, searching from - * a particular rectangle in root's coordinates. - * @param root Contains focusedRect. Cannot be null. - * @param focusedRect The starting point of the search. + * Find the next view to take accessibility focus in root's descendants, + * starting from the view that currently is accessibility focused. + * + * @param root The root which also contains the focused view. + * @param focused The current accessibility focused view. * @param direction Direction to look. * @return The next focusable view, or null if none exists. */ - public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { - return findNextFocus(root, null, focusedRect, direction); - } - - private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { - ArrayList<View> focusables = root.getFocusables(direction); - if (focusables.isEmpty()) { - // The focus cannot change. - return null; + private View findNextAccessibilityFocus(ViewGroup root, View focused, int direction) { + switch (direction) { + case View.ACCESSIBILITY_FOCUS_IN: + case View.ACCESSIBILITY_FOCUS_OUT: + case View.ACCESSIBILITY_FOCUS_FORWARD: + case View.ACCESSIBILITY_FOCUS_BACKWARD: { + return findNextHierarchicalAcessibilityFocus(root, focused, direction); + } + case View.ACCESSIBILITY_FOCUS_LEFT: + case View.ACCESSIBILITY_FOCUS_RIGHT: + case View.ACCESSIBILITY_FOCUS_UP: + case View.ACCESSIBILITY_FOCUS_DOWN: { + return findNextDirectionalAccessibilityFocus(root, focused, direction); + } + default: + throw new IllegalArgumentException("Unknown direction: " + direction); } + } - if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { - if (focused != null && !focusables.contains(focused)) { - // Add the currently focused view to the list to have it sorted - // along with the other views. - focusables.add(focused); + private View findNextHierarchicalAcessibilityFocus(ViewGroup root, View focused, + int direction) { + View current = (focused != null) ? focused : root; + switch (direction) { + case View.ACCESSIBILITY_FOCUS_IN: { + return findNextAccessibilityFocusIn(current); } - - try { - // Note: This sort is stable. - mSequentialFocusComparator.setRoot(root); - Collections.sort(focusables, mSequentialFocusComparator); - } finally { - mSequentialFocusComparator.recycle(); + case View.ACCESSIBILITY_FOCUS_OUT: { + return findNextAccessibilityFocusOut(current); + } + case View.ACCESSIBILITY_FOCUS_FORWARD: { + return findNextAccessibilityFocusForward(current); + } + case View.ACCESSIBILITY_FOCUS_BACKWARD: { + return findNextAccessibilityFocusBackward(current); } + } + return null; + } - final int count = focusables.size(); - switch (direction) { - case View.FOCUS_FORWARD: - return getForwardFocusable(root, focused, focusables, count); + private View findNextDirectionalAccessibilityFocus(ViewGroup root, View focused, + int direction) { + ArrayList<View> focusables = mTempList; + focusables.clear(); + root.addFocusables(focusables, direction, View.FOCUSABLES_ACCESSIBILITY); + Rect focusedRect = getFocusedRect(root, focused, direction); + final int inputFocusDirection = getCorrespondingInputFocusDirection(direction); + View next = findNextInputFocusInAbsoluteDirection(focusables, root, + focused, focusedRect, inputFocusDirection); + focusables.clear(); + return next; + } - case View.FOCUS_BACKWARD: - return getBackwardFocusable(root, focused, focusables, count); - } - return null; + private View findNextInputFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, + View focused, Rect focusedRect, int direction) { + try { + // Note: This sort is stable. + mSequentialFocusComparator.setRoot(root); + Collections.sort(focusables, mSequentialFocusComparator); + } finally { + mSequentialFocusComparator.recycle(); } + final int count = focusables.size(); + switch (direction) { + case View.FOCUS_FORWARD: + return getForwardFocusable(root, focused, focusables, count); + case View.FOCUS_BACKWARD: + return getBackwardFocusable(root, focused, focusables, count); + } + return focusables.get(count - 1); + } + + private void setFocusBottomRight(ViewGroup root) { + final int rootBottom = root.getScrollY() + root.getHeight(); + final int rootRight = root.getScrollX() + root.getWidth(); + mFocusedRect.set(rootRight, rootBottom, + rootRight, rootBottom); + } + + private void setFocusTopLeft(ViewGroup root) { + final int rootTop = root.getScrollY(); + final int rootLeft = root.getScrollX(); + mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + } + + View findNextInputFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, + Rect focusedRect, int direction) { // initialize the best candidate to something impossible // (so the first plausible view will become the best choice) mBestCandidateRect.set(focusedRect); @@ -201,6 +294,140 @@ public class FocusFinder { return closest; } + private View findNextAccessibilityFocusIn(View view) { + // We have to traverse the full view tree to make sure + // we consider views in the order specified by their + // parent layout managers since some managers could be + // LTR while some could be RTL. + if (mTempStack == null) { + mTempStack = new Stack<View>(); + } + Stack<View> fringe = mTempStack; + fringe.clear(); + fringe.add(view); + while (!fringe.isEmpty()) { + View current = fringe.pop(); + if (current.getAccessibilityNodeProvider() != null) { + fringe.clear(); + return current; + } + if (current != view && current.includeForAccessibility()) { + fringe.clear(); + return current; + } + if (current instanceof ViewGroup) { + ViewGroup currentGroup = (ViewGroup) current; + ChildListForAccessibility children = ChildListForAccessibility.obtain( + currentGroup, true); + final int childCount = children.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + fringe.push(children.getChildAt(i)); + } + children.recycle(); + } + } + return null; + } + + private View findNextAccessibilityFocusOut(View view) { + ViewParent parent = view.getParentForAccessibility(); + if (parent instanceof View) { + return (View) parent; + } + return null; + } + + private View findNextAccessibilityFocusForward(View view) { + // We have to traverse the full view tree to make sure + // we consider views in the order specified by their + // parent layout managers since some managers could be + // LTR while some could be RTL. + View current = view; + while (current != null) { + ViewParent parent = current.getParent(); + if (!(parent instanceof ViewGroup)) { + return null; + } + ViewGroup parentGroup = (ViewGroup) parent; + // Ask the parent to find a sibling after the current view + // that can take accessibility focus. + ChildListForAccessibility children = ChildListForAccessibility.obtain( + parentGroup, true); + final int fromIndex = children.getChildIndex(current) + 1; + final int childCount = children.getChildCount(); + for (int i = fromIndex; i < childCount; i++) { + View child = children.getChildAt(i); + View next = null; + if (child.getAccessibilityNodeProvider() != null) { + next = child; + } else if (child.includeForAccessibility()) { + next = child; + } else { + next = findNextAccessibilityFocusIn(child); + } + if (next != null) { + children.recycle(); + return next; + } + } + children.recycle(); + // Reaching a regarded for accessibility predecessor without + // finding a next view to take focus means that at this level + // there is no next accessibility focusable sibling. + if (parentGroup.includeForAccessibility()) { + return null; + } + // Try asking a predecessor to find a focusable. + current = parentGroup; + } + return null; + } + + private View findNextAccessibilityFocusBackward(View view) { + // We have to traverse the full view tree to make sure + // we consider views in the order specified by their + // parent layout managers since some managers could be + // LTR while some could be RTL. + View current = view; + while (current != null) { + ViewParent parent = current.getParent(); + if (!(parent instanceof ViewGroup)) { + return null; + } + ViewGroup parentGroup = (ViewGroup) parent; + // Ask the parent to find a sibling after the current view + // to take accessibility focus + ChildListForAccessibility children = ChildListForAccessibility.obtain( + parentGroup, true); + final int fromIndex = children.getChildIndex(current) - 1; + for (int i = fromIndex; i >= 0; i--) { + View child = children.getChildAt(i); + View next = null; + if (child.getAccessibilityNodeProvider() != null) { + next = child; + } else if (child.includeForAccessibility()) { + next = child; + } else { + next = findNextAccessibilityFocusIn(child); + } + if (next != null) { + children.recycle(); + return next; + } + } + children.recycle(); + // Reaching a regarded for accessibility predecessor without + // finding a previous view to take focus means that at this level + // there is no previous accessibility focusable sibling. + if (parentGroup.includeForAccessibility()) { + return null; + } + // Try asking a predecessor to find a focusable. + current = parentGroup; + } + return null; + } + private static View getForwardFocusable(ViewGroup root, View focused, ArrayList<View> focusables, int count) { return (root.isLayoutRtl()) ? @@ -235,6 +462,47 @@ public class FocusFinder { return focusables.get(count - 1); } + private Rect getFocusedRect(ViewGroup root, View focused, int direction) { + Rect focusedRect = mFocusedRect; + if (focused != null) { + focused.getFocusedRect(focusedRect); + root.offsetDescendantRectToMyCoords(focused, focusedRect); + } else { + switch (direction) { + case View.FOCUS_RIGHT: + case View.FOCUS_DOWN: + final int rootTop = root.getScrollY(); + final int rootLeft = root.getScrollX(); + focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + break; + + case View.FOCUS_LEFT: + case View.FOCUS_UP: + final int rootBottom = root.getScrollY() + root.getHeight(); + final int rootRight = root.getScrollX() + root.getWidth(); + focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); + break; + } + } + return focusedRect; + } + + private int getCorrespondingInputFocusDirection(int accessFocusDirection) { + switch (accessFocusDirection) { + case View.ACCESSIBILITY_FOCUS_LEFT: + return View.FOCUS_LEFT; + case View.ACCESSIBILITY_FOCUS_RIGHT: + return View.FOCUS_RIGHT; + case View.ACCESSIBILITY_FOCUS_UP: + return View.FOCUS_UP; + case View.ACCESSIBILITY_FOCUS_DOWN: + return View.FOCUS_DOWN; + default: + throw new IllegalArgumentException("Cannot map accessiblity focus" + + " direction: " + accessFocusDirection); + } + } + /** * Is rect1 a better candidate than rect2 for a focus search in a particular * direction from a source rect? This is the core routine that determines diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 75b2c746..4848a7a 100755 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -16,9 +16,12 @@ package android.view; +import android.content.Context; import android.hardware.input.InputManager; import android.os.Parcel; import android.os.Parcelable; +import android.os.Vibrator; +import android.os.NullVibrator; import java.util.ArrayList; import java.util.List; @@ -40,13 +43,17 @@ import java.util.List; */ public final class InputDevice implements Parcelable { private final int mId; + private final int mGeneration; private final String mName; private final String mDescriptor; private final int mSources; private final int mKeyboardType; private final KeyCharacterMap mKeyCharacterMap; + private final boolean mHasVibrator; private final ArrayList<MotionRange> mMotionRanges = new ArrayList<MotionRange>(); + private Vibrator mVibrator; // guarded by mMotionRanges during initialization + /** * A mask for input source classes. * @@ -302,23 +309,27 @@ public final class InputDevice implements Parcelable { }; // Called by native code. - private InputDevice(int id, String name, String descriptor, int sources, - int keyboardType, KeyCharacterMap keyCharacterMap) { + private InputDevice(int id, int generation, String name, String descriptor, int sources, + int keyboardType, KeyCharacterMap keyCharacterMap, boolean hasVibrator) { mId = id; + mGeneration = generation; mName = name; mDescriptor = descriptor; mSources = sources; mKeyboardType = keyboardType; mKeyCharacterMap = keyCharacterMap; + mHasVibrator = hasVibrator; } private InputDevice(Parcel in) { mId = in.readInt(); + mGeneration = in.readInt(); mName = in.readString(); mDescriptor = in.readString(); mSources = in.readInt(); mKeyboardType = in.readInt(); mKeyCharacterMap = KeyCharacterMap.CREATOR.createFromParcel(in); + mHasVibrator = in.readInt() != 0; for (;;) { int axis = in.readInt(); @@ -364,6 +375,19 @@ public final class InputDevice implements Parcelable { } /** + * Gets a generation number for this input device. + * The generation number is incremented whenever the device is reconfigured and its + * properties may have changed. + * + * @return The generation number. + * + * @hide + */ + public int getGeneration() { + return mGeneration; + } + + /** * Gets the input device descriptor, which is a stable identifier for an input device. * <p> * An input device descriptor uniquely identifies an input device. Its value @@ -506,6 +530,31 @@ public final class InputDevice implements Parcelable { } /** + * Gets the vibrator service associated with the device, if there is one. + * Even if the device does not have a vibrator, the result is never null. + * Use {@link Vibrator#hasVibrator} to determine whether a vibrator is + * present. + * + * Note that the vibrator associated with the device may be different from + * the system vibrator. To obtain an instance of the system vibrator instead, call + * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as argument. + * + * @return The vibrator service associated with the device, never null. + */ + public Vibrator getVibrator() { + synchronized (mMotionRanges) { + if (mVibrator == null) { + if (mHasVibrator) { + mVibrator = InputManager.getInstance().getInputDeviceVibrator(mId); + } else { + mVibrator = NullVibrator.getInstance(); + } + } + return mVibrator; + } + } + + /** * Provides information about the range of values for a particular {@link MotionEvent} axis. * * @see InputDevice#getMotionRange(int) @@ -595,11 +644,13 @@ public final class InputDevice implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(mId); + out.writeInt(mGeneration); out.writeString(mName); out.writeString(mDescriptor); out.writeInt(mSources); out.writeInt(mKeyboardType); mKeyCharacterMap.writeToParcel(out, flags); + out.writeInt(mHasVibrator ? 1 : 0); final int numRanges = mMotionRanges.size(); for (int i = 0; i < numRanges; i++) { @@ -624,6 +675,7 @@ public final class InputDevice implements Parcelable { StringBuilder description = new StringBuilder(); description.append("Input Device ").append(mId).append(": ").append(mName).append("\n"); description.append(" Descriptor: ").append(mDescriptor).append("\n"); + description.append(" Generation: ").append(mGeneration).append("\n"); description.append(" Keyboard Type: "); switch (mKeyboardType) { @@ -639,6 +691,8 @@ public final class InputDevice implements Parcelable { } description.append("\n"); + description.append(" Has Vibrator: ").append(mHasVibrator).append("\n"); + description.append(" Sources: 0x").append(Integer.toHexString(mSources)).append(" ("); appendSourceDescriptionIfApplicable(description, SOURCE_KEYBOARD, "keyboard"); appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad"); diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index 3d165ea..12d7b12 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -22,8 +22,6 @@ import android.text.method.MetaKeyKeyListener; import android.util.AndroidRuntimeException; import android.util.SparseIntArray; import android.hardware.input.InputManager; -import android.util.SparseArray; -import android.view.InputDevice.MotionRange; import java.lang.Character; diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index ba62e65..32029ba 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -187,7 +187,9 @@ public class TextureView extends View { public void setOpaque(boolean opaque) { if (opaque != mOpaque) { mOpaque = opaque; - updateLayer(); + if (mLayer != null) { + updateLayer(); + } } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1fa19d1..d569ba1 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -24,6 +24,7 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Camera; import android.graphics.Canvas; +import android.graphics.Insets; import android.graphics.Interpolator; import android.graphics.LinearGradient; import android.graphics.Matrix; @@ -975,6 +976,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; /** + * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} + * should add only accessibility focusable Views. + * + * @hide + */ + public static final int FOCUSABLES_ACCESSIBILITY = 0x00000002; + + /** * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. */ @@ -1006,6 +1015,54 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ public static final int FOCUS_DOWN = 0x00000082; + // Accessibility focus directions. + + /** + * The accessibility focus which is the current user position when + * interacting with the accessibility framework. + */ + public static final int FOCUS_ACCESSIBILITY = 0x00001000; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus left. + */ + public static final int ACCESSIBILITY_FOCUS_LEFT = FOCUS_LEFT | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus up. + */ + public static final int ACCESSIBILITY_FOCUS_UP = FOCUS_UP | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus right. + */ + public static final int ACCESSIBILITY_FOCUS_RIGHT = FOCUS_RIGHT | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus down. + */ + public static final int ACCESSIBILITY_FOCUS_DOWN = FOCUS_DOWN | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus to the next view. + */ + public static final int ACCESSIBILITY_FOCUS_FORWARD = FOCUS_FORWARD | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus to the previous view. + */ + public static final int ACCESSIBILITY_FOCUS_BACKWARD = FOCUS_BACKWARD | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus in a view. + */ + public static final int ACCESSIBILITY_FOCUS_IN = 0x00000004 | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus out of a view. + */ + public static final int ACCESSIBILITY_FOCUS_OUT = 0x00000008 | FOCUS_ACCESSIBILITY; + /** * Bits of {@link #getMeasuredWidthAndState()} and * {@link #getMeasuredWidthAndState()} that provide the actual measured size. @@ -1330,7 +1387,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal R.attr.state_accelerated, VIEW_STATE_ACCELERATED, R.attr.state_hovered, VIEW_STATE_HOVERED, R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT, - R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED, + R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED }; static { @@ -1452,7 +1509,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED - | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED; + | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; /** * Temporary Rect currently for use in setBackground(). This will probably @@ -1784,7 +1842,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ private static final int LAYOUT_DIRECTION_DEFAULT = LAYOUT_DIRECTION_INHERIT; - /** * Indicates that the view is tracking some sort of transient state * that the app should not need to be aware of, but that the framework @@ -1992,6 +2049,50 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int TEXT_ALIGNMENT_RESOLVED_DEFAULT = TEXT_ALIGNMENT_GRAVITY << TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT; + // Accessiblity constants for mPrivateFlags2 + + /** + * Shift for accessibility related bits in {@link #mPrivateFlags2}. + */ + static final int IMPORTANT_FOR_ACCESSIBILITY_SHIFT = 20; + + /** + * Automatically determine whether a view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_AUTO = 0x00000000; + + /** + * The view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 0x00000001; + + /** + * The view is not important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_NO = 0x00000002; + + /** + * The default whether the view is important for accessiblity. + */ + static final int IMPORTANT_FOR_ACCESSIBILITY_DEFAULT = IMPORTANT_FOR_ACCESSIBILITY_AUTO; + + /** + * Mask for obtainig the bits which specify how to determine + * whether a view is important for accessibility. + */ + static final int IMPORTANT_FOR_ACCESSIBILITY_MASK = (IMPORTANT_FOR_ACCESSIBILITY_AUTO + | IMPORTANT_FOR_ACCESSIBILITY_YES | IMPORTANT_FOR_ACCESSIBILITY_NO) + << IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + + /** + * Flag indicating whether a view has accessibility focus. + */ + static final int ACCESSIBILITY_FOCUSED = 0x00000040 << IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + + /** + * Flag indicating whether a view state for accessibility has changed. + */ + static final int ACCESSIBILITY_STATE_CHANGED = 0x00000080 << IMPORTANT_FOR_ACCESSIBILITY_SHIFT; /* End of masks for mPrivateFlags2 */ @@ -2598,6 +2699,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal protected int mPaddingBottom; /** + * The layout insets in pixels, that is the distance in pixels between the + * visible edges of this view its bounds. + */ + private Insets mLayoutInsets; + + /** * Briefly describes the view and is primarily used for accessibility support. */ private CharSequence mContentDescription; @@ -2952,7 +3059,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal // Set layout and text direction defaults mPrivateFlags2 = (LAYOUT_DIRECTION_DEFAULT << LAYOUT_DIRECTION_MASK_SHIFT) | (TEXT_DIRECTION_DEFAULT << TEXT_DIRECTION_MASK_SHIFT) | - (TEXT_ALIGNMENT_DEFAULT << TEXT_ALIGNMENT_MASK_SHIFT); + (TEXT_ALIGNMENT_DEFAULT << TEXT_ALIGNMENT_MASK_SHIFT) | + (IMPORTANT_FOR_ACCESSIBILITY_DEFAULT << IMPORTANT_FOR_ACCESSIBILITY_SHIFT); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); mUserPaddingStart = -1; @@ -3340,6 +3448,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal final int textAlignment = a.getInt(attr, TEXT_ALIGNMENT_DEFAULT); mPrivateFlags2 |= TEXT_ALIGNMENT_FLAGS[textAlignment]; break; + case R.styleable.View_importantForAccessibility: + setImportantForAccessibility(a.getInt(attr, + IMPORTANT_FOR_ACCESSIBILITY_DEFAULT)); } } @@ -3970,6 +4081,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } @@ -4050,16 +4165,21 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } onFocusChanged(false, 0, null); + refreshDrawableState(); ensureInputFocusOnFirstFocusable(); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } void ensureInputFocusOnFirstFocusable() { View root = getRootView(); if (root != null) { - root.requestFocus(FOCUS_FORWARD); + root.requestFocus(); } } @@ -4077,6 +4197,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal onFocusChanged(false, 0, null); refreshDrawableState(); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } @@ -4127,7 +4251,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { if (gainFocus) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + requestAccessibilityFocus(); + } } InputMethodManager imm = InputMethodManager.peekInstance(); @@ -4237,7 +4364,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { if (mAccessibilityDelegate != null) { - mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event); + mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event); } else { sendAccessibilityEventUncheckedInternal(event); } @@ -4257,6 +4384,31 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if ((event.getEventType() & POPULATING_ACCESSIBILITY_EVENT_TYPES) != 0) { dispatchPopulateAccessibilityEvent(event); } + // Intercept accessibility focus events fired by virtual nodes to keep + // track of accessibility focus position in such nodes. + final int eventType = event.getEventType(); + switch (eventType) { + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { + final long virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId( + event.getSourceNodeId()); + if (virtualNodeId != AccessibilityNodeInfo.UNDEFINED) { + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(this); + } + } + } break; + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { + final long virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId( + event.getSourceNodeId()); + if (virtualNodeId != AccessibilityNodeInfo.UNDEFINED) { + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(null); + } + } + } break; + } // In the beginning we called #isShown(), so we know that getParent() is not null. getParent().requestSendAccessibilityEvent(this, event); } @@ -4399,7 +4551,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal event.setContentDescription(mContentDescription); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && mAttachInfo != null) { - ArrayList<View> focusablesTempList = mAttachInfo.mFocusablesTempList; + ArrayList<View> focusablesTempList = mAttachInfo.mTempArrayList; getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL); event.setItemCount(focusablesTempList.size()); @@ -4488,10 +4640,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal info.setBoundsInScreen(bounds); if ((mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { - ViewParent parent = getParent(); + ViewParent parent = getParentForAccessibility(); if (parent instanceof View) { - View parentView = (View) parent; - info.setParent(parentView); + info.setParent((View) parent); } } @@ -4503,6 +4654,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal info.setClickable(isClickable()); info.setFocusable(isFocusable()); info.setFocused(isFocused()); + info.setAccessibilityFocused(isAccessibilityFocused()); info.setSelected(isSelected()); info.setLongClickable(isLongClickable()); @@ -4597,10 +4749,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * true for views that do not have textual representation (For example, * ImageButton). * - * @return The content descriptiopn. + * @return The content description. * * @attr ref android.R.styleable#View_contentDescription */ + @ViewDebug.ExportedProperty(category = "accessibility") public CharSequence getContentDescription() { return mContentDescription; } @@ -5650,8 +5803,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * Adds any focusable views that are descendants of this view (possibly * including this view if it is focusable itself) to views. This method * adds all focusable views regardless if we are in touch mode or - * only views focusable in touch mode if we are in touch mode depending on - * the focusable mode paramater. + * only views focusable in touch mode if we are in touch mode or + * only views that can take accessibility focus if accessibility is enabeld + * depending on the focusable mode paramater. * * @param views Focusable views found so far or null if all we are interested is * the number of focusables. @@ -5660,19 +5814,32 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * * @see #FOCUSABLES_ALL * @see #FOCUSABLES_TOUCH_MODE + * @see #FOCUSABLES_ACCESSIBILITY */ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { - if (!isFocusable()) { + if (views == null) { return; } - - if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && - isInTouchMode() && !isFocusableInTouchMode()) { - return; + if ((focusableMode & FOCUSABLE_IN_TOUCH_MODE) == FOCUSABLE_IN_TOUCH_MODE) { + if (isFocusable() && (!isInTouchMode() || isFocusableInTouchMode())) { + views.add(this); + return; + } } - - if (views != null) { - views.add(this); + if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) { + if (AccessibilityManager.getInstance(mContext).isEnabled() + && includeForAccessibility()) { + views.add(this); + return; + } + } + if ((focusableMode & FOCUSABLES_ALL) == FOCUSABLES_ALL) { + if (isFocusable()) { + views.add(this); + return; + } + } else { + throw new IllegalArgumentException("Unknow focusable mode: " + focusableMode); } } @@ -5734,6 +5901,149 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Returns whether this View is accessibility focused. + * + * @return True if this View is accessibility focused. + */ + boolean isAccessibilityFocused() { + return (mPrivateFlags2 & ACCESSIBILITY_FOCUSED) != 0; + } + + /** + * Call this to try to give accessibility focus to this view. + * + * A view will not actually take focus if {@link AccessibilityManager#isEnabled()} + * returns false or the view is no visible or the view already has accessibility + * focus. + * + * See also {@link #focusSearch(int)}, which is what you call to say that you + * have focus, and you want your parent to look for the next one. + * + * @return Whether this view actually took accessibility focus. + * + * @hide + */ + public boolean requestAccessibilityFocus() { + if (!AccessibilityManager.getInstance(mContext).isEnabled()) { + return false; + } + if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) { + return false; + } + if ((mPrivateFlags2 & ACCESSIBILITY_FOCUSED) == 0) { + mPrivateFlags2 |= ACCESSIBILITY_FOCUSED; + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(this); + } + invalidate(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + notifyAccessibilityStateChanged(); + // Try to give input focus to this view - not a descendant. + requestFocusNoSearch(View.FOCUS_DOWN, null); + return true; + } + return false; + } + + /** + * Call this to try to clear accessibility focus of this view. + * + * See also {@link #focusSearch(int)}, which is what you call to say that you + * have focus, and you want your parent to look for the next one. + * + * @hide + */ + public void clearAccessibilityFocus() { + if ((mPrivateFlags2 & ACCESSIBILITY_FOCUSED) != 0) { + mPrivateFlags2 &= ~ACCESSIBILITY_FOCUSED; + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(null); + } + invalidate(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + notifyAccessibilityStateChanged(); + // Try to move accessibility focus to the input focus. + View rootView = getRootView(); + if (rootView != null) { + View inputFocus = rootView.findFocus(); + if (inputFocus != null) { + inputFocus.requestAccessibilityFocus(); + } + } + } + } + + /** + * Find the best view to take accessibility focus from a hover. + * This function finds the deepest actionable view and if that + * fails ask the parent to take accessibility focus from hover. + * + * @param x The X hovered location in this view coorditantes. + * @param y The Y hovered location in this view coorditantes. + * @return Whether the request was handled. + * + * @hide + */ + public boolean requestAccessibilityFocusFromHover(float x, float y) { + if (onRequestAccessibilityFocusFromHover(x, y)) { + return true; + } + ViewParent parent = mParent; + if (parent instanceof View) { + View parentView = (View) parent; + + float[] position = mAttachInfo.mTmpTransformLocation; + position[0] = x; + position[1] = y; + + // Compensate for the transformation of the current matrix. + if (!hasIdentityMatrix()) { + getMatrix().mapPoints(position); + } + + // Compensate for the parent scroll and the offset + // of this view stop from the parent top. + position[0] += mLeft - parentView.mScrollX; + position[1] += mTop - parentView.mScrollY; + + return parentView.requestAccessibilityFocusFromHover(position[0], position[1]); + } + return false; + } + + /** + * Requests to give this View focus from hover. + * + * @param x The X hovered location in this view coorditantes. + * @param y The Y hovered location in this view coorditantes. + * @return Whether the request was handled. + * + * @hide + */ + public boolean onRequestAccessibilityFocusFromHover(float x, float y) { + if (includeForAccessibility() + && (isActionableForAccessibility() || hasListenersForAccessibility())) { + return requestAccessibilityFocus(); + } + return false; + } + + /** + * Clears accessibility focus without calling any callback methods + * normally invoked in {@link #clearAccessibilityFocus()}. This method + * is used for clearing accessibility focus when giving this focus to + * another view. + */ + void clearAccessibilityFocusNoCallbacks() { + if ((mPrivateFlags2 & ACCESSIBILITY_FOCUSED) != 0) { + mPrivateFlags2 &= ~ACCESSIBILITY_FOCUSED; + invalidate(); + } + } + + /** * Call this to try to give focus to a specific view or to one of its * descendants. * @@ -5753,7 +6063,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal return requestFocus(View.FOCUS_DOWN); } - /** * Call this to try to give focus to a specific view or to one of its * descendants and give it a hint about what direction focus is heading. @@ -5805,6 +6114,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @return Whether this view or one of its descendants actually took focus. */ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + return requestFocusNoSearch(direction, previouslyFocusedRect); + } + + private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { @@ -5864,6 +6177,248 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Gets the mode for determining whether this View is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + * + * @return The mode for determining whether a View is important for accessibility. + * + * @attr ref android.R.styleable#View_importantForAccessibility + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + @ViewDebug.ExportedProperty(category = "accessibility", mapping = { + @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_AUTO, + to = "IMPORTANT_FOR_ACCESSIBILITY_AUTO"), + @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_YES, + to = "IMPORTANT_FOR_ACCESSIBILITY_YES"), + @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_NO, + to = "IMPORTANT_FOR_ACCESSIBILITY_NO") + }) + public int getImportantForAccessibility() { + return (mPrivateFlags2 & IMPORTANT_FOR_ACCESSIBILITY_MASK) + >> IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + } + + /** + * Sets how to determine whether this view is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + * + * @param mode How to determine whether this view is important for accessibility. + * + * @attr ref android.R.styleable#View_importantForAccessibility + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + public void setImportantForAccessibility(int mode) { + if (mode != getImportantForAccessibility()) { + mPrivateFlags2 &= ~IMPORTANT_FOR_ACCESSIBILITY_MASK; + mPrivateFlags2 |= (mode << IMPORTANT_FOR_ACCESSIBILITY_SHIFT) + & IMPORTANT_FOR_ACCESSIBILITY_MASK; + notifyAccessibilityStateChanged(); + } + } + + /** + * Gets whether this view should be exposed for accessibility. + * + * @return Whether the view is exposed for accessibility. + * + * @hide + */ + public boolean isImportantForAccessibility() { + final int mode = (mPrivateFlags2 & IMPORTANT_FOR_ACCESSIBILITY_MASK) + >> IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + switch (mode) { + case IMPORTANT_FOR_ACCESSIBILITY_YES: + return true; + case IMPORTANT_FOR_ACCESSIBILITY_NO: + return false; + case IMPORTANT_FOR_ACCESSIBILITY_AUTO: + return isActionableForAccessibility() || hasListenersForAccessibility(); + default: + throw new IllegalArgumentException("Unknow important for accessibility mode: " + + mode); + } + } + + /** + * Gets the parent for accessibility purposes. Note that the parent for + * accessibility is not necessary the immediate parent. It is the first + * predecessor that is important for accessibility. + * + * @return The parent for accessibility purposes. + */ + public ViewParent getParentForAccessibility() { + if (mParent instanceof View) { + View parentView = (View) mParent; + if (parentView.includeForAccessibility()) { + return mParent; + } else { + return mParent.getParentForAccessibility(); + } + } + return null; + } + + /** + * Adds the children of a given View for accessibility. Since some Views are + * not important for accessibility the children for accessibility are not + * necessarily direct children of the riew, rather they are the first level of + * descendants important for accessibility. + * + * @param children The list of children for accessibility. + */ + public void addChildrenForAccessibility(ArrayList<View> children) { + if (includeForAccessibility()) { + children.add(this); + } + } + + /** + * Whether to regard this view for accessibility. A view is regarded for + * accessibility if it is important for accessibility or the querying + * accessibility service has explicitly requested that view not + * important for accessibility are regarded. + * + * @return Whether to regard the view for accessibility. + */ + boolean includeForAccessibility() { + if (mAttachInfo != null) { + if (!mAttachInfo.mIncludeNotImportantViews) { + return isImportantForAccessibility(); + } else { + return true; + } + } + return false; + } + + /** + * Returns whether the View is considered actionable from + * accessibility perspective. Such view are important for + * accessiiblity. + * + * @return True if the view is actionable for accessibility. + */ + private boolean isActionableForAccessibility() { + return (isClickable() || isLongClickable() || isFocusable()); + } + + /** + * Returns whether the View has registered callbacks wich makes it + * important for accessiiblity. + * + * @return True if the view is actionable for accessibility. + */ + private boolean hasListenersForAccessibility() { + ListenerInfo info = getListenerInfo(); + return mTouchDelegate != null || info.mOnKeyListener != null + || info.mOnTouchListener != null || info.mOnGenericMotionListener != null + || info.mOnHoverListener != null || info.mOnDragListener != null; + } + + /** + * Notifies accessibility services that some view's important for + * accessibility state has changed. Note that such notifications + * are made at most once every + * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()} + * to avoid unnecessary load to the system. Also once a view has + * made a notifucation this method is a NOP until the notification has + * been sent to clients. + * + * @hide + * + * TODO: Makse sure this method is called for any view state change + * that is interesting for accessilility purposes. + */ + public void notifyAccessibilityStateChanged() { + if ((mPrivateFlags2 & ACCESSIBILITY_STATE_CHANGED) == 0) { + mPrivateFlags2 |= ACCESSIBILITY_STATE_CHANGED; + if (mParent != null) { + mParent.childAccessibilityStateChanged(this); + } + } + } + + /** + * Reset the state indicating the this view has requested clients + * interested in its accessiblity state to be notified. + * + * @hide + */ + public void resetAccessibilityStateChanged() { + mPrivateFlags2 &= ~ACCESSIBILITY_STATE_CHANGED; + } + + /** + * Performs the specified accessibility action on the view. For + * possible accessibility actions look at {@link AccessibilityNodeInfo}. + * + * @param action The action to perform. + * @return Whether the action was performed. + */ + public boolean performAccessibilityAction(int action) { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: { + final long now = SystemClock.uptimeMillis(); + // Send down. + MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, + getWidth() / 2, getHeight() / 2, 0); + onTouchEvent(event); + // Send up. + event.setAction(MotionEvent.ACTION_UP); + onTouchEvent(event); + // Clean up. + event.recycle(); + } break; + case AccessibilityNodeInfo.ACTION_FOCUS: { + if (!hasFocus()) { + // Get out of touch mode since accessibility + // wants to move focus around. + getViewRootImpl().ensureTouchMode(false); + return requestFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + if (hasFocus()) { + clearFocus(); + return !isFocused(); + } + } break; + case AccessibilityNodeInfo.ACTION_SELECT: { + if (!isSelected()) { + setSelected(true); + return isSelected(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { + if (isSelected()) { + setSelected(false); + return !isSelected(); + } + } break; + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { + if (!isAccessibilityFocused()) { + return requestAccessibilityFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { + if (isAccessibilityFocused()) { + clearAccessibilityFocus(); + return true; + } + } break; + } + return false; + } + + /** * @hide */ public void dispatchStartTemporaryDetach() { @@ -6757,21 +7312,27 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal // The root view may receive hover (or touch) events that are outside the bounds of // the window. This code ensures that we only send accessibility events for // hovers that are actually within the bounds of the root view. - final int action = event.getAction(); + final int action = event.getActionMasked(); if (!mSendingHoverAccessibilityEvents) { if ((action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_MOVE) && !hasHoveredChild() && pointInView(event.getX(), event.getY())) { - mSendingHoverAccessibilityEvents = true; sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mSendingHoverAccessibilityEvents = true; + requestAccessibilityFocusFromHover((int) event.getX(), (int) event.getY()); } } else { if (action == MotionEvent.ACTION_HOVER_EXIT - || (action == MotionEvent.ACTION_HOVER_MOVE + || (action == MotionEvent.ACTION_MOVE && !pointInView(event.getX(), event.getY()))) { mSendingHoverAccessibilityEvents = false; sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + // If the window does not have input focus we take away accessibility + // focus as soon as the user stop hovering over the view. + if (!mAttachInfo.mHasWindowFocus) { + getViewRootImpl().setAccessibilityFocusedHost(null); + } } } @@ -6795,6 +7356,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal dispatchGenericMotionEventInternal(event); return true; } + return false; } @@ -6806,7 +7368,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ private boolean isHoverable() { final int viewFlags = mViewFlags; - //noinspection SimplifiableIfStatement if ((viewFlags & ENABLED_MASK) == DISABLED) { return false; } @@ -7130,6 +7691,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ if (mParent != null) mParent.focusableViewAvailable(this); } + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } if ((flags & VISIBILITY_MASK) == VISIBLE) { @@ -7161,6 +7725,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if (((mViewFlags & VISIBILITY_MASK) == GONE)) { if (hasFocus()) clearFocus(); + clearAccessibilityFocus(); destroyDrawingCache(); if (mParent instanceof View) { // GONE views noop invalidation, so invalidate the parent @@ -7185,9 +7750,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal mPrivateFlags |= DRAWN; if (((mViewFlags & VISIBILITY_MASK) == INVISIBLE) && hasFocus()) { - // root view becoming invisible shouldn't clear focus + // root view becoming invisible shouldn't clear focus and accessibility focus if (getRootView() != this) { clearFocus(); + clearAccessibilityFocus(); } } if (mAttachInfo != null) { @@ -7241,6 +7807,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal mParent.recomputeViewAttributes(this); } } + + if (AccessibilityManager.getInstance(mContext).isEnabled() + && ((changed & FOCUSABLE) != 0 || (changed & CLICKABLE) != 0 + || (changed & LONG_CLICKABLE) != 0 || (changed & ENABLED) != 0)) { + notifyAccessibilityStateChanged(); + } } /** @@ -7319,6 +7891,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @param canvas the canvas on which to draw the view */ protected void dispatchDraw(Canvas canvas) { + } /** @@ -10227,6 +10800,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal resolvePadding(); resolveTextDirection(); resolveTextAlignment(); + clearAccessibilityFocus(); if (isFocused()) { InputMethodManager imm = InputMethodManager.peekInstance(); imm.focusIn(this); @@ -10457,6 +11031,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal resetResolvedLayoutDirection(); resetResolvedTextAlignment(); + resetAccessibilityStateChanged(); + clearAccessibilityFocus(); } /** @@ -13273,6 +13849,29 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * @hide + */ + public Insets getLayoutInsets() { + if (mLayoutInsets == null) { + if (mBackground == null) { + mLayoutInsets = Insets.NONE; + } else { + Rect insetRect = new Rect(); + boolean hasInsets = mBackground.getLayoutInsets(insetRect); + mLayoutInsets = hasInsets ? Insets.of(insetRect) : Insets.NONE; + } + } + return mLayoutInsets; + } + + /** + * @hide + */ + public void setLayoutInsets(Insets layoutInsets) { + mLayoutInsets = layoutInsets; + } + + /** * Changes the selection state of this view. A view can be selected or not. * Note that selection is not the same as focus. Views are typically * selected in the context of an AdapterView like ListView or GridView; @@ -13287,6 +13886,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal invalidate(true); refreshDrawableState(); dispatchSetSelected(selected); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } @@ -13456,7 +14058,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal position[1] += view.mTop; viewParent = view.mParent; - } + } if (viewParent instanceof ViewRootImpl) { // *cough* @@ -16291,7 +16893,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal /** * Temporary list for use in collecting focusable descendents of a view. */ - final ArrayList<View> mFocusablesTempList = new ArrayList<View>(24); + final ArrayList<View> mTempArrayList = new ArrayList<View>(24); /** * The id of the window for accessibility purposes. @@ -16299,6 +16901,17 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal int mAccessibilityWindowId = View.NO_ID; /** + * Whether to ingore not exposed for accessibility Views when + * reporting the view tree to accessibility services. + */ + boolean mIncludeNotImportantViews; + + /** + * The drawable for highlighting accessibility focus. + */ + Drawable mAccessibilityFocusDrawable; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 9d06145..9134966 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -197,7 +197,7 @@ public class ViewConfiguration { * gesture and the touch up event of a subsequent tap for the latter tap to be * considered as a tap i.e. to perform a click. */ - private static final int TOUCH_EXPLORATION_TAP_SLOP = 80; + private static final int TOUCH_EXPLORE_TAP_SLOP = 80; /** * Delay before dispatching a recurring accessibility event in milliseconds. @@ -238,7 +238,7 @@ public class ViewConfiguration { private final int mDoubleTapTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; - private final int mScaledTouchExplorationTapSlop; + private final int mScaledTouchExploreTapSlop; private final int mWindowTouchSlop; private final int mMaximumDrawingCacheSize; private final int mOverscrollDistance; @@ -265,7 +265,7 @@ public class ViewConfiguration { mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; - mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP; + mScaledTouchExploreTapSlop = TOUCH_EXPLORE_TAP_SLOP; mWindowTouchSlop = WINDOW_TOUCH_SLOP; //noinspection deprecation mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; @@ -302,7 +302,7 @@ public class ViewConfiguration { mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f); - mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f); + mScaledTouchExploreTapSlop = (int) (density * TOUCH_EXPLORE_TAP_SLOP + 0.5f); mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f); final Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); @@ -559,8 +559,8 @@ public class ViewConfiguration { * * @hide */ - public int getScaledTouchExplorationTapSlop() { - return mScaledTouchExplorationTapSlop; + public int getScaledTouchExploreTapSlop() { + return mScaledTouchExploreTapSlop; } /** diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 8f6badf..cb37a1c 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -141,6 +141,18 @@ public class ViewDebug { public static final String DEBUG_LATENCY_TAG = "ViewLatency"; /** + * Enables detailed logging of accessibility focus operations. + * @hide + */ + public static final boolean DEBUG_ACCESSIBILITY_FOCUS = false; + + /** + * Tag for logging of accessibility focus operations + * @hide + */ + public static final String DEBUG_ACCESSIBILITY_FOCUS_TAG = "AccessibilityFocus"; + + /** * <p>Enables or disables views consistency check. Even when this property is enabled, * view consistency checks happen only if {@link false} is set * to true. The value of this property can be configured externally in one of the diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 121b544..5e02b49 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -45,6 +45,7 @@ import com.android.internal.R; import com.android.internal.util.Predicate; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; /** @@ -169,6 +170,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ protected int mGroupFlags; + /* + * THe layout mode: either {@link #UNDEFINED_LAYOUT_MODE}, {@link #COMPONENT_BOUNDS} or + * {@link #LAYOUT_BOUNDS} + */ + private int mLayoutMode = UNDEFINED_LAYOUT_MODE; + /** * NOTE: If you change the flags below make sure to reflect the changes * the DisplayList class @@ -334,6 +341,22 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ public static final int PERSISTENT_ALL_CACHES = 0x3; + // Layout Modes + + private static final int UNDEFINED_LAYOUT_MODE = -1; + + /** + * This constant is a {@link #setLayoutMode(int) layoutMode}. + * Component bounds are the raw values of {@link #getLeft() left}, {@link #getTop() top}, + * {@link #getRight() right} and {@link #getBottom() bottom}. + */ + public static final int COMPONENT_BOUNDS = 0; + + /** + * This constant is a {@link #setLayoutMode(int) layoutMode}. + */ + public static final int LAYOUT_BOUNDS = 1; + /** * We clip to padding when FLAG_CLIP_TO_PADDING and FLAG_PADDING_NOT_NULL * are set at the same time. @@ -611,13 +634,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} */ + @Override public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { - ViewParent parent = getParent(); + ViewParent parent = mParent; if (parent == null) { return false; } final boolean propagate = onRequestSendAccessibilityEvent(child, event); - //noinspection SimplifiableIfStatement if (!propagate) { return false; } @@ -1552,6 +1575,33 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return mFirstHoverTarget != null; } + @Override + public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { + View[] children = mChildren; + final int childrenCount = mChildrenCount; + for (int i = 0; i < childrenCount; i++) { + View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE + && (child.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { + if (child.includeForAccessibility()) { + childrenForAccessibility.add(child); + } else { + child.addChildrenForAccessibility(childrenForAccessibility); + } + } + } + } + + /** + * @hide + */ + @Override + public void childAccessibilityStateChanged(View child) { + if (mParent != null) { + mParent.childAccessibilityStateChanged(child); + } + } + /** * Implement this method to intercept hover events before they are handled * by child views. @@ -2294,33 +2344,43 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { - boolean handled = super.dispatchPopulateAccessibilityEventInternal(event); - if (handled) { - return handled; + boolean handled = false; + if (includeForAccessibility()) { + handled = super.dispatchPopulateAccessibilityEventInternal(event); + if (handled) { + return handled; + } } // Let our children have a shot in populating the event. - for (int i = 0, count = getChildCount(); i < count; i++) { - View child = getChildAt(i); + ChildListForAccessibility children = ChildListForAccessibility.obtain(this, true); + final int childCount = children.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = children.getChildAt(i); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { - handled = getChildAt(i).dispatchPopulateAccessibilityEvent(event); + handled = child.dispatchPopulateAccessibilityEvent(event); if (handled) { + children.recycle(); return handled; } } } + children.recycle(); return false; } @Override void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); - info.setClassName(ViewGroup.class.getName()); - for (int i = 0, count = mChildrenCount; i < count; i++) { - View child = mChildren[i]; - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE - && (child.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { + if (mAttachInfo != null) { + ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList; + childrenForAccessibility.clear(); + addChildrenForAccessibility(childrenForAccessibility); + final int childrenForAccessibilityCount = childrenForAccessibility.size(); + for (int i = 0; i < childrenForAccessibilityCount; i++) { + View child = childrenForAccessibility.get(i); info.addChild(child); } + childrenForAccessibility.clear(); } } @@ -2331,6 +2391,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * @hide + */ + @Override + public void resetAccessibilityStateChanged() { + super.resetAccessibilityStateChanged(); + View[] children = mChildren; + final int childCount = mChildrenCount; + for (int i = 0; i < childCount; i++) { + View child = children[i]; + child.resetAccessibilityStateChanged(); + } + } + + /** * {@inheritDoc} */ @Override @@ -3400,6 +3474,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearChildFocus(view); ensureInputFocusOnFirstFocusable(); } + + if (view.isAccessibilityFocused()) { + view.clearAccessibilityFocus(); + } } /** @@ -4368,6 +4446,52 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns the basis of alignment during the layout of this view group: + * either {@link #COMPONENT_BOUNDS} or {@link #LAYOUT_BOUNDS}. + * + * @return whether or not this view group should use the component or layout bounds during + * layout operations + * + * @see #setLayoutMode(int) + */ + public int getLayoutMode() { + if (mLayoutMode == UNDEFINED_LAYOUT_MODE) { + ViewParent parent = getParent(); + if (parent instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) parent; + return viewGroup.getLayoutMode(); + } else { + int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion; + boolean preJellyBean = targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN; + return preJellyBean ? COMPONENT_BOUNDS : LAYOUT_BOUNDS; + } + + } + return mLayoutMode; + } + + /** + * Sets the basis of alignment during alignment of this view group. + * Valid values are either {@link #COMPONENT_BOUNDS} or {@link #LAYOUT_BOUNDS}. + * <p> + * The default is to query the property of the parent if this view group has a parent. + * If this ViewGroup is the root of the view hierarchy the default + * value is {@link #LAYOUT_BOUNDS} for target SDK's greater than JellyBean, + * {@link #LAYOUT_BOUNDS} otherwise. + * + * @return whether or not this view group should use the component or layout bounds during + * layout operations + * + * @see #getLayoutMode() + */ + public void setLayoutMode(int layoutMode) { + if (mLayoutMode != layoutMode) { + mLayoutMode = layoutMode; + requestLayout(); + } + } + + /** * Returns a new set of layout parameters based on the supplied attributes set. * * @param attrs the attributes to build the layout parameters from @@ -5622,4 +5746,218 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } } + + /** + * Pooled class that orderes the children of a ViewGroup from start + * to end based on how they are laid out and the layout direction. + */ + static class ChildListForAccessibility { + + private static final int MAX_POOL_SIZE = 32; + + private static final Object sPoolLock = new Object(); + + private static ChildListForAccessibility sPool; + + private static int sPoolSize; + + private boolean mIsPooled; + + private ChildListForAccessibility mNext; + + private final ArrayList<View> mChildren = new ArrayList<View>(); + + private final ArrayList<ViewLocationHolder> mHolders = new ArrayList<ViewLocationHolder>(); + + public static ChildListForAccessibility obtain(ViewGroup parent, boolean sort) { + ChildListForAccessibility list = null; + synchronized (sPoolLock) { + if (sPool != null) { + list = sPool; + sPool = list.mNext; + list.mNext = null; + list.mIsPooled = false; + sPoolSize--; + } else { + list = new ChildListForAccessibility(); + } + list.init(parent, sort); + return list; + } + } + + public void recycle() { + if (mIsPooled) { + throw new IllegalStateException("Instance already recycled."); + } + clear(); + if (sPoolSize < MAX_POOL_SIZE) { + mNext = sPool; + mIsPooled = true; + sPool = this; + sPoolSize++; + } + } + + public int getChildCount() { + return mChildren.size(); + } + + public View getChildAt(int index) { + return mChildren.get(index); + } + + public int getChildIndex(View child) { + return mChildren.indexOf(child); + } + + private void init(ViewGroup parent, boolean sort) { + ArrayList<View> children = mChildren; + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = parent.getChildAt(i); + children.add(child); + } + if (sort) { + ArrayList<ViewLocationHolder> holders = mHolders; + for (int i = 0; i < childCount; i++) { + View child = children.get(i); + ViewLocationHolder holder = ViewLocationHolder.obtain(parent, child); + holders.add(holder); + } + Collections.sort(holders); + for (int i = 0; i < childCount; i++) { + ViewLocationHolder holder = holders.get(i); + children.set(i, holder.mView); + holder.recycle(); + } + holders.clear(); + } + } + + private void clear() { + mChildren.clear(); + } + } + + /** + * Pooled class that holds a View and its location with respect to + * a specified root. This enables sorting of views based on their + * coordinates without recomputing the position relative to the root + * on every comparison. + */ + static class ViewLocationHolder implements Comparable<ViewLocationHolder> { + + private static final int MAX_POOL_SIZE = 32; + + private static final Object sPoolLock = new Object(); + + private static ViewLocationHolder sPool; + + private static int sPoolSize; + + private boolean mIsPooled; + + private ViewLocationHolder mNext; + + private final Rect mLocation = new Rect(); + + public View mView; + + private int mLayoutDirection; + + public static ViewLocationHolder obtain(ViewGroup root, View view) { + ViewLocationHolder holder = null; + synchronized (sPoolLock) { + if (sPool != null) { + holder = sPool; + sPool = holder.mNext; + holder.mNext = null; + holder.mIsPooled = false; + sPoolSize--; + } else { + holder = new ViewLocationHolder(); + } + holder.init(root, view); + return holder; + } + } + + public void recycle() { + if (mIsPooled) { + throw new IllegalStateException("Instance already recycled."); + } + clear(); + if (sPoolSize < MAX_POOL_SIZE) { + mNext = sPool; + mIsPooled = true; + sPool = this; + sPoolSize++; + } + } + + @Override + public int compareTo(ViewLocationHolder another) { + // This instance is greater than an invalid argument. + if (another == null) { + return 1; + } + if (getClass() != another.getClass()) { + return 1; + } + // First is above second. + if (mLocation.bottom - another.mLocation.top <= 0) { + return -1; + } + // First is below second. + if (mLocation.top - another.mLocation.bottom >= 0) { + return 1; + } + // LTR + if (mLayoutDirection == LAYOUT_DIRECTION_LTR) { + final int leftDifference = mLocation.left - another.mLocation.left; + // First more to the left than second. + if (leftDifference != 0) { + return leftDifference; + } + } else { // RTL + final int rightDifference = mLocation.right - another.mLocation.right; + // First more to the right than second. + if (rightDifference != 0) { + return -rightDifference; + } + } + // Break tie by top. + final int topDiference = mLocation.top - another.mLocation.top; + if (topDiference != 0) { + return topDiference; + } + // Break tie by height. + final int heightDiference = mLocation.height() - another.mLocation.height(); + if (heightDiference != 0) { + return -heightDiference; + } + // Break tie by width. + final int widthDiference = mLocation.width() - another.mLocation.width(); + if (widthDiference != 0) { + return -widthDiference; + } + // Return nondeterministically one of them since we do + // not want to ignore any views. + return 1; + } + + private void init(ViewGroup root, View view) { + Rect viewLocation = mLocation; + view.getDrawingRect(viewLocation); + root.offsetDescendantRectToMyCoords(view, viewLocation); + mView = view; + mLayoutDirection = root.getResolvedLayoutDirection(); + } + + private void clear() { + mView = null; + mLocation.set(0, 0, 0, 0); + } + } } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 75e9151..ddff91d 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -277,4 +277,22 @@ public interface ViewParent { * View.fitSystemWindows(Rect)} be performed. */ public void requestFitSystemWindows(); + + /** + * Gets the parent of a given View for accessibility. Since some Views are not + * exposed to the accessibility layer the parent for accessibility is not + * necessarily the direct parent of the View, rather it is a predecessor. + * + * @return The parent or <code>null</code> if no such is found. + */ + public ViewParent getParentForAccessibility(); + + /** + * A child notifies its parent that its state for accessibility has changed. + * That is some of the child properties reported to accessibility services has + * changed, hence the interested services have to be notified for the new state. + * + * @hide + */ + public void childAccessibilityStateChanged(View child); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 2e3ff38..b4554d5 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -37,6 +37,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; +import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Binder; import android.os.Bundle; @@ -56,17 +57,11 @@ import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; -import android.util.Pool; -import android.util.Poolable; -import android.util.PoolableManager; -import android.util.Pools; import android.util.Slog; -import android.util.SparseLongArray; import android.util.TypedValue; import android.view.View.AttachInfo; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityNodeInfo; @@ -79,6 +74,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; +import com.android.internal.R; import com.android.internal.policy.PolicyManager; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.IInputMethodCallback; @@ -89,9 +85,7 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.HashSet; /** * The top of a view hierarchy, implementing the needed protocol between View @@ -181,6 +175,10 @@ public final class ViewRootImpl implements ViewParent, View mFocusedView; View mRealFocusedView; // this is not set to null in touch mode View mOldFocusedView; + + View mAccessibilityFocusedHost; + AccessibilityNodeInfo mAccessibilityFocusedVirtualView; + int mViewVisibility; boolean mAppVisible = true; int mOrigWindowType = -1; @@ -321,7 +319,7 @@ public final class ViewRootImpl implements ViewParent, SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent; - AccessibilityNodePrefetcher mAccessibilityNodePrefetcher; + HashSet<View> mTempHashSet; private final int mDensity; @@ -630,6 +628,10 @@ public final class ViewRootImpl implements ViewParent, if (mAccessibilityManager.isEnabled()) { mAccessibilityInteractionConnectionManager.ensureConnection(); } + + if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } } } } @@ -1418,6 +1420,8 @@ public final class ViewRootImpl implements ViewParent, mView.draw(layerCanvas); + drawAccessibilityFocusedDrawableIfNeeded(layerCanvas); + mResizeBufferStartTime = SystemClock.uptimeMillis(); mResizeBufferDuration = mView.getResources().getInteger( com.android.internal.R.integer.config_mediumAnimTime); @@ -1712,7 +1716,7 @@ public final class ViewRootImpl implements ViewParent, attachInfo.mTreeObserver.dispatchOnGlobalLayout(); if (AccessibilityManager.getInstance(host.mContext).isEnabled()) { - postSendWindowContentChangedCallback(); + postSendWindowContentChangedCallback(mView); } } @@ -1880,6 +1884,7 @@ public final class ViewRootImpl implements ViewParent, mResizePaint.setAlpha(mResizeAlpha); canvas.drawHardwareLayer(mResizeBuffer, 0.0f, mHardwareYOffset, mResizePaint); } + drawAccessibilityFocusedDrawableIfNeeded(canvas); } /** @@ -2234,6 +2239,8 @@ public final class ViewRootImpl implements ViewParent, mView.draw(canvas); + drawAccessibilityFocusedDrawableIfNeeded(canvas); + if (ViewDebug.DEBUG_LATENCY) { long now = System.nanoTime(); Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- draw() took " @@ -2274,6 +2281,64 @@ public final class ViewRootImpl implements ViewParent, return true; } + /** + * We want to draw a highlight around the current accessibility focused. + * Since adding a style for all possible view is not a viable option we + * have this specialized drawing method. + * + * Note: We are doing this here to be able to draw the highlight for + * virtual views in addition to real ones. + * + * @param canvas The canvas on which to draw. + */ + private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) { + if (!AccessibilityManager.getInstance(mView.mContext).isEnabled()) { + return; + } + if (mAccessibilityFocusedHost == null || mAccessibilityFocusedHost.mAttachInfo == null) { + return; + } + Drawable drawable = getAccessibilityFocusedDrawable(); + if (drawable == null) { + return; + } + AccessibilityNodeProvider provider = + mAccessibilityFocusedHost.getAccessibilityNodeProvider(); + Rect bounds = mView.mAttachInfo.mTmpInvalRect; + if (provider == null) { + mAccessibilityFocusedHost.getDrawingRect(bounds); + if (mView instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) mView; + viewGroup.offsetDescendantRectToMyCoords(mAccessibilityFocusedHost, bounds); + } + } else { + if (mAccessibilityFocusedVirtualView == null) { + mAccessibilityFocusedVirtualView = provider.findAccessibilitiyFocus(View.NO_ID); + } + mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds); + bounds.offset(-mAttachInfo.mWindowLeft, -mAttachInfo.mWindowTop); + } + drawable.setBounds(bounds); + drawable.draw(canvas); + } + + private Drawable getAccessibilityFocusedDrawable() { + if (mAttachInfo != null) { + // Lazily load the accessibility focus drawable. + if (mAttachInfo.mAccessibilityFocusDrawable == null) { + TypedValue value = new TypedValue(); + final boolean resolved = mView.mContext.getTheme().resolveAttribute( + R.attr.accessibilityFocusedDrawable, value, true); + if (resolved) { + mAttachInfo.mAccessibilityFocusDrawable = + mView.mContext.getResources().getDrawable(value.resourceId); + } + } + return mAttachInfo.mAccessibilityFocusDrawable; + } + return null; + } + void invalidateDisplayLists() { final ArrayList<DisplayList> displayLists = mDisplayLists; final int count = displayLists.size(); @@ -2407,6 +2472,14 @@ public final class ViewRootImpl implements ViewParent, return handled; } + void setAccessibilityFocusedHost(View host) { + if (mAccessibilityFocusedHost != null && mAccessibilityFocusedVirtualView == null) { + mAccessibilityFocusedHost.clearAccessibilityFocusNoCallbacks(); + } + mAccessibilityFocusedHost = host; + mAccessibilityFocusedVirtualView = null; + } + public void requestChildFocus(View child, View focused) { checkThread(); @@ -2437,9 +2510,13 @@ public final class ViewRootImpl implements ViewParent, mFocusedView = mRealFocusedView = null; } + @Override + public ViewParent getParentForAccessibility() { + return null; + } + public void focusableViewAvailable(View v) { checkThread(); - if (mView != null) { if (!mView.hasFocus()) { v.requestFocus(); @@ -2547,7 +2624,7 @@ public final class ViewRootImpl implements ViewParent, /** * Return true if child is an ancestor of parent, (or equal to the parent). */ - private static boolean isViewDescendantOf(View child, View parent) { + static boolean isViewDescendantOf(View child, View parent) { if (child == parent) { return true; } @@ -2585,13 +2662,9 @@ public final class ViewRootImpl implements ViewParent, private final static int MSG_DISPATCH_DRAG_LOCATION_EVENT = 16; private final static int MSG_DISPATCH_SYSTEM_UI_VISIBILITY = 17; private final static int MSG_UPDATE_CONFIGURATION = 18; - private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 19; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 20; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 21; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 22; - private final static int MSG_PROCESS_INPUT_EVENTS = 23; - private final static int MSG_DISPATCH_SCREEN_STATE = 24; - private final static int MSG_INVALIDATE_DISPLAY_LIST = 25; + private final static int MSG_PROCESS_INPUT_EVENTS = 19; + private final static int MSG_DISPATCH_SCREEN_STATE = 20; + private final static int MSG_INVALIDATE_DISPLAY_LIST = 21; final class ViewRootHandler extends Handler { @Override @@ -2633,14 +2706,6 @@ public final class ViewRootImpl implements ViewParent, return "MSG_DISPATCH_SYSTEM_UI_VISIBILITY"; case MSG_UPDATE_CONFIGURATION: return "MSG_UPDATE_CONFIGURATION"; - case MSG_PERFORM_ACCESSIBILITY_ACTION: - return "MSG_PERFORM_ACCESSIBILITY_ACTION"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; case MSG_PROCESS_INPUT_EVENTS: return "MSG_PROCESS_INPUT_EVENTS"; case MSG_DISPATCH_SCREEN_STATE: @@ -2770,8 +2835,28 @@ public final class ViewRootImpl implements ViewParent, mHasHadWindowFocus = true; } - if (hasWindowFocus && mView != null && mAccessibilityManager.isEnabled()) { - mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + if (mView != null && mAccessibilityManager.isEnabled()) { + if (hasWindowFocus) { + mView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + // Give accessibility focus to the view that has input + // focus if such, otherwise to the first one. + if (mView instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) mView; + View focused = viewGroup.findFocus(); + if (focused != null) { + focused.requestAccessibilityFocus(); + } + } + // There is no accessibility focus, despite our effort + // above, now just give it to the first view. + if (mAccessibilityFocusedHost == null) { + mView.requestAccessibilityFocus(); + } + } else { + // Clear accessibility focus when the window loses input focus. + setAccessibilityFocusedHost(null); + } } } } break; @@ -2828,30 +2913,6 @@ public final class ViewRootImpl implements ViewParent, } updateConfiguration(config, false); } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: { - if (mView != null) { - getAccessibilityInteractionController() - .findAccessibilityNodeInfoByAccessibilityIdUiThread(msg); - } - } break; - case MSG_PERFORM_ACCESSIBILITY_ACTION: { - if (mView != null) { - getAccessibilityInteractionController() - .perfromAccessibilityActionUiThread(msg); - } - } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { - if (mView != null) { - getAccessibilityInteractionController() - .findAccessibilityNodeInfoByViewIdUiThread(msg); - } - } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { - if (mView != null) { - getAccessibilityInteractionController() - .findAccessibilityNodeInfosByTextUiThread(msg); - } - } break; case MSG_DISPATCH_SCREEN_STATE: { if (mView != null) { handleScreenStateChange(msg.arg1 == 1); @@ -2917,28 +2978,25 @@ public final class ViewRootImpl implements ViewParent, // set yet. final View focused = mView.findFocus(); if (focused != null && !focused.isFocusableInTouchMode()) { - final ViewGroup ancestorToTakeFocus = findAncestorToTakeFocusInTouchMode(focused); if (ancestorToTakeFocus != null) { // there is an ancestor that wants focus after its descendants that // is focusable in touch mode.. give it focus return ancestorToTakeFocus.requestFocus(); - } else { - // nothing appropriate to have focus in touch mode, clear it out - mView.unFocus(); - mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); - mFocusedView = null; - mOldFocusedView = null; - return true; } } + // nothing appropriate to have focus in touch mode, clear it out + mView.unFocus(); + mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); + mFocusedView = null; + mOldFocusedView = null; + return true; } } return false; } - /** * Find an ancestor of focused that wants focus after its descendants and is * focusable in touch mode. @@ -2964,25 +3022,45 @@ public final class ViewRootImpl implements ViewParent, private boolean leaveTouchMode() { if (mView != null) { + boolean inputFocusValid = false; if (mView.hasFocus()) { // i learned the hard way to not trust mFocusedView :) mFocusedView = mView.findFocus(); if (!(mFocusedView instanceof ViewGroup)) { // some view has focus, let it keep it - return false; - } else if (((ViewGroup)mFocusedView).getDescendantFocusability() != + inputFocusValid = true; + } else if (((ViewGroup) mFocusedView).getDescendantFocusability() != ViewGroup.FOCUS_AFTER_DESCENDANTS) { // some view group has focus, and doesn't prefer its children // over itself for focus, so let them keep it. - return false; + inputFocusValid = true; } } - - // find the best view to give focus to in this brave new non-touch-mode - // world - final View focused = focusSearch(null, View.FOCUS_DOWN); - if (focused != null) { - return focused.requestFocus(View.FOCUS_DOWN); + // In accessibility mode we always have a view that has the + // accessibility focus and input focus follows it, i.e. we + // try to give input focus to the accessibility focused view. + if (!AccessibilityManager.getInstance(mView.mContext).isEnabled()) { + // If the current input focus is not valid, find the best view to give + // focus to in this brave new non-touch-mode world. + if (!inputFocusValid) { + final View focused = focusSearch(null, View.FOCUS_DOWN); + if (focused != null) { + return focused.requestFocus(View.FOCUS_DOWN); + } + } + } else { + // If the current input focus is not valid clear it but do not + // give it to another view since the accessibility focus is + // leading now and the input one follows. + if (!inputFocusValid) { + if (mFocusedView != null) { + mView.unFocus(); + mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, null); + mFocusedView = null; + mOldFocusedView = null; + return true; + } + } } } return false; @@ -3487,37 +3565,36 @@ public final class ViewRootImpl implements ViewParent, if (event.getAction() == KeyEvent.ACTION_DOWN) { int direction = 0; switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (event.hasNoModifiers()) { - direction = View.FOCUS_LEFT; - } - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (event.hasNoModifiers()) { - direction = View.FOCUS_RIGHT; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (event.hasNoModifiers()) { - direction = View.FOCUS_UP; - } - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (event.hasNoModifiers()) { - direction = View.FOCUS_DOWN; - } - break; - case KeyEvent.KEYCODE_TAB: - if (event.hasNoModifiers()) { - direction = View.FOCUS_FORWARD; - } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { - direction = View.FOCUS_BACKWARD; - } - break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasNoModifiers()) { + direction = View.FOCUS_LEFT; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasNoModifiers()) { + direction = View.FOCUS_RIGHT; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (event.hasNoModifiers()) { + direction = View.FOCUS_UP; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (event.hasNoModifiers()) { + direction = View.FOCUS_DOWN; + } + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + direction = View.FOCUS_FORWARD; + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + direction = View.FOCUS_BACKWARD; + } + break; } - if (direction != 0) { - View focused = mView != null ? mView.findFocus() : null; + View focused = mView.findFocus(); if (focused != null) { View v = focused.focusSearch(direction); if (v != null && v != focused) { @@ -3532,8 +3609,8 @@ public final class ViewRootImpl implements ViewParent, v, mTempRect); } if (v.requestFocus(direction, mTempRect)) { - playSoundEffect( - SoundEffectConstants.getContantForFocusDirection(direction)); + playSoundEffect(SoundEffectConstants + .getContantForFocusDirection(direction)); finishInputEvent(q, true); return; } @@ -3683,22 +3760,11 @@ public final class ViewRootImpl implements ViewParent, + " called when there is no mView"); } if (mAccessibilityInteractionController == null) { - mAccessibilityInteractionController = new AccessibilityInteractionController(); + mAccessibilityInteractionController = new AccessibilityInteractionController(this); } return mAccessibilityInteractionController; } - public AccessibilityNodePrefetcher getAccessibilityNodePrefetcher() { - if (mView == null) { - throw new IllegalStateException("getAccessibilityNodePrefetcher" - + " called when there is no mView"); - } - if (mAccessibilityNodePrefetcher == null) { - mAccessibilityNodePrefetcher = new AccessibilityNodePrefetcher(); - } - return mAccessibilityNodePrefetcher; - } - private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { @@ -4375,15 +4441,19 @@ public final class ViewRootImpl implements ViewParent, * This event is send at most once every * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}. */ - private void postSendWindowContentChangedCallback() { + private void postSendWindowContentChangedCallback(View source) { if (mSendWindowContentChangedAccessibilityEvent == null) { mSendWindowContentChangedAccessibilityEvent = new SendWindowContentChangedAccessibilityEvent(); } - if (!mSendWindowContentChangedAccessibilityEvent.mIsPending) { - mSendWindowContentChangedAccessibilityEvent.mIsPending = true; + View oldSource = mSendWindowContentChangedAccessibilityEvent.mSource; + if (oldSource == null) { + mSendWindowContentChangedAccessibilityEvent.mSource = source; mHandler.postDelayed(mSendWindowContentChangedAccessibilityEvent, ViewConfiguration.getSendRecurringAccessibilityEventsInterval()); + } else { + View newSource = getCommonPredecessor(oldSource, source); + mSendWindowContentChangedAccessibilityEvent.mSource = newSource; } } @@ -4419,6 +4489,46 @@ public final class ViewRootImpl implements ViewParent, return true; } + @Override + public void childAccessibilityStateChanged(View child) { + postSendWindowContentChangedCallback(child); + } + + private View getCommonPredecessor(View first, View second) { + if (mAttachInfo != null) { + if (mTempHashSet == null) { + mTempHashSet = new HashSet<View>(); + } + HashSet<View> seen = mTempHashSet; + seen.clear(); + View firstCurrent = first; + while (firstCurrent != null) { + seen.add(firstCurrent); + ViewParent firstCurrentParent = firstCurrent.mParent; + if (firstCurrentParent instanceof View) { + firstCurrent = (View) firstCurrentParent; + } else { + firstCurrent = null; + } + } + View secondCurrent = second; + while (secondCurrent != null) { + if (seen.contains(secondCurrent)) { + seen.clear(); + return secondCurrent; + } + ViewParent secondCurrentParent = secondCurrent.mParent; + if (secondCurrentParent instanceof View) { + secondCurrent = (View) secondCurrentParent; + } else { + secondCurrent = null; + } + } + seen.clear(); + } + return null; + } + void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( @@ -4953,6 +5063,7 @@ public final class ViewRootImpl implements ViewParent, } } else { ensureNoConnection(); + setAccessibilityFocusedHost(null); } } @@ -4991,14 +5102,15 @@ public final class ViewRootImpl implements ViewParent, mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl); } + @Override public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int prefetchFlags, int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId, - interactionId, callback, prefetchFlags, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } else { // We cannot make the call and notify the caller so it does not wait. try { @@ -5009,16 +5121,17 @@ public final class ViewRootImpl implements ViewParent, } } + @Override public void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interogatingPid, long interrogatingTid) { + int flags, int interogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .performAccessibilityActionClientThread(accessibilityNodeId, action, - interactionId, callback, interogatingPid, interrogatingTid); + interactionId, callback, flags, interogatingPid, interrogatingTid); } else { - // We cannot make the call and notify the caller so it does not + // We cannot make the call and notify the caller so it does not wait. try { callback.setPerformAccessibilityActionResult(false, interactionId); } catch (RemoteException re) { @@ -5027,16 +5140,17 @@ public final class ViewRootImpl implements ViewParent, } } + @Override public void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByViewIdClientThread(accessibilityNodeId, viewId, - interactionId, callback, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } else { - // We cannot make the call and notify the caller so it does not + // We cannot make the call and notify the caller so it does not wait. try { callback.setFindAccessibilityNodeInfoResult(null, interactionId); } catch (RemoteException re) { @@ -5045,16 +5159,17 @@ public final class ViewRootImpl implements ViewParent, } } + @Override public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text, - interactionId, callback, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } else { - // We cannot make the call and notify the caller so it does not + // We cannot make the call and notify the caller so it does not wait. try { callback.setFindAccessibilityNodeInfosResult(null, interactionId); } catch (RemoteException re) { @@ -5062,606 +5177,54 @@ public final class ViewRootImpl implements ViewParent, } } } - } - - /** - * Computes whether a view is visible on the screen. - * - * @param view The view to check. - * @return Whether the view is visible on the screen. - */ - private boolean isDisplayedOnScreen(View view) { - return (view.mAttachInfo != null - && view.mAttachInfo.mWindowVisibility == View.VISIBLE - && view.getVisibility() == View.VISIBLE - && view.getGlobalVisibleRect(mTempRect)); - } - - /** - * Class for managing accessibility interactions initiated from the system - * and targeting the view hierarchy. A *ClientThread method is to be - * called from the interaction connection this ViewAncestor gives the - * system to talk to it and a corresponding *UiThread method that is executed - * on the UI thread. - */ - final class AccessibilityInteractionController { - private static final int POOL_SIZE = 5; - - private ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = - new ArrayList<AccessibilityNodeInfo>(); - - // Reusable poolable arguments for interacting with the view hierarchy - // to fit more arguments than Message and to avoid sharing objects between - // two messages since several threads can send messages concurrently. - private final Pool<SomeArgs> mPool = Pools.synchronizedPool(Pools.finitePool( - new PoolableManager<SomeArgs>() { - public SomeArgs newInstance() { - return new SomeArgs(); - } - - public void onAcquired(SomeArgs info) { - /* do nothing */ - } - - public void onReleased(SomeArgs info) { - info.clear(); - } - }, POOL_SIZE) - ); - - public class SomeArgs implements Poolable<SomeArgs> { - private SomeArgs mNext; - private boolean mIsPooled; - - public Object arg1; - public Object arg2; - public int argi1; - public int argi2; - public int argi3; - - public SomeArgs getNextPoolable() { - return mNext; - } - - public boolean isPooled() { - return mIsPooled; - } - - public void setNextPoolable(SomeArgs args) { - mNext = args; - } - - public void setPooled(boolean isPooled) { - mIsPooled = isPooled; - } - - private void clear() { - arg1 = null; - arg2 = null; - argi1 = 0; - argi2 = 0; - argi3 = 0; - } - } - public void findAccessibilityNodeInfoByAccessibilityIdClientThread( - long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int prefetchFlags, + @Override + public void findFocus(long accessibilityNodeId, int interactionId, int focusType, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; - message.arg1 = prefetchFlags; - SomeArgs args = mPool.acquire(); - args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); - args.argi3 = interactionId; - args.arg1 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interrogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .findFocusClientThread(accessibilityNodeId, interactionId, focusType, + callback, flags, interrogatingPid, interrogatingTid); } else { - mHandler.sendMessage(message); - } - } - - public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { - final int prefetchFlags = message.arg1; - SomeArgs args = (SomeArgs) message.obj; - final int accessibilityViewId = args.argi1; - final int virtualDescendantId = args.argi2; - final int interactionId = args.argi3; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg1; - mPool.release(args); - List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; - infos.clear(); - try { - View target = null; - if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { - target = ViewRootImpl.this.mView; - } else { - target = findViewByAccessibilityId(accessibilityViewId); - } - if (target != null && isDisplayedOnScreen(target)) { - getAccessibilityNodePrefetcher().prefetchAccessibilityNodeInfos(target, - virtualDescendantId, prefetchFlags, infos); - } - } finally { + // We cannot make the call and notify the caller so it does not wait. try { - callback.setFindAccessibilityNodeInfosResult(infos, interactionId); - infos.clear(); + callback.setFindAccessibilityNodeInfoResult(null, interactionId); } catch (RemoteException re) { - /* ignore - the other side will time out */ + /* best effort - ignore */ } } } - public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, - int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + @Override + public void focusSearch(long accessibilityNodeId, int interactionId, int direction, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; - message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - SomeArgs args = mPool.acquire(); - args.argi1 = viewId; - args.argi2 = interactionId; - args.arg1 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interrogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); - } else { - mHandler.sendMessage(message); - } - } - - public void findAccessibilityNodeInfoByViewIdUiThread(Message message) { - final int accessibilityViewId = message.arg1; - SomeArgs args = (SomeArgs) message.obj; - final int viewId = args.argi1; - final int interactionId = args.argi2; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg1; - mPool.release(args); - AccessibilityNodeInfo info = null; - try { - View root = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { - root = findViewByAccessibilityId(accessibilityViewId); - } else { - root = ViewRootImpl.this.mView; - } - if (root != null) { - View target = root.findViewById(viewId); - if (target != null && isDisplayedOnScreen(target)) { - info = target.createAccessibilityNodeInfo(); - } - } - } finally { - try { - callback.setFindAccessibilityNodeInfoResult(info, interactionId); - } catch (RemoteException re) { - /* ignore - the other side will time out */ - } - } - } - - public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId, - String text, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, - long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; - SomeArgs args = mPool.acquire(); - args.arg1 = text; - args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); - args.argi3 = interactionId; - args.arg2 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interrogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); - } else { - mHandler.sendMessage(message); - } - } - - public void findAccessibilityNodeInfosByTextUiThread(Message message) { - SomeArgs args = (SomeArgs) message.obj; - final String text = (String) args.arg1; - final int accessibilityViewId = args.argi1; - final int virtualDescendantId = args.argi2; - final int interactionId = args.argi3; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg2; - mPool.release(args); - List<AccessibilityNodeInfo> infos = null; - try { - View target; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { - target = findViewByAccessibilityId(accessibilityViewId); - } else { - target = ViewRootImpl.this.mView; - } - if (target != null && isDisplayedOnScreen(target)) { - AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); - if (provider != null) { - infos = provider.findAccessibilityNodeInfosByText(text, - virtualDescendantId); - } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { - ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; - foundViews.clear(); - target.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT - | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION - | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS); - if (!foundViews.isEmpty()) { - infos = mTempAccessibilityNodeInfoList; - infos.clear(); - final int viewCount = foundViews.size(); - for (int i = 0; i < viewCount; i++) { - View foundView = foundViews.get(i); - if (isDisplayedOnScreen(foundView)) { - provider = foundView.getAccessibilityNodeProvider(); - if (provider != null) { - List<AccessibilityNodeInfo> infosFromProvider = - provider.findAccessibilityNodeInfosByText(text, - virtualDescendantId); - if (infosFromProvider != null) { - infos.addAll(infosFromProvider); - } - } else { - infos.add(foundView.createAccessibilityNodeInfo()); - } - } - } - } - } - } - } finally { - try { - callback.setFindAccessibilityNodeInfosResult(infos, interactionId); - } catch (RemoteException re) { - /* ignore - the other side will time out */ - } - } - } - - public void performAccessibilityActionClientThread(long accessibilityNodeId, int action, - int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interogatingPid, long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_PERFORM_ACCESSIBILITY_ACTION; - message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - message.arg2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); - SomeArgs args = mPool.acquire(); - args.argi1 = action; - args.argi2 = interactionId; - args.arg1 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .focusSearchClientThread(accessibilityNodeId, interactionId, direction, + callback, flags, interrogatingPid, interrogatingTid); } else { - mHandler.sendMessage(message); - } - } - - public void perfromAccessibilityActionUiThread(Message message) { - final int accessibilityViewId = message.arg1; - final int virtualDescendantId = message.arg2; - SomeArgs args = (SomeArgs) message.obj; - final int action = args.argi1; - final int interactionId = args.argi2; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg1; - mPool.release(args); - boolean succeeded = false; - try { - View target = findViewByAccessibilityId(accessibilityViewId); - if (target != null && isDisplayedOnScreen(target)) { - AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); - if (provider != null) { - succeeded = provider.performAccessibilityAction(action, - virtualDescendantId); - } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { - switch (action) { - case AccessibilityNodeInfo.ACTION_FOCUS: { - if (!target.hasFocus()) { - // Get out of touch mode since accessibility - // wants to move focus around. - ensureTouchMode(false); - succeeded = target.requestFocus(); - } - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { - if (target.hasFocus()) { - target.clearFocus(); - succeeded = !target.isFocused(); - } - } break; - case AccessibilityNodeInfo.ACTION_SELECT: { - if (!target.isSelected()) { - target.setSelected(true); - succeeded = target.isSelected(); - } - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { - if (target.isSelected()) { - target.setSelected(false); - succeeded = !target.isSelected(); - } - } break; - } - } - } - } finally { + // We cannot make the call and notify the caller so it does not wait. try { - callback.setPerformAccessibilityActionResult(succeeded, interactionId); + callback.setFindAccessibilityNodeInfoResult(null, interactionId); } catch (RemoteException re) { - /* ignore - the other side will time out */ + /* best effort - ignore */ } } } - - private View findViewByAccessibilityId(int accessibilityId) { - View root = ViewRootImpl.this.mView; - if (root == null) { - return null; - } - View foundView = root.findViewByAccessibilityId(accessibilityId); - if (foundView != null && foundView.getVisibility() != View.VISIBLE) { - return null; - } - return foundView; - } } private class SendWindowContentChangedAccessibilityEvent implements Runnable { - public volatile boolean mIsPending; + public View mSource; public void run() { - if (mView != null) { - mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - mIsPending = false; - } - } - } - - /** - * This class encapsulates a prefetching strategy for the accessibility APIs for - * querying window content. It is responsible to prefetch a batch of - * AccessibilityNodeInfos in addition to the one for a requested node. - */ - class AccessibilityNodePrefetcher { - - private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50; - - public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int prefetchFlags, - List<AccessibilityNodeInfo> outInfos) { - AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); - if (provider == null) { - AccessibilityNodeInfo root = view.createAccessibilityNodeInfo(); - if (root != null) { - outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { - prefetchPredecessorsOfRealNode(view, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { - prefetchSiblingsOfRealNode(view, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { - prefetchDescendantsOfRealNode(view, outInfos); - } - } - } else { - AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId); - if (root != null) { - outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { - prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { - prefetchSiblingsOfVirtualNode(root, view, provider, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { - prefetchDescendantsOfVirtualNode(root, provider, outInfos); - } - } - } - } - - private void prefetchPredecessorsOfRealNode(View view, - List<AccessibilityNodeInfo> outInfos) { - ViewParent parent = view.getParent(); - while (parent instanceof View - && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - View parentView = (View) parent; - final long parentNodeId = AccessibilityNodeInfo.makeNodeId( - parentView.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); - AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo(); - if (info != null) { - outInfos.add(info); - } - parent = parent.getParent(); - } - } - - private void prefetchSiblingsOfRealNode(View current, - List<AccessibilityNodeInfo> outInfos) { - ViewParent parent = current.getParent(); - if (parent instanceof ViewGroup) { - ViewGroup parentGroup = (ViewGroup) parent; - final int childCount = parentGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = parentGroup.getChildAt(i); - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE - && child.getAccessibilityViewId() != current.getAccessibilityViewId() - && isDisplayedOnScreen(child)) { - final long childNodeId = AccessibilityNodeInfo.makeNodeId( - child.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); - AccessibilityNodeInfo info = null; - AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); - if (provider == null) { - info = child.createAccessibilityNodeInfo(); - } else { - info = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.UNDEFINED); - } - if (info != null) { - outInfos.add(info); - } - } - } - } - } - - private void prefetchDescendantsOfRealNode(View root, - List<AccessibilityNodeInfo> outInfos) { - if (root instanceof ViewGroup) { - ViewGroup rootGroup = (ViewGroup) root; - HashMap<View, AccessibilityNodeInfo> addedChildren = - new HashMap<View, AccessibilityNodeInfo>(); - final int childCount = rootGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = rootGroup.getChildAt(i); - if (isDisplayedOnScreen(child) - && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final long childNodeId = AccessibilityNodeInfo.makeNodeId( - child.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); - AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); - if (provider == null) { - AccessibilityNodeInfo info = child.createAccessibilityNodeInfo(); - if (info != null) { - outInfos.add(info); - addedChildren.put(child, null); - } - } else { - AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.UNDEFINED); - if (info != null) { - outInfos.add(info); - addedChildren.put(child, info); - } - } - } - } - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) { - View addedChild = entry.getKey(); - AccessibilityNodeInfo virtualRoot = entry.getValue(); - if (virtualRoot == null) { - prefetchDescendantsOfRealNode(addedChild, outInfos); - } else { - AccessibilityNodeProvider provider = - addedChild.getAccessibilityNodeProvider(); - prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos); - } - } - } - } - } - - private void prefetchPredecessorsOfVirtualNode(AccessibilityNodeInfo root, - View providerHost, AccessibilityNodeProvider provider, - List<AccessibilityNodeInfo> outInfos) { - long parentNodeId = root.getParentNodeId(); - int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); - while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { - final int virtualDescendantId = - AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); - if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED - || accessibilityViewId == providerHost.getAccessibilityViewId()) { - AccessibilityNodeInfo parent = provider.createAccessibilityNodeInfo( - virtualDescendantId); - if (parent != null) { - outInfos.add(parent); - } - parentNodeId = parent.getParentNodeId(); - accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId( - parentNodeId); - } else { - prefetchPredecessorsOfRealNode(providerHost, outInfos); - return; - } - } - } - - private void prefetchSiblingsOfVirtualNode(AccessibilityNodeInfo current, View providerHost, - AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { - final long parentNodeId = current.getParentNodeId(); - final int parentAccessibilityViewId = - AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); - final int parentVirtualDescendantId = - AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); - if (parentVirtualDescendantId != AccessibilityNodeInfo.UNDEFINED - || parentAccessibilityViewId == providerHost.getAccessibilityViewId()) { - AccessibilityNodeInfo parent = - provider.createAccessibilityNodeInfo(parentVirtualDescendantId); - if (parent != null) { - SparseLongArray childNodeIds = parent.getChildNodeIds(); - final int childCount = childNodeIds.size(); - for (int i = 0; i < childCount; i++) { - final long childNodeId = childNodeIds.get(i); - if (childNodeId != current.getSourceNodeId() - && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final int childVirtualDescendantId = - AccessibilityNodeInfo.getVirtualDescendantId(childNodeId); - AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( - childVirtualDescendantId); - if (child != null) { - outInfos.add(child); - } - } - } - } - } else { - prefetchSiblingsOfRealNode(providerHost, outInfos); - } - } - - private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root, - AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { - SparseLongArray childNodeIds = root.getChildNodeIds(); - final int initialOutInfosSize = outInfos.size(); - final int childCount = childNodeIds.size(); - for (int i = 0; i < childCount; i++) { - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final long childNodeId = childNodeIds.get(i); - AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.getVirtualDescendantId(childNodeId)); - if (child != null) { - outInfos.add(child); - } - } - } - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final int addedChildCount = outInfos.size() - initialOutInfosSize; - for (int i = 0; i < addedChildCount; i++) { - AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i); - prefetchDescendantsOfVirtualNode(child, provider, outInfos); - } + if (mSource != null) { + mSource.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + mSource.resetAccessibilityStateChanged(); + mSource = null; } } } diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index 9152cc3..110c239 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -263,7 +263,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; - mVibrator = new Vibrator(); + mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable); mShowCombinedVolumes = !mVoiceCapable && !useMasterVolume; diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 66bdc5d..27baaea 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -667,7 +667,7 @@ public interface WindowManagerPolicy { /** * Create and return an animation to re-display a force hidden window. */ - public Animation createForceHideEnterAnimation(); + public Animation createForceHideEnterAnimation(boolean onWallpaper); /** * Called from the input reader thread before a key is enqueued. diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 0998c80..6cb1578 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -508,7 +508,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_VIEW_SELECTED = 0x00000004; /** - * Represents the event of focusing a {@link android.view.View}. + * Represents the event of setting input focus of a {@link android.view.View}. */ public static final int TYPE_VIEW_FOCUSED = 0x00000008; @@ -549,7 +549,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; /** - * Represents the event of changing the content of a window. + * Represents the event of changing the content of a window and more + * specifically the sub-tree rooted at the event's source. */ public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800; @@ -569,6 +570,16 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_ANNOUNCEMENT = 0x00004000; /** + * Represents the event of gaining accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000; + + /** + * Represents the event of clearing accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000; + + /** * Mask for {@link AccessibilityEvent} all types. * * @see #TYPE_VIEW_CLICKED @@ -1018,6 +1029,10 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par return "TYPE_VIEW_SCROLLED"; case TYPE_ANNOUNCEMENT: return "TYPE_ANNOUNCEMENT"; + case TYPE_VIEW_ACCESSIBILITY_FOCUSED: + return "TYPE_VIEW_ACCESSIBILITY_FOCUSED"; + case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: + return "TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED"; default: return null; } diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index be74b31..35f0d9d 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -18,7 +18,9 @@ package android.view.accessibility; import android.accessibilityservice.IAccessibilityServiceConnection; import android.graphics.Rect; +import android.os.Binder; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; @@ -174,7 +176,7 @@ public final class AccessibilityInteractionClient final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( accessibilityWindowId, accessibilityNodeId, interactionId, this, - Thread.currentThread().getId(), prefetchFlags); + prefetchFlags, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( @@ -293,6 +295,96 @@ public final class AccessibilityInteractionClient } /** + * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the + * specified focus type. The search is performed in the window whose id is specified + * and starts from the node whose accessibility id is specified. + * + * @param connectionId The id of a connection for interacting with the system. + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param focusType The focus type. + * @return The accessibility focused {@link AccessibilityNodeInfo}. + */ + public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId, + long accessibilityNodeId, int focusType) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection != null) { + final int interactionId = mInteractionIdCounter.getAndIncrement(); + final float windowScale = connection.findFocus(accessibilityWindowId, + accessibilityNodeId, focusType, interactionId, this, + Thread.currentThread().getId()); + // If the scale is zero the call has failed. + if (windowScale > 0) { + AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( + interactionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + return info; + } + } else { + if (DEBUG) { + Log.w(LOG_TAG, "No connection for connection id: " + connectionId); + } + } + } catch (RemoteException re) { + if (DEBUG) { + Log.w(LOG_TAG, "Error while calling remote findAccessibilityFocus", re); + } + } + return null; + } + + /** + * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}. + * The search is performed in the window whose id is specified and starts from the + * node whose accessibility id is specified. + * + * @param connectionId The id of a connection for interacting with the system. + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param direction The direction in which to search for focusable. + * @return The accessibility focused {@link AccessibilityNodeInfo}. + */ + public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId, + long accessibilityNodeId, int direction) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection != null) { + final int interactionId = mInteractionIdCounter.getAndIncrement(); + final float windowScale = connection.focusSearch(accessibilityWindowId, + accessibilityNodeId, direction, interactionId, this, + Thread.currentThread().getId()); + // If the scale is zero the call has failed. + if (windowScale > 0) { + AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( + interactionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + return info; + } + } else { + if (DEBUG) { + Log.w(LOG_TAG, "No connection for connection id: " + connectionId); + } + } + } catch (RemoteException re) { + if (DEBUG) { + Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); + } + } + return null; + } + + /** * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * * @param connectionId The id of a connection for interacting with the system. @@ -382,7 +474,12 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { final boolean success = waitForResultTimedLocked(interactionId); - List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null; + List<AccessibilityNodeInfo> result = null; + if (success) { + result = mFindAccessibilityNodeInfosResult; + } else { + result = Collections.emptyList(); + } clearResultLocked(); return result; } @@ -395,13 +492,18 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { if (interactionId > mInteractionId) { - // If the call is not an IPC, i.e. it is made from the same process, we need to - // instantiate new result list to avoid passing internal instances to clients. - final boolean isIpcCall = (queryLocalInterface(getInterfaceDescriptor()) == null); - if (!isIpcCall) { - mFindAccessibilityNodeInfosResult = new ArrayList<AccessibilityNodeInfo>(infos); + if (infos != null) { + // If the call is not an IPC, i.e. it is made from the same process, we need to + // instantiate new result list to avoid passing internal instances to clients. + final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid()); + if (!isIpcCall) { + mFindAccessibilityNodeInfosResult = + new ArrayList<AccessibilityNodeInfo>(infos); + } else { + mFindAccessibilityNodeInfosResult = infos; + } } else { - mFindAccessibilityNodeInfosResult = infos; + mFindAccessibilityNodeInfosResult = Collections.emptyList(); } mInteractionId = interactionId; } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index e37de6f..77fd12a 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -204,6 +204,12 @@ public final class AccessibilityManager { * @param event The event to send. * * @throws IllegalStateException if accessibility is not enabled. + * + * <strong>Note:</strong> The preferred mechanism for sending custom accessibility + * events is through calling + * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * instead of this method to allow predecessors to augment/filter events sent by + * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { if (!mIsEnabled) { diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index f616dca..1071c65 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -74,29 +74,57 @@ public class AccessibilityNodeInfo implements Parcelable { public static final int FLAG_PREFETCH_SIBLINGS = 0x00000002; /** @hide */ - public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000003; + public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004; + + /** @hide */ + public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008; // Actions. /** - * Action that focuses the node. + * Action that gives input focus to the node. */ - public static final int ACTION_FOCUS = 0x00000001; + public static final int ACTION_FOCUS = 0x00000001; /** - * Action that unfocuses the node. + * Action that clears input focus of the node. */ - public static final int ACTION_CLEAR_FOCUS = 0x00000002; + public static final int ACTION_CLEAR_FOCUS = 0x00000002; /** * Action that selects the node. */ - public static final int ACTION_SELECT = 0x00000004; + public static final int ACTION_SELECT = 0x00000004; /** * Action that unselects the node. */ - public static final int ACTION_CLEAR_SELECTION = 0x00000008; + public static final int ACTION_CLEAR_SELECTION = 0x00000008; + + /** + * Action that gives accessibility focus to the node. + */ + public static final int ACTION_ACCESSIBILITY_FOCUS = 0x00000010; + + /** + * Action that clears accessibility focus of the node. + */ + public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000020; + + /** + * Action that clicks on the node info./AccessibilityNodeInfoCache.java + */ + public static final int ACTION_CLICK = 0x00000040; + + /** + * The input focus. + */ + public static final int FOCUS_INPUT = 1; + + /** + * The accessibility focus. + */ + public static final int FOCUS_ACCESSIBILITY = 2; // Boolean attributes. @@ -120,6 +148,8 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int PROPERTY_SCROLLABLE = 0x00000200; + private static final int PROPERTY_ACCESSIBILITY_FOCUSED = 0x00000400; + /** * Bits that provide the id of a virtual descendant of a view. */ @@ -248,6 +278,57 @@ public class AccessibilityNodeInfo implements Parcelable { (root != null) ? root.getAccessibilityViewId() : UNDEFINED; mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } + + /** + * Find the view that has the input focus. The search starts from + * the view represented by this node info. + * + * @param focus The focus to find. One of {@link #FOCUS_INPUT} or + * {@link #FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * + * @see #FOCUS_INPUT + * @see #FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfo findFocus(int focus) { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return null; + } + return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, mWindowId, + mSourceNodeId, focus); + } + + /** + * Searches for the nearest view in the specified direction that can take + * the input focus. + * + * @param direction The direction. Can be one of: + * {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_UP}, + * {@link View#FOCUS_LEFT}, + * {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_FORWARD}, + * {@link View#FOCUS_BACKWARD}, + * {@link View#ACCESSIBILITY_FOCUS_IN}, + * {@link View#ACCESSIBILITY_FOCUS_OUT}, + * {@link View#ACCESSIBILITY_FOCUS_FORWARD}, + * {@link View#ACCESSIBILITY_FOCUS_BACKWARD}, + * {@link View#ACCESSIBILITY_FOCUS_UP}, + * {@link View#ACCESSIBILITY_FOCUS_RIGHT}, + * {@link View#ACCESSIBILITY_FOCUS_DOWN}, + * {@link View#ACCESSIBILITY_FOCUS_LEFT}. + * + * @return The node info for the view that can take accessibility focus. + */ + public AccessibilityNodeInfo focusSearch(int direction) { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return null; + } + return AccessibilityInteractionClient.getInstance().focusSearch(mConnectionId, mWindowId, + mSourceNodeId, direction); + } /** * Gets the id of the window from which the info comes from. @@ -642,6 +723,31 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets whether this node is accessibility focused. + * + * @return True if the node is accessibility focused. + */ + public boolean isAccessibilityFocused() { + return getBooleanProperty(PROPERTY_ACCESSIBILITY_FOCUSED); + } + + /** + * Sets whether this node is accessibility focused. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param focused True if the node is accessibility focused. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAccessibilityFocused(boolean focused) { + setBooleanProperty(PROPERTY_ACCESSIBILITY_FOCUSED, focused); + } + + /** * Gets whether this node is selected. * * @return True if the node is selected. diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java index dfbfc70..d2609bb 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java @@ -18,6 +18,7 @@ package android.view.accessibility; import android.util.Log; import android.util.LongSparseArray; +import android.util.SparseLongArray; /** * Simple cache for AccessibilityNodeInfos. The cache is mapping an @@ -54,20 +55,25 @@ public class AccessibilityNodeInfoCache { * @param event An event. */ public void onAccessibilityEvent(AccessibilityEvent event) { - final int eventType = event.getEventType(); - switch (eventType) { - case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: - case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: - case AccessibilityEvent.TYPE_VIEW_SCROLLED: - clear(); - break; - case AccessibilityEvent.TYPE_VIEW_FOCUSED: - case AccessibilityEvent.TYPE_VIEW_SELECTED: - case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: - case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: - final long accessibilityNodeId = event.getSourceNodeId(); - remove(accessibilityNodeId); - break; + if (ENABLED) { + final int eventType = event.getEventType(); + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { + clear(); + } break; + case AccessibilityEvent.TYPE_VIEW_FOCUSED: + case AccessibilityEvent.TYPE_VIEW_SELECTED: + case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: + case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { + final long accessibilityNodeId = event.getSourceNodeId(); + remove(accessibilityNodeId); + } break; + case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: + case AccessibilityEvent.TYPE_VIEW_SCROLLED: { + final long accessibilityNodeId = event.getSourceNodeId(); + clearSubTree(accessibilityNodeId); + } break; + } } } @@ -167,4 +173,23 @@ public class AccessibilityNodeInfoCache { } } } + + /** + * Clears a subtree rooted at the node with the given id. + * + * @param rootNodeId The root id. + */ + private void clearSubTree(long rootNodeId) { + AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId); + if (current == null) { + return; + } + mCacheImpl.remove(rootNodeId); + SparseLongArray childNodeIds = current.getChildNodeIds(); + final int childCount = childNodeIds.size(); + for (int i = 0; i < childCount; i++) { + final long childNodeId = childNodeIds.valueAt(i); + clearSubTree(childNodeId); + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeProvider.java b/core/java/android/view/accessibility/AccessibilityNodeProvider.java index 5890417..19e35dd 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeProvider.java +++ b/core/java/android/view/accessibility/AccessibilityNodeProvider.java @@ -87,6 +87,7 @@ public abstract class AccessibilityNodeProvider { * @return A populated {@link AccessibilityNodeInfo} for a virtual descendant or the * host View. * + * @see View#createAccessibilityNodeInfo() * @see AccessibilityNodeInfo */ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { @@ -102,6 +103,7 @@ public abstract class AccessibilityNodeProvider { * @param virtualViewId A client defined virtual view id. * @return True if the action was performed. * + * @see View#performAccessibilityAction(int) * @see #createAccessibilityNodeInfo(int) * @see AccessibilityNodeInfo */ @@ -127,4 +129,58 @@ public abstract class AccessibilityNodeProvider { int virtualViewId) { return null; } + + /** + * Finds the accessibility focused {@link AccessibilityNodeInfo}. The search is + * relative to the virtual view, i.e. a descendant of the host View, with the + * given <code>virtualViewId</code> or the host View itself + * <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * <strong>Note:</strong> Normally the system is responsible to transparently find + * accessibility focused view starting from a given root but for virtual view + * hierarchies it is a responsibility of this provider's implementor to find + * the accessibility focused virtual view. + * + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo findAccessibilitiyFocus(int virtualViewId) { + return null; + } + + /** + * Finds {@link AccessibilityNodeInfo} to take accessibility focus in the given + * <code>direction</code>. The search is relative to the virtual view, i.e. a + * descendant of the host View, with the given <code>virtualViewId</code> or + * the host View itself <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * <strong>Note:</strong> Normally the system is responsible to transparently find + * the next view to take accessibility focus but for virtual view hierarchies + * it is a responsibility of this provider's implementor to compute the next + * focusable. + * + * @param direction The direction in which to search for a focus candidate. + * Values are + * {@link View#ACCESSIBILITY_FOCUS_IN}, + * {@link View#ACCESSIBILITY_FOCUS_OUT}, + * {@link View#ACCESSIBILITY_FOCUS_FORWARD}, + * {@link View#ACCESSIBILITY_FOCUS_BACKWARD}, + * {@link View#ACCESSIBILITY_FOCUS_UP}, + * {@link View#ACCESSIBILITY_FOCUS_DOWN}, + * {@link View#ACCESSIBILITY_FOCUS_LEFT}, + * {@link View#ACCESSIBILITY_FOCUS_RIGHT}. + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo accessibilityFocusSearch(int direction, int virtualViewId) { + return null; + } } diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index d25b3db..78a7d46 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -62,6 +62,7 @@ public class AccessibilityRecord { private static final int PROPERTY_PASSWORD = 0x00000004; private static final int PROPERTY_FULL_SCREEN = 0x00000080; private static final int PROPERTY_SCROLLABLE = 0x00000100; + private static final int PROPERTY_IMPORTANT_FOR_ACCESSIBILITY = 0x00000200; private static final int GET_SOURCE_PREFETCH_FLAGS = AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS @@ -77,7 +78,7 @@ public class AccessibilityRecord { private boolean mIsInPool; boolean mSealed; - int mBooleanProperties; + int mBooleanProperties = PROPERTY_IMPORTANT_FOR_ACCESSIBILITY; int mCurrentItemIndex = UNDEFINED; int mItemCount = UNDEFINED; int mFromIndex = UNDEFINED; @@ -134,6 +135,8 @@ public class AccessibilityRecord { */ public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); + final boolean important = (root != null) ? root.isImportantForAccessibility() : true; + setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, important); mSourceWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; final int rootViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED; mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId); @@ -274,6 +277,23 @@ public class AccessibilityRecord { } /** + * Gets if the source is important for accessibility. + * + * <strong>Note:</strong> Used only internally to determine whether + * to deliver the event to a given accessibility service since some + * services may want to regard all views for accessibility while others + * may want to regard only the important views for accessibility. + * + * @return True if the source is important for accessibility, + * false otherwise. + * + * @hide + */ + public boolean isImportantForAccessibility() { + return getBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY); + } + + /** * Gets the number of items that can be visited. * * @return The number of items. @@ -755,7 +775,7 @@ public class AccessibilityRecord { */ void clear() { mSealed = false; - mBooleanProperties = 0; + mBooleanProperties = PROPERTY_IMPORTANT_FOR_ACCESSIBILITY; mCurrentItemIndex = UNDEFINED; mItemCount = UNDEFINED; mFromIndex = UNDEFINED; diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index fc3651c..8182d29 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -28,18 +28,26 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback; oneway interface IAccessibilityInteractionConnection { void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int prefetchFlags, - int interrogatingPid, long interrogatingTid); + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int id, int interactionId, - IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid); + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); + + void findFocus(long accessibilityNodeId, int interactionId, int focusType, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); + + void focusSearch(long accessibilityNodeId, int interactionId, int direction, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid); void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid); } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 320c75d..5b5134a 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -19,7 +19,7 @@ package android.view.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceConnection; -import android.accessibilityservice.IEventListener; +import android.accessibilityservice.IAccessibilityServiceClient; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnection; @@ -49,7 +49,8 @@ interface IAccessibilityManager { void removeAccessibilityInteractionConnection(IWindow windowToken); - void registerUiTestAutomationService(IEventListener listener, in AccessibilityServiceInfo info); + void registerUiTestAutomationService(IAccessibilityServiceClient client, + in AccessibilityServiceInfo info); - void unregisterUiTestAutomationService(IEventListener listener); + void unregisterUiTestAutomationService(IAccessibilityServiceClient client); } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 800ebc8..64fbdd5 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -147,6 +147,40 @@ class CallbackProxy extends Handler { } } + private class JsResultReceiver implements JsResult.ResultReceiver { + // This prevents a user from interacting with the result before WebCore is + // ready to handle it. + private boolean mReady; + // Tells us if the user tried to confirm or cancel the result before WebCore + // is ready. + private boolean mTriedToNotifyBeforeReady; + + public JsPromptResult mJsResult = new JsPromptResult(this); + + final void setReady() { + mReady = true; + if (mTriedToNotifyBeforeReady) { + notifyCallbackProxy(); + } + } + + /* Wake up the WebCore thread. */ + @Override + public void onJsResultComplete(JsResult result) { + if (mReady) { + notifyCallbackProxy(); + } else { + mTriedToNotifyBeforeReady = true; + } + } + + private void notifyCallbackProxy() { + synchronized (CallbackProxy.this) { + CallbackProxy.this.notify(); + } + } +} + /** * Construct a new CallbackProxy. */ @@ -479,18 +513,18 @@ class CallbackProxy extends Handler { String databaseIdentifier = (String) map.get("databaseIdentifier"); String url = (String) map.get("url"); - long currentQuota = - ((Long) map.get("currentQuota")).longValue(); - long totalUsedQuota = - ((Long) map.get("totalUsedQuota")).longValue(); - long estimatedSize = - ((Long) map.get("estimatedSize")).longValue(); + long quota = + ((Long) map.get("quota")).longValue(); + long totalQuota = + ((Long) map.get("totalQuota")).longValue(); + long estimatedDatabaseSize = + ((Long) map.get("estimatedDatabaseSize")).longValue(); WebStorage.QuotaUpdater quotaUpdater = (WebStorage.QuotaUpdater) map.get("quotaUpdater"); mWebChromeClient.onExceededDatabaseQuota(url, - databaseIdentifier, currentQuota, estimatedSize, - totalUsedQuota, quotaUpdater); + databaseIdentifier, quota, estimatedDatabaseSize, + totalQuota, quotaUpdater); } break; @@ -498,15 +532,15 @@ class CallbackProxy extends Handler { if (mWebChromeClient != null) { HashMap<String, Object> map = (HashMap<String, Object>) msg.obj; - long spaceNeeded = - ((Long) map.get("spaceNeeded")).longValue(); - long totalUsedQuota = - ((Long) map.get("totalUsedQuota")).longValue(); + long requiredStorage = + ((Long) map.get("requiredStorage")).longValue(); + long quota = + ((Long) map.get("quota")).longValue(); WebStorage.QuotaUpdater quotaUpdater = (WebStorage.QuotaUpdater) map.get("quotaUpdater"); - mWebChromeClient.onReachedMaxAppCacheSize(spaceNeeded, - totalUsedQuota, quotaUpdater); + mWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, + quota, quotaUpdater); } break; @@ -531,14 +565,15 @@ class CallbackProxy extends Handler { case JS_ALERT: if (mWebChromeClient != null) { - final JsResult res = (JsResult) msg.obj; + final JsResultReceiver receiver = (JsResultReceiver) msg.obj; + final JsResult res = receiver.mJsResult; String message = msg.getData().getString("message"); String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsAlert(mWebView.getWebView(), url, message, res)) { if (!canShowAlertDialog()) { res.cancel(); - res.setReady(); + receiver.setReady(); break; } new AlertDialog.Builder(mContext) @@ -561,20 +596,21 @@ class CallbackProxy extends Handler { }) .show(); } - res.setReady(); + receiver.setReady(); } break; case JS_CONFIRM: if (mWebChromeClient != null) { - final JsResult res = (JsResult) msg.obj; + final JsResultReceiver receiver = (JsResultReceiver) msg.obj; + final JsResult res = receiver.mJsResult; String message = msg.getData().getString("message"); String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsConfirm(mWebView.getWebView(), url, message, res)) { if (!canShowAlertDialog()) { res.cancel(); - res.setReady(); + receiver.setReady(); break; } new AlertDialog.Builder(mContext) @@ -605,13 +641,14 @@ class CallbackProxy extends Handler { } // Tell the JsResult that it is ready for client // interaction. - res.setReady(); + receiver.setReady(); } break; case JS_PROMPT: if (mWebChromeClient != null) { - final JsPromptResult res = (JsPromptResult) msg.obj; + final JsResultReceiver receiver = (JsResultReceiver) msg.obj; + final JsPromptResult res = receiver.mJsResult; String message = msg.getData().getString("message"); String defaultVal = msg.getData().getString("default"); String url = msg.getData().getString("url"); @@ -619,7 +656,7 @@ class CallbackProxy extends Handler { defaultVal, res)) { if (!canShowAlertDialog()) { res.cancel(); - res.setReady(); + receiver.setReady(); break; } final LayoutInflater factory = LayoutInflater @@ -662,20 +699,21 @@ class CallbackProxy extends Handler { } // Tell the JsResult that it is ready for client // interaction. - res.setReady(); + receiver.setReady(); } break; case JS_UNLOAD: if (mWebChromeClient != null) { - final JsResult res = (JsResult) msg.obj; + final JsResultReceiver receiver = (JsResultReceiver) msg.obj; + final JsResult res = receiver.mJsResult; String message = msg.getData().getString("message"); String url = msg.getData().getString("url"); if (!mWebChromeClient.onJsBeforeUnload(mWebView.getWebView(), url, message, res)) { if (!canShowAlertDialog()) { res.cancel(); - res.setReady(); + receiver.setReady(); break; } final String m = mContext.getString( @@ -700,19 +738,20 @@ class CallbackProxy extends Handler { }) .show(); } - res.setReady(); + receiver.setReady(); } break; case JS_TIMEOUT: if(mWebChromeClient != null) { - final JsResult res = (JsResult) msg.obj; + final JsResultReceiver receiver = (JsResultReceiver) msg.obj; + final JsResult res = receiver.mJsResult; if(mWebChromeClient.onJsTimeout()) { res.confirm(); } else { res.cancel(); } - res.setReady(); + receiver.setReady(); } break; @@ -791,7 +830,8 @@ class CallbackProxy extends Handler { case OPEN_FILE_CHOOSER: if (mWebChromeClient != null) { UploadFileMessageData data = (UploadFileMessageData)msg.obj; - mWebChromeClient.openFileChooser(data.getUploadFile(), data.getAcceptType()); + mWebChromeClient.openFileChooser(data.getUploadFile(), data.getAcceptType(), + data.getCapture()); } break; @@ -1331,7 +1371,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient == null) { return; } - JsResult result = new JsResult(this, false); + JsResultReceiver result = new JsResultReceiver(); Message alert = obtainMessage(JS_ALERT, result); alert.getData().putString("message", message); alert.getData().putString("url", url); @@ -1352,7 +1392,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient == null) { return false; } - JsResult result = new JsResult(this, false); + JsResultReceiver result = new JsResultReceiver(); Message confirm = obtainMessage(JS_CONFIRM, result); confirm.getData().putString("message", message); confirm.getData().putString("url", url); @@ -1365,7 +1405,7 @@ class CallbackProxy extends Handler { Log.e(LOGTAG, Log.getStackTraceString(e)); } } - return result.getResult(); + return result.mJsResult.getResult(); } public String onJsPrompt(String url, String message, String defaultValue) { @@ -1374,7 +1414,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient == null) { return null; } - JsPromptResult result = new JsPromptResult(this); + JsResultReceiver result = new JsResultReceiver(); Message prompt = obtainMessage(JS_PROMPT, result); prompt.getData().putString("message", message); prompt.getData().putString("default", defaultValue); @@ -1388,7 +1428,7 @@ class CallbackProxy extends Handler { Log.e(LOGTAG, Log.getStackTraceString(e)); } } - return result.getStringResult(); + return result.mJsResult.getStringResult(); } public boolean onJsBeforeUnload(String url, String message) { @@ -1397,7 +1437,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient == null) { return true; } - JsResult result = new JsResult(this, true); + JsResultReceiver result = new JsResultReceiver(); Message confirm = obtainMessage(JS_UNLOAD, result); confirm.getData().putString("message", message); confirm.getData().putString("url", url); @@ -1410,7 +1450,7 @@ class CallbackProxy extends Handler { Log.e(LOGTAG, Log.getStackTraceString(e)); } } - return result.getResult(); + return result.mJsResult.getResult(); } /** @@ -1422,19 +1462,21 @@ class CallbackProxy extends Handler { * @param url The URL that caused the quota overflow. * @param databaseIdentifier The identifier of the database that the * transaction that caused the overflow was running on. - * @param currentQuota The current quota the origin is allowed. - * @param estimatedSize The estimated size of the database. - * @param totalUsedQuota is the sum of all origins' quota. + * @param quota The current quota the origin is allowed. + * @param estimatedDatabaseSize The estimated size of the database. + * @param totalQuota is the sum of all origins' quota. * @param quotaUpdater An instance of a class encapsulating a callback * to WebViewCore to run when the decision to allow or deny more * quota has been made. */ public void onExceededDatabaseQuota( - String url, String databaseIdentifier, long currentQuota, - long estimatedSize, long totalUsedQuota, + String url, String databaseIdentifier, long quota, + long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) { if (mWebChromeClient == null) { - quotaUpdater.updateQuota(currentQuota); + // Native-side logic prevents the quota being updated to a smaller + // value. + quotaUpdater.updateQuota(quota); return; } @@ -1442,9 +1484,9 @@ class CallbackProxy extends Handler { HashMap<String, Object> map = new HashMap(); map.put("databaseIdentifier", databaseIdentifier); map.put("url", url); - map.put("currentQuota", currentQuota); - map.put("estimatedSize", estimatedSize); - map.put("totalUsedQuota", totalUsedQuota); + map.put("quota", quota); + map.put("estimatedDatabaseSize", estimatedDatabaseSize); + map.put("totalQuota", totalQuota); map.put("quotaUpdater", quotaUpdater); exceededQuota.obj = map; sendMessage(exceededQuota); @@ -1453,24 +1495,26 @@ class CallbackProxy extends Handler { /** * Called by WebViewCore to inform the Java side that the appcache has * exceeded its max size. - * @param spaceNeeded is the amount of disk space that would be needed - * in order for the last appcache operation to succeed. - * @param totalUsedQuota is the sum of all origins' quota. + * @param requiredStorage is the amount of storage, in bytes, that would be + * needed in order for the last appcache operation to succeed. + * @param quota is the current quota (for all origins). * @param quotaUpdater An instance of a class encapsulating a callback * to WebViewCore to run when the decision to allow or deny a bigger * app cache size has been made. */ - public void onReachedMaxAppCacheSize(long spaceNeeded, - long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { + public void onReachedMaxAppCacheSize(long requiredStorage, + long quota, WebStorage.QuotaUpdater quotaUpdater) { if (mWebChromeClient == null) { - quotaUpdater.updateQuota(0); + // Native-side logic prevents the quota being updated to a smaller + // value. + quotaUpdater.updateQuota(quota); return; } Message msg = obtainMessage(REACHED_APPCACHE_MAXSIZE); HashMap<String, Object> map = new HashMap(); - map.put("spaceNeeded", spaceNeeded); - map.put("totalUsedQuota", totalUsedQuota); + map.put("requiredStorage", requiredStorage); + map.put("quota", quota); map.put("quotaUpdater", quotaUpdater); msg.obj = map; sendMessage(msg); @@ -1540,7 +1584,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient == null) { return true; } - JsResult result = new JsResult(this, true); + JsResultReceiver result = new JsResultReceiver(); Message timeout = obtainMessage(JS_TIMEOUT, result); synchronized (this) { sendMessage(timeout); @@ -1551,7 +1595,7 @@ class CallbackProxy extends Handler { Log.e(LOGTAG, Log.getStackTraceString(e)); } } - return result.getResult(); + return result.mJsResult.getResult(); } public void getVisitedHistory(ValueCallback<String[]> callback) { @@ -1566,10 +1610,12 @@ class CallbackProxy extends Handler { private static class UploadFileMessageData { private UploadFile mCallback; private String mAcceptType; + private String mCapture; - public UploadFileMessageData(UploadFile uploadFile, String acceptType) { + public UploadFileMessageData(UploadFile uploadFile, String acceptType, String capture) { mCallback = uploadFile; mAcceptType = acceptType; + mCapture = capture; } public UploadFile getUploadFile() { @@ -1579,6 +1625,10 @@ class CallbackProxy extends Handler { public String getAcceptType() { return mAcceptType; } + + public String getCapture() { + return mCapture; + } } private class UploadFile implements ValueCallback<Uri> { @@ -1597,13 +1647,13 @@ class CallbackProxy extends Handler { /** * Called by WebViewCore to open a file chooser. */ - /* package */ Uri openFileChooser(String acceptType) { + /* package */ Uri openFileChooser(String acceptType, String capture) { if (mWebChromeClient == null) { return null; } Message myMessage = obtainMessage(OPEN_FILE_CHOOSER); UploadFile uploadFile = new UploadFile(); - UploadFileMessageData data = new UploadFileMessageData(uploadFile, acceptType); + UploadFileMessageData data = new UploadFileMessageData(uploadFile, acceptType, capture); myMessage.obj = data; synchronized (this) { sendMessage(myMessage); diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index 5f7ef41..2997c1a 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -26,7 +26,7 @@ import android.util.Log; * Manages the cookies used by an application's {@link WebView} instances. * Cookies are manipulated according to RFC2109. */ -public final class CookieManager { +public class CookieManager { private static CookieManager sRef; diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java index 93eb082..1441541 100755 --- a/core/java/android/webkit/GeolocationPermissions.java +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -50,7 +50,7 @@ import java.util.Vector; // Within WebKit, Geolocation permissions may be applied either temporarily // (for the duration of the page) or permanently. This class deals only with // permanent permissions. -public final class GeolocationPermissions { +public class GeolocationPermissions { /** * A callback interface used by the host application to set the Geolocation * permission state for an origin. @@ -293,6 +293,16 @@ public final class GeolocationPermissions { postMessage(Message.obtain(null, CLEAR_ALL)); } + /** + * This class should not be instantiated directly, applications must only use + * {@link #getInstance()} to obtain the instance. + * Note this constructor was erroneously public and published in SDK levels prior to 16, but + * applications using it would receive a non-functional instance of this class (there was no + * way to call createHandler() and createUIHandler(), so it would not work). + * @hide + */ + public GeolocationPermissions() {} + // Native functions, run on the WebKit thread. private static native Set nativeGetOrigins(); private static native boolean nativeGetAllowed(String origin); diff --git a/core/java/android/webkit/JsPromptResult.java b/core/java/android/webkit/JsPromptResult.java index 9fcd1bc..a1bf124 100644 --- a/core/java/android/webkit/JsPromptResult.java +++ b/core/java/android/webkit/JsPromptResult.java @@ -18,11 +18,11 @@ package android.webkit; /** - * Public class for handling javascript prompt requests. A - * JsDialogHandlerInterface implentation will receive a jsPrompt call with a - * JsPromptResult parameter. This parameter is used to return a result to - * WebView. The client can call cancel() to cancel the dialog or confirm() with - * the user's input to confirm the dialog. + * Public class for handling JavaScript prompt requests. The WebChromeClient will receive a + * {@link WebChromeClient#onJsPrompt(WebView, String, String, String, JsPromptResult)} call with a + * JsPromptResult instance as a parameter. This parameter is used to return the result of this user + * dialog prompt back to the WebView instance. The client can call cancel() to cancel the dialog or + * confirm() with the user's input to confirm the dialog. */ public class JsPromptResult extends JsResult { // String result of the prompt @@ -36,17 +36,17 @@ public class JsPromptResult extends JsResult { confirm(); } - /*package*/ JsPromptResult(CallbackProxy proxy) { - super(proxy, /* unused */ false); + /** + * @hide Only for use by WebViewProvider implementations + */ + public JsPromptResult(ResultReceiver receiver) { + super(receiver); } - /*package*/ String getStringResult() { + /** + * @hide Only for use by WebViewProvider implementations + */ + public String getStringResult() { return mStringResult; } - - @Override - /*package*/ void handleDefault() { - mStringResult = null; - super.handleDefault(); - } } diff --git a/core/java/android/webkit/JsResult.java b/core/java/android/webkit/JsResult.java index e61ab21..e4e6851 100644 --- a/core/java/android/webkit/JsResult.java +++ b/core/java/android/webkit/JsResult.java @@ -16,23 +16,24 @@ package android.webkit; - +/** + * An instance of this class is passed as a parameter in various {@link WebChromeClient} action + * notifications. The object is used as a handle onto the underlying JavaScript-originated request, + * and provides a means for the client to indicate whether this action should proceed. + */ public class JsResult { - // This prevents a user from interacting with the result before WebCore is - // ready to handle it. - private boolean mReady; - // Tells us if the user tried to confirm or cancel the result before WebCore - // is ready. - private boolean mTriedToNotifyBeforeReady; - // This is a basic result of a confirm or prompt dialog. - protected boolean mResult; /** - * This is the caller of the prompt and is the object that is waiting. - * @hide + * Callback interface, implemented by the WebViewProvider implementation to receive + * notifications when the JavaScript result represented by a JsResult instance has + * @hide Only for use by WebViewProvider implementations */ - protected final CallbackProxy mProxy; - // This is the default value of the result. - private final boolean mDefaultValue; + public interface ResultReceiver { + public void onJsResultComplete(JsResult result); + } + // This is the caller of the prompt and is the object that is waiting. + private final ResultReceiver mReceiver; + // This is a basic result of a confirm or prompt dialog. + private boolean mResult; /** * Handle the result if the user cancelled the dialog. @@ -50,36 +51,22 @@ public class JsResult { wakeUp(); } - /*package*/ JsResult(CallbackProxy proxy, boolean defaultVal) { - mProxy = proxy; - mDefaultValue = defaultVal; + /** + * @hide Only for use by WebViewProvider implementations + */ + public JsResult(ResultReceiver receiver) { + mReceiver = receiver; } - /*package*/ final boolean getResult() { + /** + * @hide Only for use by WebViewProvider implementations + */ + public final boolean getResult() { return mResult; } - /*package*/ final void setReady() { - mReady = true; - if (mTriedToNotifyBeforeReady) { - wakeUp(); - } - } - - /*package*/ void handleDefault() { - setReady(); - mResult = mDefaultValue; - wakeUp(); - } - - /* Wake up the WebCore thread. */ - protected final void wakeUp() { - if (mReady) { - synchronized (mProxy) { - mProxy.notify(); - } - } else { - mTriedToNotifyBeforeReady = true; - } + /* Notify the caller that the JsResult has completed */ + private final void wakeUp() { + mReceiver.onJsResultComplete(this); } } diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index a6ef0ce..4e8790b 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -216,37 +216,54 @@ public class WebChromeClient { } /** - * Tell the client that the database quota for the origin has been exceeded. - * @param url The URL that triggered the notification - * @param databaseIdentifier The identifier of the database that caused the - * quota overflow. - * @param currentQuota The current quota for the origin. - * @param estimatedSize The estimated size of the database. - * @param totalUsedQuota is the sum of all origins' quota. - * @param quotaUpdater A callback to inform the WebCore thread that a new - * quota is available. This callback must always be executed at some - * point to ensure that the sleeping WebCore thread is woken up. + * Tell the client that the quota has been exceeded for the Web SQL Database + * API for a particular origin and request a new quota. The client must + * respond by invoking the + * {@link WebStorage.QuotaUpdater#updateQuota(long) updateQuota(long)} + * method of the supplied {@link WebStorage.QuotaUpdater} instance. The + * minimum value that can be set for the new quota is the current quota. The + * default implementation responds with the current quota, so the quota will + * not be increased. + * @param url The URL of the page that triggered the notification + * @param databaseIdentifier The identifier of the database where the quota + * was exceeded. + * @param quota The quota for the origin, in bytes + * @param estimatedDatabaseSize The estimated size of the offending + * database, in bytes + * @param totalQuota The total quota for all origins, in bytes + * @param quotaUpdater An instance of {@link WebStorage.QuotaUpdater} which + * must be used to inform the WebView of the new quota. */ + // Note that the callback must always be executed at some point to ensure + // that the sleeping WebCore thread is woken up. public void onExceededDatabaseQuota(String url, String databaseIdentifier, - long currentQuota, long estimatedSize, long totalUsedQuota, - WebStorage.QuotaUpdater quotaUpdater) { + long quota, long estimatedDatabaseSize, long totalQuota, + WebStorage.QuotaUpdater quotaUpdater) { // This default implementation passes the current quota back to WebCore. // WebCore will interpret this that new quota was declined. - quotaUpdater.updateQuota(currentQuota); + quotaUpdater.updateQuota(quota); } /** - * Tell the client that the Application Cache has exceeded its max size. - * @param spaceNeeded is the amount of disk space that would be needed - * in order for the last appcache operation to succeed. - * @param totalUsedQuota is the sum of all origins' quota. - * @param quotaUpdater A callback to inform the WebCore thread that a new - * app cache size is available. This callback must always be executed at - * some point to ensure that the sleeping WebCore thread is woken up. + * Tell the client that the quota has been reached for the Application Cache + * API and request a new quota. The client must respond by invoking the + * {@link WebStorage.QuotaUpdater#updateQuota(long) updateQuota(long)} + * method of the supplied {@link WebStorage.QuotaUpdater} instance. The + * minimum value that can be set for the new quota is the current quota. The + * default implementation responds with the current quota, so the quota will + * not be increased. + * @param requiredStorage The amount of storage required by the Application + * Cache operation that triggered this notification, + * in bytes. + * @param quota The quota, in bytes + * @param quotaUpdater An instance of {@link WebStorage.QuotaUpdater} which + * must be used to inform the WebView of the new quota. */ - public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, + // Note that the callback must always be executed at some point to ensure + // that the sleeping WebCore thread is woken up. + public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) { - quotaUpdater.updateQuota(0); + quotaUpdater.updateQuota(quota); } /** @@ -346,9 +363,11 @@ public class WebChromeClient { * onReceiveValue must be called to wake up the thread.a * @param acceptType The value of the 'accept' attribute of the input tag * associated with this file picker. + * @param capture The value of the 'capture' attribute of the input tag + * associated with this file picker. * @hide */ - public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType) { + public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) { uploadFile.onReceiveValue(null); } diff --git a/core/java/android/webkit/WebIconDatabase.java b/core/java/android/webkit/WebIconDatabase.java index 54dfab3..9299b71 100644 --- a/core/java/android/webkit/WebIconDatabase.java +++ b/core/java/android/webkit/WebIconDatabase.java @@ -35,7 +35,7 @@ import java.util.Vector; * WebIconDatabase object is a single instance and all methods operate on that * single object. */ -public final class WebIconDatabase { +public class WebIconDatabase { private static final String LOGTAG = "WebIconDatabase"; // Global instance of a WebIconDatabase private static WebIconDatabase sIconDatabase; diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index cddd7ab..105285c 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -170,45 +170,62 @@ public class WebSettings { } /** - * Set whether the WebView supports zoom + * Sets whether the WebView should support zooming using its on-screen zoom + * controls and gestures. The particular zoom mechanisms that should be used + * can be set with {@link #setBuiltInZoomControls}. This setting does not + * affect zooming performed using the {@link WebView#zoomIn()} and + * {@link WebView#zoomOut()} methods. + * @param support Whether the WebView should support zoom. */ public void setSupportZoom(boolean support) { throw new MustOverrideException(); } /** - * Returns whether the WebView supports zoom + * Returns true if the WebView supports zoom. The default is true. + * @return True if the WebView supports zoom. */ public boolean supportZoom() { throw new MustOverrideException(); } /** - * Sets whether the zoom mechanism built into WebView is used. + * Sets whether the WebView should use its built-in zoom mechanisms, as + * opposed to separate zoom controls. The built-in zoom mechanisms comprise + * on-screen zoom controls, which are displayed over the WebView's content, + * and the use of a pinch gesture to control zooming. Whether or not these + * on-screen controls are displayed can be set with {@link #setDisplayZoomControls}. + * The separate zoom controls are no longer supported, so it is recommended + * that this setting is always enabled. + * @param enabled Whether the WebView should use the built-in zoom mechanism. */ public void setBuiltInZoomControls(boolean enabled) { throw new MustOverrideException(); } /** - * Returns true if the zoom mechanism built into WebView is being used. + * Returns true if the zoom mechanisms built into WebView are being used. + * The default is false. + * @return True if the zoom mechanisms built into WebView are being used. */ public boolean getBuiltInZoomControls() { throw new MustOverrideException(); } /** - * Sets whether the on screen zoom buttons are used. - * A combination of built in zoom controls enabled - * and on screen zoom controls disabled allows for pinch to zoom - * to work without the on screen controls + * Sets whether the WebView should display on-screen zoom controls when + * using the built-in zoom mechanisms. See {@link #setBuiltInZoomControls}. + * @param enabled Whether the WebView should display on-screen zoom controls. */ public void setDisplayZoomControls(boolean enabled) { throw new MustOverrideException(); } /** - * Returns true if the on screen zoom buttons are being used. + * Returns true if the WebView displays on-screen zoom controls when using + * the built-in zoom mechanisms. The default is true. + * @return True if the WebView displays on-screen zoom controls when using + * the built-in zoom mechanisms. */ public boolean getDisplayZoomControls() { throw new MustOverrideException(); diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java index 2300c2e..041791b 100644 --- a/core/java/android/webkit/WebStorage.java +++ b/core/java/android/webkit/WebStorage.java @@ -25,19 +25,34 @@ import java.util.Map; import java.util.Set; /** - * Functionality for manipulating the webstorage databases. + * This class is used to manage the JavaScript storage APIs provided by the + * {@link WebView}. It manages the Application Cache API, the Web SQL Database + * API and the HTML5 Web Storage API. + * + * The Web SQL Database API provides storage which is private to a given + * origin, where an origin comprises the host, scheme and port of a URI. + * Similarly, use of the Application Cache API can be attributed to an origin. + * This class provides access to the storage use and quotas for these APIs for + * a given origin. Origins are represented using {@link WebStorage.Origin}. */ -public final class WebStorage { +public class WebStorage { /** - * Encapsulates a callback function to be executed when a new quota is made - * available. We primarily want this to allow us to call back the sleeping - * WebCore thread from outside the WebViewCore class (as the native call - * is private). It is imperative that this the setDatabaseQuota method is - * executed once a decision to either allow or deny new quota is made, - * otherwise the WebCore thread will remain asleep. + * Encapsulates a callback function which is used to provide a new quota + * for a JavaScript storage API. See + * {@link WebChromeClient#onExceededDatabaseQuota} and + * {@link WebChromeClient#onReachedMaxAppCacheSize}. */ + // We primarily want this to allow us to call back the sleeping WebCore + // thread from outside the WebViewCore class (as the native call is + // private). It is imperative that the setDatabaseQuota method is + // executed after a decision to either allow or deny new quota is made, + // otherwise the WebCore thread will remain asleep. public interface QuotaUpdater { + /** + * Provide a new quota, specified in bytes. + * @param newQuota The new quota, in bytes + */ public void updateQuota(long newQuota); }; @@ -70,7 +85,9 @@ public final class WebStorage { private Handler mUIHandler = null; /** - * Class containing the HTML5 database quota and usage for an origin. + * This class encapsulates information about the amount of storage + * currently used by an origin for the JavaScript storage APIs. + * See {@link WebStorage} for details. */ public static class Origin { private String mOrigin = null; @@ -93,28 +110,32 @@ public final class WebStorage { } /** - * An origin string is created using WebCore::SecurityOrigin::toString(). - * Note that WebCore::SecurityOrigin uses 0 (which is not printed) for - * the port if the port is the default for the protocol. Eg - * http://www.google.com and http://www.google.com:80 both record a port - * of 0 and hence toString() == 'http://www.google.com' for both. - * @return The origin string. + * Get the string representation of this origin. + * @return The string representation of this origin */ + // An origin string is created using WebCore::SecurityOrigin::toString(). + // Note that WebCore::SecurityOrigin uses 0 (which is not printed) for + // the port if the port is the default for the protocol. Eg + // http://www.google.com and http://www.google.com:80 both record a port + // of 0 and hence toString() == 'http://www.google.com' for both. public String getOrigin() { return mOrigin; } /** - * Returns the quota for this origin's HTML5 database. - * @return The quota in bytes. + * Get the quota for this origin, for the Web SQL Database API, in + * bytes. If this origin does not use the Web SQL Database API, this + * quota will be set to zero. + * @return The quota, in bytes. */ public long getQuota() { return mQuota; } /** - * Returns the usage for this origin's HTML5 database. - * @return The usage in bytes. + * Get the total amount of storage currently being used by this origin, + * for all JavaScript storage APIs, in bytes. + * @return The total amount of storage, in bytes. */ public long getUsage() { return mUsage; @@ -122,8 +143,8 @@ public final class WebStorage { } /** - * @hide * Message handler, UI side + * @hide */ public void createUIHandler() { if (mUIHandler == null) { @@ -156,8 +177,8 @@ public final class WebStorage { } /** + * Message handler, WebCore side * @hide - * Message handler, webcore side */ public synchronized void createHandler() { if (mHandler == null) { @@ -231,19 +252,22 @@ public final class WebStorage { /* * When calling getOrigins(), getUsageForOrigin() and getQuotaForOrigin(), - * we need to get the values from webcore, but we cannot block while doing so - * as we used to do, as this could result in a full deadlock (other webcore + * we need to get the values from WebCore, but we cannot block while doing so + * as we used to do, as this could result in a full deadlock (other WebCore * messages received while we are still blocked here, see http://b/2127737). * * We have to do everything asynchronously, by providing a callback function. - * We post a message on the webcore thread (mHandler) that will get the result - * from webcore, and we post it back on the UI thread (using mUIHandler). + * We post a message on the WebCore thread (mHandler) that will get the result + * from WebCore, and we post it back on the UI thread (using mUIHandler). * We can then use the callback function to return the value. */ /** - * Returns a list of origins having a database. The Map is of type - * Map<String, Origin>. + * Get the origins currently using either the Application Cache or Web SQL + * Database APIs. This method operates asynchronously, with the result + * being provided via a {@link ValueCallback}. The origins are provided as + * a map, of type {@code Map<String, WebStorage.Origin>}, from the string + * representation of the origin to a {@link WebStorage.Origin} object. */ public void getOrigins(ValueCallback<Map> callback) { if (callback != null) { @@ -269,7 +293,11 @@ public final class WebStorage { } /** - * Returns the use for a given origin + * Get the amount of storage currently being used by both the Application + * Cache and Web SQL Database APIs by the given origin. The amount is given + * in bytes and the origin is specified using its string representation. + * This method operates asynchronously, with the result being provided via + * a {@link ValueCallback}. */ public void getUsageForOrigin(String origin, ValueCallback<Long> callback) { if (callback == null) { @@ -292,7 +320,11 @@ public final class WebStorage { } /** - * Returns the quota for a given origin + * Get the storage quota for the Web SQL Database API for the given origin. + * The quota is given in bytes and the origin is specified using its string + * representation. This method operates asynchronously, with the result + * being provided via a {@link ValueCallback}. Note that a quota is not + * enforced on a per-origin basis for the Application Cache API. */ public void getQuotaForOrigin(String origin, ValueCallback<Long> callback) { if (callback == null) { @@ -315,7 +347,10 @@ public final class WebStorage { } /** - * Set the quota for a given origin + * Set the storage quota for the Web SQL Database API for the given origin. + * The quota is specified in bytes and the origin is specified using its string + * representation. Note that a quota is not enforced on a per-origin basis + * for the Application Cache API. */ public void setQuotaForOrigin(String origin, long quota) { if (origin != null) { @@ -329,7 +364,9 @@ public final class WebStorage { } /** - * Delete a given origin + * Clear the storage currently being used by both the Application Cache and + * Web SQL Database APIs by the given origin. The origin is specified using + * its string representation. */ public void deleteOrigin(String origin) { if (origin != null) { @@ -343,7 +380,9 @@ public final class WebStorage { } /** - * Delete all databases + * Clear all storage currently being used by the JavaScript storage APIs. + * This includes the Application Cache, Web SQL Database and the HTML5 Web + * Storage APIs. */ public void deleteAllData() { if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { @@ -381,8 +420,8 @@ public final class WebStorage { } /** - * Get the global instance of WebStorage. - * @return A single instance of WebStorage. + * Get the singleton instance of this class. + * @return The singleton {@link WebStorage} instance. */ public static WebStorage getInstance() { if (sWebStorage == null) { @@ -404,7 +443,7 @@ public final class WebStorage { } /** - * Run on the webcore thread + * Run on the WebCore thread * set the local values with the current ones */ private void syncValues() { @@ -418,6 +457,16 @@ public final class WebStorage { } } + /** + * This class should not be instantiated directly, applications must only use + * {@link #getInstance()} to obtain the instance. + * Note this constructor was erroneously public and published in SDK levels prior to 16, but + * applications using it would receive a non-functional instance of this class (there was no + * way to call createHandler() and createUIHandler(), so it would not work). + * @hide + */ + public WebStorage() {} + // Native functions private static native Set nativeGetOrigins(); private static native long nativeGetUsageForOrigin(String origin); diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index d1cfc6b..5498622 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -1281,8 +1281,10 @@ public class WebView extends AbsoluteLayout * @deprecated {@link #findAllAsync} is preferred. * @see #setFindListener */ + @Deprecated public int findAll(String find) { checkThread(); + StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); return mProvider.findAll(find); } diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index 9895a87..893849b 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -828,6 +828,13 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // if AUTO_REDRAW_HACK is true, then the CALL key will toggle redrawing // the screen all-the-time. Good for profiling our drawing code static private final boolean AUTO_REDRAW_HACK = false; + + // The rate at which edit text is scrolled in content pixels per millisecond + static private final float TEXT_SCROLL_RATE = 0.01f; + + // The presumed scroll rate for the first scroll of edit text + static private final long TEXT_SCROLL_FIRST_SCROLL_MS = 16; + // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; @@ -853,6 +860,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc boolean mIsEditingText = false; ArrayList<Message> mBatchedTextChanges = new ArrayList<Message>(); boolean mIsBatchingTextChanges = false; + private long mLastEditScroll = 0; private static class OnTrimMemoryListener implements ComponentCallbacks2 { private static OnTrimMemoryListener sInstance = null; @@ -1037,9 +1045,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // pages with the space bar, in pixels. private static final int PAGE_SCROLL_OVERLAP = 24; - // Time between successive calls to text scroll fling animation - private static final int TEXT_SCROLL_ANIMATION_DELAY_MS = 16; - /** * These prevent calling requestLayout if either dimension is fixed. This * depends on the layout parameters and the measure specs. @@ -1207,7 +1212,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc static final int RELOCATE_AUTO_COMPLETE_POPUP = 146; static final int FOCUS_NODE_CHANGED = 147; static final int AUTOFILL_FORM = 148; - static final int ANIMATE_TEXT_SCROLL = 149; + static final int SCROLL_EDIT_TEXT = 149; static final int EDIT_TEXT_SIZE_CHANGED = 150; static final int SHOW_CARET_HANDLE = 151; static final int UPDATE_CONTENT_BOUNDS = 152; @@ -4672,6 +4677,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc * Select the word at the indicated content coordinates. */ boolean selectText(int x, int y) { + if (mWebViewCore == null) { + return false; + } mWebViewCore.sendMessage(EventHub.SELECT_WORD_AT, x, y); return true; } @@ -6002,9 +6010,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc data.mNativeLayer = nativeScrollableLayer( contentX, contentY, data.mNativeLayerRect, null); data.mSlop = viewToContentDimension(mNavSlop); - mTouchHighlightRegion.setEmpty(); + removeTouchHighlight(); if (!mBlockWebkitViewMessages) { - mTouchHighlightRequested = System.currentTimeMillis(); + mTouchHighlightRequested = SystemClock.uptimeMillis(); mWebViewCore.sendMessageAtFrontOfQueue( EventHub.HIT_TEST, data); } @@ -6091,6 +6099,11 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mSelectDraggingTextQuad.containsPoint(handleX, handleY); boolean inEditBounds = mEditTextContentBounds .contains(handleX, handleY); + if (mIsEditingText && !inEditBounds) { + beginScrollEdit(); + } else { + endScrollEdit(); + } if (inCursorText || (mIsEditingText && !inEditBounds)) { snapDraggingCursor(); } @@ -6240,6 +6253,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc break; } case MotionEvent.ACTION_UP: { + endScrollEdit(); if (!mConfirmMove && mIsEditingText && mSelectionStarted && mIsCaretSelection) { showPasteWindow(); @@ -6335,6 +6349,86 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc } } + /** + * Returns the text scroll speed in content pixels per millisecond based on + * the touch location. + * @param coordinate The x or y touch coordinate in content space + * @param min The minimum coordinate (x or y) of the edit content bounds + * @param max The maximum coordinate (x or y) of the edit content bounds + */ + private static float getTextScrollSpeed(int coordinate, int min, int max) { + if (coordinate < min) { + return (coordinate - min) * TEXT_SCROLL_RATE; + } else if (coordinate >= max) { + return (coordinate - max + 1) * TEXT_SCROLL_RATE; + } else { + return 0.0f; + } + } + + private void beginScrollEdit() { + if (mLastEditScroll == 0) { + mLastEditScroll = SystemClock.uptimeMillis() - + TEXT_SCROLL_FIRST_SCROLL_MS; + scrollEditWithCursor(); + } + } + + private void endScrollEdit() { + mLastEditScroll = 0; + } + + private static int getTextScrollDelta(float speed, long deltaT) { + float distance = speed * deltaT; + int intDistance = (int)Math.floor(distance); + float probability = distance - intDistance; + if (Math.random() < probability) { + intDistance++; + } + return intDistance; + } + /** + * Scrolls edit text a distance based on the last touch point, + * the last scroll time, and the edit text content bounds. + */ + private void scrollEditWithCursor() { + if (mLastEditScroll != 0) { + int x = viewToContentX(mLastTouchX + getScrollX() + mSelectDraggingOffset.x); + float scrollSpeedX = getTextScrollSpeed(x, mEditTextContentBounds.left, + mEditTextContentBounds.right); + int y = viewToContentY(mLastTouchY + getScrollY() + mSelectDraggingOffset.y); + float scrollSpeedY = getTextScrollSpeed(y, mEditTextContentBounds.top, + mEditTextContentBounds.bottom); + if (scrollSpeedX == 0.0f && scrollSpeedY == 0.0f) { + endScrollEdit(); + } else { + long currentTime = SystemClock.uptimeMillis(); + long timeSinceLastUpdate = currentTime - mLastEditScroll; + int deltaX = getTextScrollDelta(scrollSpeedX, timeSinceLastUpdate); + int deltaY = getTextScrollDelta(scrollSpeedY, timeSinceLastUpdate); + mLastEditScroll = currentTime; + if (deltaX == 0 && deltaY == 0) { + // By probability no text scroll this time. Try again later. + mPrivateHandler.sendEmptyMessageDelayed(SCROLL_EDIT_TEXT, + TEXT_SCROLL_FIRST_SCROLL_MS); + } else { + int scrollX = getTextScrollX() + deltaX; + scrollX = Math.min(getMaxTextScrollX(), scrollX); + scrollX = Math.max(0, scrollX); + int scrollY = getTextScrollY() + deltaY; + scrollY = Math.min(getMaxTextScrollY(), scrollY); + scrollY = Math.max(0, scrollY); + scrollEditText(scrollX, scrollY); + int cursorX = mSelectDraggingCursor.x; + int cursorY = mSelectDraggingCursor.y; + mSelectDraggingCursor.set(x - deltaX, y - deltaY); + updateWebkitSelection(); + mSelectDraggingCursor.set(cursorX, cursorY); + } + } + } + } + private void startTouch(float x, float y, long eventTime) { // Remember where the motion event started mStartTouchX = mLastTouchX = Math.round(x); @@ -7053,7 +7147,8 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc if (mFindIsUp) return false; boolean result = false; result = mWebViewPrivate.super_requestFocus(direction, previouslyFocusedRect); - if (mWebViewCore.getSettings().getNeedInitialFocus() && !mWebView.isInTouchMode()) { + if (mWebViewCore.getSettings().getNeedInitialFocus() + && !mWebView.isInTouchMode()) { // For cases such as GMail, where we gain focus from a direction, // we want to move to the first available link. // FIXME: If there are no visible links, we may not want to @@ -7074,7 +7169,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc default: return result; } - // TODO: Send initial focus request to webkit (b/6108927) + mWebViewCore.sendMessage(EventHub.SET_INITIAL_FOCUS, fakeKeyDirection); } return result; } @@ -7673,10 +7768,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc msg.arg1, /* unused */0); break; - case ANIMATE_TEXT_SCROLL: - computeEditTextScroll(); - break; - case EDIT_TEXT_SIZE_CHANGED: if (msg.arg1 == mFieldPointer) { mEditTextContent.set((Rect)msg.obj); @@ -7695,6 +7786,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mEditTextContentBounds.set((Rect) msg.obj); break; + case SCROLL_EDIT_TEXT: + scrollEditWithCursor(); + break; + default: super.handleMessage(msg); break; @@ -7777,13 +7872,16 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc if (mFocusedNode.mHasFocus && mFocusedNode.mEditable) { return false; } - long delay = System.currentTimeMillis() - mTouchHighlightRequested; + long delay = SystemClock.uptimeMillis() - mTouchHighlightRequested; if (delay < ViewConfiguration.getTapTimeout()) { Rect r = mTouchHighlightRegion.getBounds(); mWebView.postInvalidateDelayed(delay, r.left, r.top, r.right, r.bottom); return false; } - return true; + if (mInputDispatcher == null) { + return false; + } + return mInputDispatcher.shouldShowTapHighlight(); } @@ -7893,8 +7991,8 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc if (viewRect.width() < getWidth() >> 1 || viewRect.height() < getHeight() >> 1) { mTouchHighlightRegion.union(viewRect); - } else { - Log.w(LOGTAG, "Skip the huge selection rect:" + } else if (DebugFlags.WEB_VIEW) { + Log.d(LOGTAG, "Skip the huge selection rect:" + viewRect); } } @@ -7995,16 +8093,17 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mWebView.invalidate(); } - if (mPictureListener != null) { - mPictureListener.onNewPicture(getWebView(), capturePicture()); - } - // update the zoom information based on the new picture mZoomManager.onNewPicture(draw); if (isPictureAfterFirstLayout) { mViewManager.postReadyToDrawAll(); } + scrollEditWithCursor(); + + if (mPictureListener != null) { + mPictureListener.onNewPicture(getWebView(), capturePicture()); + } } /** @@ -8047,13 +8146,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc invalidate(); } - private void computeEditTextScroll() { - if (mEditTextScroller.computeScrollOffset()) { - scrollEditText(mEditTextScroller.getCurrX(), - mEditTextScroller.getCurrY()); - } - } - private void scrollEditText(int scrollX, int scrollY) { // Scrollable edit text. Scroll it. float maxScrollX = getMaxTextScrollX(); @@ -8061,8 +8153,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mEditTextContent.offsetTo(-scrollX, -scrollY); mWebViewCore.sendMessageAtFrontOfQueue(EventHub.SCROLL_TEXT_INPUT, 0, scrollY, (Float)scrollPercentX); - mPrivateHandler.sendEmptyMessageDelayed(ANIMATE_TEXT_SCROLL, - TEXT_SCROLL_ANIMATION_DELAY_MS); } private void beginTextBatch() { diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 15a2d48..e2880d6 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -394,10 +394,12 @@ public final class WebViewCore { * Called by JNI. Open a file chooser to upload a file. * @param acceptType The value of the 'accept' attribute of the * input tag associated with this file picker. + * @param capture The value of the 'capture' attribute of the + * input tag associated with this file picker. * @return String version of the URI. */ - private String openFileChooser(String acceptType) { - Uri uri = mCallbackProxy.openFileChooser(acceptType); + private String openFileChooser(String acceptType, String capture) { + Uri uri = mCallbackProxy.openFileChooser(acceptType, capture); if (uri != null) { String filePath = ""; // Note - querying for MediaStore.Images.Media.DATA @@ -428,38 +430,38 @@ public final class WebViewCore { * Notify the browser that the origin has exceeded it's database quota. * @param url The URL that caused the overflow. * @param databaseIdentifier The identifier of the database. - * @param currentQuota The current quota for the origin. - * @param estimatedSize The estimated size of the database. + * @param quota The current quota for the origin. + * @param estimatedDatabaseSize The estimated size of the database. */ protected void exceededDatabaseQuota(String url, String databaseIdentifier, - long currentQuota, - long estimatedSize) { + long quota, + long estimatedDatabaseSize) { // Inform the callback proxy of the quota overflow. Send an object // that encapsulates a call to the nativeSetDatabaseQuota method to // awaken the sleeping webcore thread when a decision from the // client to allow or deny quota is available. mCallbackProxy.onExceededDatabaseQuota(url, databaseIdentifier, - currentQuota, estimatedSize, getUsedQuota(), + quota, estimatedDatabaseSize, getUsedQuota(), new WebStorage.QuotaUpdater() { @Override - public void updateQuota(long quota) { - nativeSetNewStorageLimit(mNativeClass, quota); + public void updateQuota(long newQuota) { + nativeSetNewStorageLimit(mNativeClass, newQuota); } }); } /** * Notify the browser that the appcache has exceeded its max size. - * @param spaceNeeded is the amount of disk space that would be needed - * in order for the last appcache operation to succeed. + * @param requiredStorage is the amount of storage, in bytes, that would be + * needed in order for the last appcache operation to succeed. */ - protected void reachedMaxAppCacheSize(long spaceNeeded) { - mCallbackProxy.onReachedMaxAppCacheSize(spaceNeeded, getUsedQuota(), + protected void reachedMaxAppCacheSize(long requiredStorage) { + mCallbackProxy.onReachedMaxAppCacheSize(requiredStorage, getUsedQuota(), new WebStorage.QuotaUpdater() { @Override - public void updateQuota(long quota) { - nativeSetNewStorageLimit(mNativeClass, quota); + public void updateQuota(long newQuota) { + nativeSetNewStorageLimit(mNativeClass, newQuota); } }); } @@ -1174,6 +1176,7 @@ public final class WebViewCore { // key was pressed (down and up) static final int KEY_PRESS = 223; + static final int SET_INITIAL_FOCUS = 224; // Private handler for WebCore messages. private Handler mHandler; @@ -1746,6 +1749,9 @@ public final class WebViewCore { WebViewClassic.UPDATE_MATCH_COUNT, request).sendToTarget(); break; } + case SET_INITIAL_FOCUS: + nativeSetInitialFocus(mNativeClass, msg.arg1); + break; } } }; @@ -3069,6 +3075,7 @@ public final class WebViewCore { private native void nativeClearTextSelection(int nativeClass); private native boolean nativeSelectWordAt(int nativeClass, int x, int y); private native void nativeSelectAll(int nativeClass); + private native void nativeSetInitialFocus(int nativeClass, int keyDirection); private static native void nativeCertTrustChanged(); } diff --git a/core/java/android/webkit/WebViewInputDispatcher.java b/core/java/android/webkit/WebViewInputDispatcher.java index e7024d9..0a0afaa 100644 --- a/core/java/android/webkit/WebViewInputDispatcher.java +++ b/core/java/android/webkit/WebViewInputDispatcher.java @@ -269,9 +269,8 @@ final class WebViewInputDispatcher { */ public boolean postPointerEvent(MotionEvent event, int webKitXOffset, int webKitYOffset, float webKitScale) { - if (event == null - || (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) { - throw new IllegalArgumentException("event must be a pointer event"); + if (event == null) { + throw new IllegalArgumentException("event cannot be null"); } if (DEBUG) { @@ -349,6 +348,12 @@ final class WebViewInputDispatcher { } } + public boolean shouldShowTapHighlight() { + synchronized (mLock) { + return mPostLongPressScheduled || mPostClickScheduled; + } + } + private void postLongPress() { synchronized (mLock) { if (!mPostLongPressScheduled) { diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 057aabe..e68049c 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2062,6 +2062,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te child = mAdapter.getView(position, scrapView, this); + if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW, position, getChildCount()); @@ -2082,6 +2086,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } else { child = mAdapter.getView(position, null, this); + + if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index efdfae3..f279f8e 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -191,6 +191,10 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { if (view == null) { // Make a new one view = mAdapter.getView(selectedPosition, null, this); + + if (view.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } if (view != null) { diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 97a864c..abfc577 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -24,14 +24,15 @@ import android.util.AttributeSet; import android.util.SparseArray; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; - /** * An AdapterView is a view whose children are determined by an {@link Adapter}. * @@ -232,6 +233,11 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { public AdapterView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } /** @@ -643,6 +649,12 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { public void setEmptyView(View emptyView) { mEmptyView = emptyView; + // If not explicitly specified this view is important for accessibility. + if (emptyView != null + && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + final T adapter = getAdapter(); final boolean empty = ((adapter == null) || adapter.isEmpty()); updateEmptyStatus(empty); @@ -846,12 +858,14 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } } else { fireOnSelected(); + performAccessibilityActionsOnSelected(); } } } void selectionChanged() { - if (mOnItemSelectedListener != null) { + if (mOnItemSelectedListener != null + || AccessibilityManager.getInstance(mContext).isEnabled()) { if (mInLayout || mBlockLayoutRequests) { // If we are in a layout traversal, defer notification // by posting. This ensures that the view tree is @@ -863,20 +877,16 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { post(mSelectionNotifier); } else { fireOnSelected(); + performAccessibilityActionsOnSelected(); } } - - // we fire selection events here not in View - if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } } private void fireOnSelected() { - if (mOnItemSelectedListener == null) + if (mOnItemSelectedListener == null) { return; - - int selection = this.getSelectedItemPosition(); + } + final int selection = getSelectedItemPosition(); if (selection >= 0) { View v = getSelectedView(); mOnItemSelectedListener.onItemSelected(this, v, selection, @@ -886,6 +896,17 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } } + private void performAccessibilityActionsOnSelected() { + if (!AccessibilityManager.getInstance(mContext).isEnabled()) { + return; + } + final int position = getSelectedItemPosition(); + if (position >= 0) { + // we fire selection events here not in View + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + } + @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { View selectedView = getSelectedView(); @@ -936,6 +957,24 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { event.setItemCount(getCount()); } + /** + * @hide + */ + @Override + public boolean onRequestAccessibilityFocusFromHover(float x, float y) { + // We prefer to five focus to the child instead of this view. + // Usually the children are not actionable for accessibility, + // and they will not take accessibility focus, so we give it. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (isTransformedTouchPointInView(x, y, child, null)) { + return child.requestAccessibilityFocus(); + } + } + return super.onRequestAccessibilityFocusFromHover(x, y); + } + private boolean isScrollableForAccessibility() { T adapter = getAdapter(); if (adapter != null) { @@ -1012,6 +1051,9 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { mNeedSync = false; checkSelectionChanged(); } + + //TODO: Hmm, we do not know the old state so this is sub-optimal + notifyAccessibilityStateChanged(); } void checkSelectionChanged() { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index bb00049..c557963 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -414,6 +414,10 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> // get the fresh child from the adapter final View updatedChild = mAdapter.getView(modulo(i, adapterCount), null, this); + if (updatedChild.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + updatedChild.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (mViewsMap.containsKey(index)) { final FrameLayout fl = (FrameLayout) mViewsMap.get(index).view; // add the new child to the frame, if it exists diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java index 0685eea..858c415 100644 --- a/core/java/android/widget/CheckBox.java +++ b/core/java/android/widget/CheckBox.java @@ -21,8 +21,6 @@ import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import com.android.internal.R; - /** * <p> @@ -71,16 +69,6 @@ public class CheckBox extends CompoundButton { } @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); - if (isChecked()) { - event.getText().add(mContext.getString(R.string.checkbox_checked)); - } else { - event.getText().add(mContext.getString(R.string.checkbox_not_checked)); - } - } - - @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(CheckBox.class.getName()); diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index c5066b6..108b720 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -279,8 +279,13 @@ public class DatePicker extends FrameLayout { // re-order the number spinners to match the current date format reorderSpinners(); - // set content descriptions + // accessibility setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } /** diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index cbff58c..040a385 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -1681,7 +1681,7 @@ public class Editor { final int itemCount = clipData.getItemCount(); for (int i=0; i < itemCount; i++) { Item item = clipData.getItemAt(i); - content.append(item.coerceToText(mTextView.getContext())); + content.append(item.coerceToStyledText(mTextView.getContext())); } final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 60dd55c..1cb676f 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Insets; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; @@ -559,9 +560,9 @@ public class GridLayout extends ViewGroup { int flags = (gravity & mask) >> shift; switch (flags) { case (AXIS_SPECIFIED | AXIS_PULL_BEFORE): - return LEADING; + return horizontal ? LEFT : TOP; case (AXIS_SPECIFIED | AXIS_PULL_AFTER): - return TRAILING; + return horizontal ? RIGHT : BOTTOM; case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER): return FILL; case AXIS_SPECIFIED: @@ -1042,12 +1043,15 @@ public class GridLayout extends ViewGroup { int rightMargin = getMargin(c, true, false); int bottomMargin = getMargin(c, false, false); + int sumMarginsX = leftMargin + rightMargin; + int sumMarginsY = topMargin + bottomMargin; + // Alignment offsets: the location of the view relative to its alignment group. - int alignmentOffsetX = boundsX.getOffset(c, hAlign, leftMargin + pWidth + rightMargin); - int alignmentOffsetY = boundsY.getOffset(c, vAlign, topMargin + pHeight + bottomMargin); + int alignmentOffsetX = boundsX.getOffset(this, c, hAlign, pWidth + sumMarginsX, true); + int alignmentOffsetY = boundsY.getOffset(this, c, vAlign, pHeight + sumMarginsY, false); - int width = hAlign.getSizeInCell(c, pWidth, cellWidth - leftMargin - rightMargin); - int height = vAlign.getSizeInCell(c, pHeight, cellHeight - topMargin - bottomMargin); + int width = hAlign.getSizeInCell(c, pWidth, cellWidth - sumMarginsX); + int height = vAlign.getSizeInCell(c, pHeight, cellHeight - sumMarginsY); int dx = x1 + gravityOffsetX + alignmentOffsetX; @@ -1181,7 +1185,7 @@ public class GridLayout extends ViewGroup { View c = getChildAt(i); LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; - groupBounds.getValue(i).include(c, spec, GridLayout.this, this); + groupBounds.getValue(i).include(GridLayout.this, c, spec, this); } } @@ -2138,16 +2142,30 @@ public class GridLayout extends ViewGroup { return before + after; } - protected int getOffset(View c, Alignment alignment, int size) { - return before - alignment.getAlignmentValue(c, size); + private int getAlignmentValue(GridLayout gl, View c, int size, Alignment a, boolean horiz) { + boolean useLayoutBounds = gl.getLayoutMode() == LAYOUT_BOUNDS; + if (!useLayoutBounds) { + return a.getAlignmentValue(c, size); + } else { + Insets insets = c.getLayoutInsets(); + int leadingInset = horiz ? insets.left : insets.top; // RTL? + int trailingInset = horiz ? insets.right : insets.bottom; // RTL? + int totalInset = leadingInset + trailingInset; + return leadingInset + a.getAlignmentValue(c, size - totalInset); + } + } + + protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean horizontal) { + return before - getAlignmentValue(gl, c, size, a, horizontal); } - protected final void include(View c, Spec spec, GridLayout gridLayout, Axis axis) { + protected final void include(GridLayout gl, View c, Spec spec, Axis axis) { this.flexibility &= spec.getFlexibility(); - int size = gridLayout.getMeasurementIncludingMargin(c, axis.horizontal); - Alignment alignment = gridLayout.getAlignment(spec.alignment, axis.horizontal); + boolean horizontal = axis.horizontal; + int size = gl.getMeasurementIncludingMargin(c, horizontal); + Alignment alignment = gl.getAlignment(spec.alignment, horizontal); // todo test this works correctly when the returned value is UNDEFINED - int before = alignment.getAlignmentValue(c, size); + int before = getAlignmentValue(gl, c, size, alignment, horizontal); include(before, size - before); } @@ -2614,8 +2632,8 @@ public class GridLayout extends ViewGroup { } @Override - protected int getOffset(View c, Alignment alignment, int size) { - return max(0, super.getOffset(c, alignment, size)); + protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean hrz) { + return max(0, super.getOffset(gl, c, a, size, hrz)); } }; } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 91e2e49..6c7ea67 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -105,11 +105,11 @@ public class ImageView extends View { super(context); initImageView(); } - + public ImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - + public ImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initImageView(); diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index d897a39..992849d 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -112,8 +112,7 @@ public class NumberPicker extends LinearLayout { private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; /** - * The duration of scrolling to the next/previous value while snapping to - * a given position. + * The duration of scrolling while snapping to a given position. */ private static final int SNAP_SCROLL_DURATION = 300; @@ -579,7 +578,7 @@ public class NumberPicker extends LinearLayout { throw new IllegalArgumentException("minWidth > maxWidth"); } - mComputeMaxWidth = (mMaxWidth == Integer.MAX_VALUE); + mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED); attributesArray.recycle(); @@ -680,6 +679,11 @@ public class NumberPicker extends LinearLayout { mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); updateInputTextView(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } @Override @@ -771,6 +775,8 @@ public class NumberPicker extends LinearLayout { mLastDownEventTime = event.getEventTime(); mIngonreMoveEvents = false; mShowSoftInputOnTap = false; + // Make sure we wupport flinging inside scrollables. + getParent().requestDisallowInterceptTouchEvent(true); if (!mFlingScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); @@ -1096,12 +1102,7 @@ public class NumberPicker extends LinearLayout { * @see #setMaxValue(int) */ public void setValue(int value) { - if (mValue == value) { - return; - } setValueInternal(value, false); - initializeSelectorWheelIndices(); - invalidate(); } /** @@ -1498,6 +1499,8 @@ public class NumberPicker extends LinearLayout { if (notifyChange) { notifyChange(previous, current); } + initializeSelectorWheelIndices(); + invalidate(); } /** diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java index 22e9ef1..080b87d 100644 --- a/core/java/android/widget/ShareActionProvider.java +++ b/core/java/android/widget/ShareActionProvider.java @@ -80,16 +80,22 @@ public class ShareActionProvider extends ActionProvider { /** * Called when a share target has been selected. The client can - * decide whether to handle the intent or rely on the default - * behavior which is launching it. + * decide whether to perform some action before the sharing is + * actually performed. * <p> * <strong>Note:</strong> Modifying the intent is not permitted and * any changes to the latter will be ignored. * </p> + * <p> + * <strong>Note:</strong> You should <strong>not</strong> handle the + * intent here. This callback aims to notify the client that a + * sharing is being performed, so the client can update the UI + * if necessary. + * </p> * * @param source The source of the notification. * @param intent The intent for launching the chosen share target. - * @return Whether the client has handled the intent. + * @return The return result is ignored. Always return false for consistency. */ public boolean onShareTargetSelected(ShareActionProvider source, Intent intent); } @@ -308,7 +314,7 @@ public class ShareActionProvider extends ActionProvider { @Override public boolean onChooseActivity(ActivityChooserModel host, Intent intent) { if (mOnShareTargetSelectedListener != null) { - return mOnShareTargetSelectedListener.onShareTargetSelected( + mOnShareTargetSelectedListener.onShareTargetSelected( ShareActionProvider.this, intent); } return false; diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index aef8a34..36d1ee0 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -907,7 +907,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { public void onItemClick(AdapterView parent, View v, int position, long id) { Spinner.this.setSelection(position); if (mOnItemClickListener != null) { - Spinner.this.performItemClick(null, position, mAdapter.getItemId(position)); + Spinner.this.performItemClick(v, position, mAdapter.getItemId(position)); } dismiss(); } diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index a897cc3..0786909 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -806,5 +806,16 @@ public class Switch extends CompoundButton { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(Switch.class.getName()); + CharSequence switchText = isChecked() ? mTextOn : mTextOff; + if (!TextUtils.isEmpty(switchText)) { + CharSequence oldText = info.getText(); + if (TextUtils.isEmpty(oldText)) { + info.setText(switchText); + } else { + StringBuilder newText = new StringBuilder(); + newText.append(oldText).append(' ').append(switchText); + info.setText(newText); + } + } } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 9867e47..37d9db7 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -1105,6 +1105,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setLongClickable(longClickable); if (mEditor != null) mEditor.prepareCursorControllers(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -7710,7 +7715,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (clip != null) { boolean didFirst = false; for (int i=0; i<clip.getItemCount(); i++) { - CharSequence paste = clip.getItemAt(i).coerceToText(getContext()); + CharSequence paste = clip.getItemAt(i).coerceToStyledText(getContext()); if (paste != null) { if (!didFirst) { long minMax = prepareSpacesAroundPaste(min, max, paste); diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index bc88b62..18f7a91 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -251,6 +251,11 @@ public class TimePicker extends FrameLayout { // set the content descriptions setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } @Override |
