diff options
41 files changed, 3484 insertions, 1477 deletions
diff --git a/api/current.txt b/api/current.txt index fced7f17d39a..d0176a53390a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5640,6 +5640,7 @@ package android.content { field public static final int FLAG_GRANT_READ_URI_PERMISSION = 1; // 0x1 field public static final int FLAG_GRANT_WRITE_URI_PERMISSION = 2; // 0x2 field public static final int FLAG_INCLUDE_STOPPED_PACKAGES = 32; // 0x20 + field public static final int FLAG_RECEIVER_FOREGROUND = 268435456; // 0x10000000 field public static final int FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; // 0x40000000 field public static final int FLAG_RECEIVER_REPLACE_PENDING = 536870912; // 0x20000000 field public static final java.lang.String METADATA_DOCK_HOME = "android.dock_home"; diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 211be52e0622..a463a62c5045 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -17,8 +17,10 @@ package android.accessibilityservice; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; @@ -218,10 +220,17 @@ public abstract class AccessibilityService extends Service { private static final String LOG_TAG = "AccessibilityService"; - private AccessibilityServiceInfo mInfo; + interface Callbacks { + public void onAccessibilityEvent(AccessibilityEvent event); + public void onInterrupt(); + public void onServiceConnected(); + public void onSetConnectionId(int connectionId); + } private int mConnectionId; + private AccessibilityServiceInfo mInfo; + /** * Callback for {@link android.view.accessibility.AccessibilityEvent}s. * @@ -282,27 +291,49 @@ public abstract class AccessibilityService extends Service { */ @Override public final IBinder onBind(Intent intent) { - return new IEventListenerWrapper(this); + return new IEventListenerWrapper(this, getMainLooper(), new Callbacks() { + @Override + public void onServiceConnected() { + AccessibilityService.this.onServiceConnected(); + } + + @Override + public void onInterrupt() { + AccessibilityService.this.onInterrupt(); + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + AccessibilityService.this.onAccessibilityEvent(event); + } + + @Override + public void onSetConnectionId( int connectionId) { + mConnectionId = connectionId; + } + }); } /** * Implements the internal {@link IEventListener} interface to convert * incoming calls to it back to calls on an {@link AccessibilityService}. */ - class IEventListenerWrapper extends IEventListener.Stub + static class IEventListenerWrapper extends IEventListener.Stub implements HandlerCaller.Callback { + static final int NO_ID = -1; + 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 final HandlerCaller mCaller; - private final AccessibilityService mTarget; + private final Callbacks mCallback; - public IEventListenerWrapper(AccessibilityService context) { - mTarget = context; - mCaller = new HandlerCaller(context, this); + public IEventListenerWrapper(Context context, Looper looper, Callbacks callback) { + mCallback = callback; + mCaller = new HandlerCaller(context, looper, this); } public void setConnection(IAccessibilityServiceConnection connection, int connectionId) { @@ -326,12 +357,13 @@ public abstract class AccessibilityService extends Service { case DO_ON_ACCESSIBILITY_EVENT : AccessibilityEvent event = (AccessibilityEvent) message.obj; if (event != null) { - mTarget.onAccessibilityEvent(event); + AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event); + mCallback.onAccessibilityEvent(event); event.recycle(); } return; case DO_ON_INTERRUPT : - mTarget.onInterrupt(); + mCallback.onInterrupt(); return; case DO_SET_SET_CONNECTION : final int connectionId = message.arg1; @@ -340,12 +372,11 @@ public abstract class AccessibilityService extends Service { if (connection != null) { AccessibilityInteractionClient.getInstance().addConnection(connectionId, connection); - mConnectionId = connectionId; - mTarget.onServiceConnected(); + mCallback.onSetConnectionId(connectionId); + mCallback.onServiceConnected(); } else { AccessibilityInteractionClient.getInstance().removeConnection(connectionId); - mConnectionId = AccessibilityInteractionClient.NO_ID; - // TODO: Do we need a onServiceDisconnected callback? + mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); } return; default : diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index e53b31395bb1..c9468eb11f23 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -32,8 +32,13 @@ interface IAccessibilityServiceConnection { /** * Finds an {@link AccessibilityNodeInfo} by accessibility id. * - * @param accessibilityWindowId A unique window id. - * @param accessibilityNodeId A unique view id or virtual descendant id. + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#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 com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID} + * 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 threadId The id of the calling thread. @@ -46,57 +51,58 @@ interface IAccessibilityServiceConnection { /** * Finds {@link AccessibilityNodeInfo}s by View text. The match is case * insensitive containment. The search is performed in the window whose - * id is specified and starts from the View whose accessibility id is + * id is specified and starts from the node whose accessibility id is * specified. * - * @param text The searched text. - * @param accessibilityWindowId A unique window id. + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#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.View#NO_ID} 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 threadId The id of the calling thread. - * @return The current window scale, where zero means a failure. - */ - float findAccessibilityNodeInfosByText(String text, int accessibilityWindowId, - long accessibilityNodeId, int interractionId, - IAccessibilityInteractionConnectionCallback callback, long threadId); - - /** - * Finds {@link AccessibilityNodeInfo}s by View text. The match is case - * insensitive containment. The search is performed in the currently - * active window and start from the root View in the window. - * + * where to start the search. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID} + * to start from the root. * @param text The searched text. - * @param accessibilityId The id of the view from which to start searching. - * Use {@link android.view.View#NO_ID} 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 threadId The id of the calling thread. * @return The current window scale, where zero means a failure. */ - float findAccessibilityNodeInfosByTextInActiveWindow(String text, - int interactionId, IAccessibilityInteractionConnectionCallback callback, + float findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, + String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); /** - * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed - * in the currently active window and starts from the root View in the window. + * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in + * the window whose id is specified and starts from the node whose accessibility + * id is specified. * + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#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 com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID} + * to start from the root. * @param id The id of the node. * @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 findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, long threadId); + float findAccessibilityNodeInfoByViewId(int accessibilityWindowId, long accessibilityNodeId, + int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + long threadId); /** * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * - * @param accessibilityWindowId The id of the window. - * @param accessibilityNodeId A unique view id or virtual descendant id. + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#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 com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID} + * to start from the root. * @param action The action to perform. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java new file mode 100644 index 000000000000..9d48efc66dc3 --- /dev/null +++ b/core/java/android/accessibilityservice/UiTestAutomationBridge.java @@ -0,0 +1,418 @@ +/* + * 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.accessibilityservice; + +import android.accessibilityservice.AccessibilityService.Callbacks; +import android.accessibilityservice.AccessibilityService.IEventListenerWrapper; +import android.content.Context; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityManager; + +import com.android.internal.util.Predicate; + +import java.util.List; +import java.util.concurrent.TimeoutException; + +/** + * This class represents a bridge that can be used for UI test + * automation. It is responsible for connecting to the system, + * keeping track of the last accessibility event, and exposing + * window content querying APIs. This class is designed to be + * used from both an Android application and a Java program + * run from the shell. + * + * @hide + */ +public class UiTestAutomationBridge { + + private static final String LOG_TAG = UiTestAutomationBridge.class.getSimpleName(); + + public static final int ACTIVE_WINDOW_ID = -1; + + public static final long ROOT_NODE_ID = -1; + + private static final int TIMEOUT_REGISTER_SERVICE = 5000; + + private final Object mLock = new Object(); + + private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; + + private IEventListenerWrapper mListener; + + private AccessibilityEvent mLastEvent; + + private volatile boolean mWaitingForEventDelivery; + + private volatile boolean mUnprocessedEventAvailable; + + /** + * Gets the last received {@link AccessibilityEvent}. + * + * @return The event. + */ + public AccessibilityEvent getLastAccessibilityEvent() { + return mLastEvent; + } + + /** + * Callback for receiving an {@link AccessibilityEvent}. + * + * <strong>Note:</strong> This method is <strong>NOT</strong> + * executed on the application main thread. The client are + * responsible for proper synchronization. + * + * @param event The received event. + */ + public void onAccessibilityEvent(AccessibilityEvent event) { + /* hook - do nothing */ + } + + /** + * Callback for requests to stop feedback. + * + * <strong>Note:</strong> This method is <strong>NOT</strong> + * executed on the application main thread. The client are + * responsible for proper synchronization. + */ + public void onInterrupt() { + /* hook - do nothing */ + } + + /** + * Connects this service. + * + * @throws IllegalStateException If already connected. + */ + public void connect() { + if (isConnected()) { + throw new IllegalStateException("Already connected."); + } + + // Serialize binder calls to a handler on a dedicated thread + // different from the main since we expose APIs that block + // the main thread waiting for a result the deliver of which + // on the main thread will prevent that thread from waking up. + // The serialization is needed also to ensure that events are + // examined in delivery order. Otherwise, a fair locking + // is needed for making sure the binder calls are interleaved + // with check for the expected event and also to make sure the + // binder threads are allowed to proceed in the received order. + HandlerThread handlerThread = new HandlerThread("UiTestAutomationBridge"); + handlerThread.start(); + Looper looper = handlerThread.getLooper(); + + mListener = new IEventListenerWrapper(null, looper, new Callbacks() { + @Override + public void onServiceConnected() { + /* do nothing */ + } + + @Override + public void onInterrupt() { + UiTestAutomationBridge.this.onInterrupt(); + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + while (true) { + if (!mWaitingForEventDelivery) { + break; + } + if (!mUnprocessedEventAvailable) { + mUnprocessedEventAvailable = true; + mLastEvent = AccessibilityEvent.obtain(event); + mLock.notifyAll(); + break; + } + try { + mLock.wait(); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + UiTestAutomationBridge.this.onAccessibilityEvent(event); + } + + @Override + public void onSetConnectionId(int connectionId) { + synchronized (mLock) { + mConnectionId = connectionId; + mLock.notifyAll(); + } + } + }); + + final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); + + final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; + info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; + + try { + manager.registerUiTestAutomationService(mListener, info); + } catch (RemoteException re) { + throw new IllegalStateException("Cound not register UiAutomationService.", re); + } + + synchronized (mLock) { + final long startTimeMillis = SystemClock.uptimeMillis(); + while (true) { + if (isConnected()) { + return; + } + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + final long remainingTimeMillis = TIMEOUT_REGISTER_SERVICE - elapsedTimeMillis; + if (remainingTimeMillis <= 0) { + throw new IllegalStateException("Cound not register UiAutomationService."); + } + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + } + + /** + * Disconnects this service. + * + * @throws IllegalStateException If already disconnected. + */ + public void disconnect() { + if (!isConnected()) { + throw new IllegalStateException("Already disconnected."); + } + + IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); + + try { + manager.unregisterUiTestAutomationService(mListener); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while unregistering UiTestAutomationService", re); + } + } + + /** + * Gets whether this service is connected. + * + * @return True if connected. + */ + public boolean isConnected() { + return (mConnectionId != AccessibilityInteractionClient.NO_ID); + } + + /** + * Executes a command and waits for a specific accessibility event type up + * to a given timeout. + * + * @param command The command to execute before starting to wait for the event. + * @param predicate Predicate for recognizing the awaited event. + * @param timeoutMillis The max wait time in milliseconds. + */ + public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, + Predicate<AccessibilityEvent> predicate, long timeoutMillis) + throws TimeoutException, Exception { + synchronized (mLock) { + // Prepare to wait for an event. + mWaitingForEventDelivery = true; + mUnprocessedEventAvailable = false; + if (mLastEvent != null) { + mLastEvent.recycle(); + mLastEvent = null; + } + // Execute the command. + command.run(); + // Wait for the event. + final long startTimeMillis = SystemClock.uptimeMillis(); + while (true) { + // If the expected event is received, that's it. + if ((mUnprocessedEventAvailable && predicate.apply(mLastEvent))) { + mWaitingForEventDelivery = false; + mUnprocessedEventAvailable = false; + mLock.notifyAll(); + return mLastEvent; + } + // Ask for another event. + mWaitingForEventDelivery = true; + mUnprocessedEventAvailable = false; + mLock.notifyAll(); + // Check if timed out and if not wait. + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; + if (remainingTimeMillis <= 0) { + mWaitingForEventDelivery = false; + mUnprocessedEventAvailable = false; + mLock.notifyAll(); + throw new TimeoutException("Expacted event not received within: " + + timeoutMillis + " ms."); + } + try { + mLock.wait(remainingTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + } + + /** + * Finds an {@link AccessibilityNodeInfo} by accessibility id in the active + * window. The search is performed from the root node. + * + * @param accessibilityNodeId A unique view id or virtual descendant id for + * which to search. + * @return The current window scale, where zero means a failure. + */ + public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityIdInActiveWindow( + long accessibilityNodeId) { + return findAccessibilityNodeInfoByAccessibilityId(ACTIVE_WINDOW_ID, accessibilityNodeId); + } + + /** + * Finds an {@link AccessibilityNodeInfo} by accessibility id. + * + * @param accessibilityWindowId A unique window id. Use {@link #ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id for + * which to search. + * @return The current window scale, where zero means a failure. + */ + public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId( + int accessibilityWindowId, long accessibilityNodeId) { + // Cache the id to avoid locking + final int connectionId = mConnectionId; + ensureValidConnection(connectionId); + return AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfoByAccessibilityId(mConnectionId, + accessibilityWindowId, accessibilityNodeId); + } + + /** + * Finds an {@link AccessibilityNodeInfo} by View id in the active + * window. The search is performed from the root node. + * + * @return The current window scale, where zero means a failure. + */ + public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId) { + return findAccessibilityNodeInfoByViewId(ACTIVE_WINDOW_ID, ROOT_NODE_ID, viewId); + } + + /** + * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in + * the window whose id is specified and starts from the node whose accessibility + * id is specified. + * + * @param accessibilityWindowId A unique window id. Use {@link #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 #ROOT_NODE_ID} to start from the root. + * @return The current window scale, where zero means a failure. + */ + public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int accessibilityWindowId, + long accessibilityNodeId, int viewId) { + // Cache the id to avoid locking + final int connectionId = mConnectionId; + ensureValidConnection(connectionId); + return AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfoByViewId(connectionId, accessibilityWindowId, + accessibilityNodeId, viewId); + } + + /** + * Finds {@link AccessibilityNodeInfo}s by View text in the active + * window. The search is performed from the root node. + * + * @param text The searched text. + * @return The current window scale, where zero means a failure. + */ + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(String text) { + return findAccessibilityNodeInfosByText(ACTIVE_WINDOW_ID, ROOT_NODE_ID, text); + } + + /** + * Finds {@link AccessibilityNodeInfo}s by View text. The match is case + * insensitive containment. The search is performed in the window whose + * id is specified and starts from the node whose accessibility id is + * specified. + * + * @param accessibilityWindowId A unique window id. Use {@link #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 #ROOT_NODE_ID} to start from the root. + * @param text The searched text. + * @return The current window scale, where zero means a failure. + */ + public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int accessibilityWindowId, + long accessibilityNodeId, String text) { + // Cache the id to avoid locking + final int connectionId = mConnectionId; + ensureValidConnection(connectionId); + return AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfosByText(connectionId, accessibilityWindowId, + accessibilityNodeId, text); + } + + /** + * Performs an accessibility action on an {@link AccessibilityNodeInfo} + * in the active window. + * + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). + * @param action The action to perform. + * @return Whether the action was performed. + */ + public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action) { + return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action); + } + + /** + * Performs an accessibility action on an {@link AccessibilityNodeInfo}. + * + * @param accessibilityWindowId A unique window id. Use {@link #ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). + * @param action The action to perform. + * @return Whether the action was performed. + */ + public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, + int action) { + // Cache the id to avoid locking + final int connectionId = mConnectionId; + ensureValidConnection(connectionId); + return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId, + accessibilityWindowId, accessibilityNodeId, action); + } + + private void ensureValidConnection(int connectionId) { + if (connectionId == AccessibilityInteractionClient.NO_ID) { + throw new IllegalStateException("UiAutomationService not connected." + + " Did you call #register()?"); + } + } +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index fbc1b2b7eca1..ab62c446e57a 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2957,6 +2957,13 @@ public class Intent implements Parcelable, Cloneable { */ public static final int FLAG_RECEIVER_REPLACE_PENDING = 0x20000000; /** + * If set, when sending a broadcast the recipient is allowed to run at + * foreground priority, with a shorter timeout interval. During normal + * broadcasts the receivers are not automatically hoisted out of the + * background priority class. + */ + public static final int FLAG_RECEIVER_FOREGROUND = 0x10000000; + /** * If set, when sending a broadcast <i>before boot has completed</i> only * registered receivers will be called -- no BroadcastReceiver components * will be launched. Sticky intent state will be recorded properly even @@ -2969,14 +2976,14 @@ public class Intent implements Parcelable, Cloneable { * * @hide */ - public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x10000000; + public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x08000000; /** * Set when this broadcast is for a boot upgrade, a special mode that * allows the broadcast to be sent before the system is ready and launches * the app process with no providers running in it. * @hide */ - public static final int FLAG_RECEIVER_BOOT_UPGRADE = 0x08000000; + public static final int FLAG_RECEIVER_BOOT_UPGRADE = 0x04000000; /** * @hide Flags that can't be changed with PendingIntent. diff --git a/core/java/android/view/AccessibilityNodeInfoCache.java b/core/java/android/view/AccessibilityNodeInfoCache.java new file mode 100644 index 000000000000..244a49191f74 --- /dev/null +++ b/core/java/android/view/AccessibilityNodeInfoCache.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.util.LongSparseArray; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * Simple cache for AccessibilityNodeInfos. The cache is mapping an + * accessibility id to an info. The cache allows storing of + * <code>null</code> values. It also tracks accessibility events + * and invalidates accordingly. + * + * @hide + */ +public class AccessibilityNodeInfoCache { + + private final boolean ENABLED = true; + + /** + * @return A new <strong>not synchronized</strong> AccessibilityNodeInfoCache. + */ + public static AccessibilityNodeInfoCache newAccessibilityNodeInfoCache() { + return new AccessibilityNodeInfoCache(); + } + + /** + * @return A new <strong>synchronized</strong> AccessibilityNodeInfoCache. + */ + public static AccessibilityNodeInfoCache newSynchronizedAccessibilityNodeInfoCache() { + return new AccessibilityNodeInfoCache() { + private final Object mLock = new Object(); + + @Override + public void clear() { + synchronized(mLock) { + super.clear(); + } + } + + @Override + public AccessibilityNodeInfo get(long accessibilityNodeId) { + synchronized(mLock) { + return super.get(accessibilityNodeId); + } + } + + @Override + public void put(long accessibilityNodeId, AccessibilityNodeInfo info) { + synchronized(mLock) { + super.put(accessibilityNodeId, info); + } + } + + @Override + public void remove(long accessibilityNodeId) { + synchronized(mLock) { + super.remove(accessibilityNodeId); + } + } + }; + } + + private final LongSparseArray<AccessibilityNodeInfo> mCacheImpl; + + private AccessibilityNodeInfoCache() { + if (ENABLED) { + mCacheImpl = new LongSparseArray<AccessibilityNodeInfo>(); + } else { + mCacheImpl = null; + } + } + + /** + * The cache keeps track of {@link AccessibilityEvent}s and invalidates + * cached nodes as appropriate. + * + * @param event An event. + */ + public void onAccessibilityEvent(AccessibilityEvent event) { + final int eventType = event.getEventType(); + switch (eventType) { + 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; + } + } + + /** + * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id. + * + * @param accessibilityNodeId The info accessibility node id. + * @return The cached {@link AccessibilityNodeInfo} or null if such not found. + */ + public AccessibilityNodeInfo get(long accessibilityNodeId) { + if (ENABLED) { + return mCacheImpl.get(accessibilityNodeId); + } else { + return null; + } + } + + /** + * Caches an {@link AccessibilityNodeInfo} given its accessibility node id. + * + * @param accessibilityNodeId The info accessibility node id. + * @param info The {@link AccessibilityNodeInfo} to cache. + */ + public void put(long accessibilityNodeId, AccessibilityNodeInfo info) { + if (ENABLED) { + mCacheImpl.put(accessibilityNodeId, info); + } + } + + /** + * Returns whether the cache contains an accessibility node id key. + * + * @param accessibilityNodeId The key for which to check. + * @return True if the key is in the cache. + */ + public boolean containsKey(long accessibilityNodeId) { + if (ENABLED) { + return (mCacheImpl.indexOfKey(accessibilityNodeId) >= 0); + } else { + return false; + } + } + + /** + * Removes a cached {@link AccessibilityNodeInfo}. + * + * @param accessibilityNodeId The info accessibility node id. + */ + public void remove(long accessibilityNodeId) { + if (ENABLED) { + mCacheImpl.remove(accessibilityNodeId); + } + } + + /** + * Clears the cache. + */ + public void clear() { + if (ENABLED) { + mCacheImpl.clear(); + } + } +} diff --git a/core/java/android/view/ActionMode.java b/core/java/android/view/ActionMode.java index 34e7d4d746bb..0349a2be3fbf 100644 --- a/core/java/android/view/ActionMode.java +++ b/core/java/android/view/ActionMode.java @@ -18,9 +18,15 @@ package android.view; /** - * Represents a contextual mode of the user interface. Action modes can be used for - * modal interactions with content and replace parts of the normal UI until finished. - * Examples of good action modes include selection modes, search, content editing, etc. + * Represents a contextual mode of the user interface. Action modes can be used to provide + * alternative interaction modes and replace parts of the normal UI until finished. + * Examples of good action modes include text selection and contextual actions. + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For information about how to provide contextual actions with {@code ActionMode}, + * read the <a href="{@docRoot}guide/topics/ui/menu.html#context-menu">Menus</a> + * developer guide.</p> + * </div> */ public abstract class ActionMode { private Object mTag; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 8597017a6498..d80d0801f2c9 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12679,6 +12679,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal ViewDebug.trace(this, ViewDebug.HierarchyTraceType.REQUEST_LAYOUT); } + if (getAccessibilityNodeProvider() != null) { + throw new IllegalStateException("Views with AccessibilityNodeProvider" + + " can't have children."); + } + mPrivateFlags |= FORCE_LAYOUT; mPrivateFlags |= INVALIDATED; diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 75598620a362..d3af61841295 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3356,6 +3356,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { + if (getAccessibilityNodeProvider() != null) { + throw new IllegalStateException("Views with AccessibilityNodeProvider" + + " can't have children."); + } + if (mTransition != null) { // Don't prevent other add transitions from completing, but cancel remove // transitions to let them complete the process before we add to the container diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 2ef843ba5e17..3f61e6b38fe8 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -52,6 +52,7 @@ import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; +import android.util.LongSparseArray; import android.util.Pool; import android.util.Poolable; import android.util.PoolableManager; @@ -302,6 +303,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent; + AccessibilityPrefetchStrategy mAccessibilityPrefetchStrategy; + private final int mDensity; /** @@ -3483,6 +3486,17 @@ public final class ViewRootImpl extends Handler implements ViewParent, return mAccessibilityInteractionController; } + public AccessibilityPrefetchStrategy getAccessibilityPrefetchStrategy() { + if (mView == null) { + throw new IllegalStateException("getAccessibilityPrefetchStrategy" + + " called when there is no mView"); + } + if (mAccessibilityPrefetchStrategy == null) { + mAccessibilityPrefetchStrategy = new AccessibilityPrefetchStrategy(); + } + return mAccessibilityPrefetchStrategy; + } + private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { @@ -3983,6 +3997,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, if (mView == null) { return false; } + getAccessibilityPrefetchStrategy().onAccessibilityEvent(event); mAccessibilityManager.sendAccessibilityEvent(event); return true; } @@ -4542,6 +4557,13 @@ public final class ViewRootImpl extends Handler implements ViewParent, viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId, interactionId, callback, interrogatingPid, interrogatingTid); + } else { + // We cannot make the call and notify the caller so it does not wait. + try { + callback.setFindAccessibilityNodeInfosResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } } } @@ -4553,28 +4575,49 @@ public final class ViewRootImpl extends Handler implements ViewParent, viewRootImpl.getAccessibilityInteractionController() .performAccessibilityActionClientThread(accessibilityNodeId, action, interactionId, callback, interogatingPid, interrogatingTid); + } else { + // We cannot make the call and notify the caller so it does not + try { + callback.setPerformAccessibilityActionResult(false, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } } } - public void findAccessibilityNodeInfoByViewId(int viewId, + public void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback, - interrogatingPid, interrogatingTid); + .findAccessibilityNodeInfoByViewIdClientThread(accessibilityNodeId, viewId, + interactionId, callback, interrogatingPid, interrogatingTid); + } else { + // We cannot make the call and notify the caller so it does not + try { + callback.setFindAccessibilityNodeInfoResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } } } - public void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId, + public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() - .findAccessibilityNodeInfosByTextClientThread(text, accessibilityNodeId, + .findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text, interactionId, callback, interrogatingPid, interrogatingTid); + } else { + // We cannot make the call and notify the caller so it does not + try { + callback.setFindAccessibilityNodeInfosResult(null, interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } } } } @@ -4652,6 +4695,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, long interrogatingTid) { Message message = Message.obtain(); message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; + message.arg1 = interrogatingPid; SomeArgs args = mPool.acquire(); args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); @@ -4674,40 +4718,47 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { SomeArgs args = (SomeArgs) message.obj; + final int interrogatingPid = message.arg1; final int accessibilityViewId = args.argi1; final int virtualDescendantId = args.argi2; final int interactionId = args.argi3; final IAccessibilityInteractionConnectionCallback callback = (IAccessibilityInteractionConnectionCallback) args.arg1; mPool.release(args); - AccessibilityNodeInfo info = null; + List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; + infos.clear(); try { View target = findViewByAccessibilityId(accessibilityViewId); if (target != null && target.getVisibility() == View.VISIBLE) { AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); if (provider != null) { - info = provider.createAccessibilityNodeInfo(virtualDescendantId); + infos.add(provider.createAccessibilityNodeInfo(virtualDescendantId)); } else if (virtualDescendantId == View.NO_ID) { - info = target.createAccessibilityNodeInfo(); + getAccessibilityPrefetchStrategy().prefetchAccessibilityNodeInfos( + interrogatingPid, target, infos); } } } finally { try { - callback.setFindAccessibilityNodeInfoResult(info, interactionId); + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + infos.clear(); } catch (RemoteException re) { /* ignore - the other side will time out */ } } } - public void findAccessibilityNodeInfoByViewIdClientThread(int viewId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, - long interrogatingTid) { + public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, + int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + int interrogatingPid, long interrogatingTid) { Message message = Message.obtain(); message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; - message.arg1 = viewId; - message.arg2 = interactionId; - message.obj = callback; + 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 @@ -4723,17 +4774,26 @@ public final class ViewRootImpl extends Handler implements ViewParent, } public void findAccessibilityNodeInfoByViewIdUiThread(Message message) { - final int viewId = message.arg1; - final int interactionId = message.arg2; + final int accessibilityViewId = message.arg1; + SomeArgs args = (SomeArgs) message.obj; + final int viewId = args.argi1; + final int interactionId = args.argi2; final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) message.obj; - + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); AccessibilityNodeInfo info = null; try { - View root = ViewRootImpl.this.mView; - View target = root.findViewById(viewId); - if (target != null && target.getVisibility() == View.VISIBLE) { - info = target.createAccessibilityNodeInfo(); + View root = null; + if (accessibilityViewId != View.NO_ID) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = ViewRootImpl.this.mView; + } + if (root != null) { + View target = root.findViewById(viewId); + if (target != null && target.getVisibility() == View.VISIBLE) { + info = target.createAccessibilityNodeInfo(); + } } } finally { try { @@ -4744,8 +4804,8 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } - public void findAccessibilityNodeInfosByTextClientThread(String text, - long accessibilityNodeId, int interactionId, + public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId, + String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid) { Message message = Message.obtain(); @@ -4937,4 +4997,88 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } } + + /** + * 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. It caches + * the ids of the prefeteched nodes such that they are fetched only once. + */ + class AccessibilityPrefetchStrategy { + private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 100; + + // We need to keep track of what we have sent for each interrogating + // process. Usually there will be only one such process but we + // should support the general case. Note that the accessibility event + // stream will take care of clearing caches of querying processes that + // are not longer alive, so we do not waste memory. + private final LongSparseArray<AccessibilityNodeInfoCache> mAccessibilityNodeInfoCaches = + new LongSparseArray<AccessibilityNodeInfoCache>(); + + private AccessibilityNodeInfoCache getCacheForInterrogatingPid(long interrogatingPid) { + AccessibilityNodeInfoCache cache = mAccessibilityNodeInfoCaches.get(interrogatingPid); + if (cache == null) { + cache = AccessibilityNodeInfoCache.newAccessibilityNodeInfoCache(); + mAccessibilityNodeInfoCaches.put(interrogatingPid, cache); + } + return cache; + } + + public void onAccessibilityEvent(AccessibilityEvent event) { + final int cacheCount = mAccessibilityNodeInfoCaches.size(); + for (int i = 0; i < cacheCount; i++) { + AccessibilityNodeInfoCache cache = mAccessibilityNodeInfoCaches.valueAt(i); + cache.onAccessibilityEvent(event); + } + } + + public void prefetchAccessibilityNodeInfos(long interrogatingPid, View root, + List<AccessibilityNodeInfo> outInfos) { + addAndCacheNotCachedNodeInfo(interrogatingPid, root, outInfos); + addAndCacheNotCachedPredecessorInfos(interrogatingPid, root, outInfos); + addAndCacheNotCachedDescendantInfos(interrogatingPid, root, outInfos); + } + + private void addAndCacheNotCachedNodeInfo(long interrogatingPid, + View view, List<AccessibilityNodeInfo> outInfos) { + final long accessibilityNodeId = AccessibilityNodeInfo.makeNodeId( + view.getAccessibilityViewId(), View.NO_ID); + AccessibilityNodeInfoCache cache = getCacheForInterrogatingPid(interrogatingPid); + if (!cache.containsKey(accessibilityNodeId)) { + // Account for the ids of the fetched infos. The infos will be + // cached in the window querying process. We just need to know + // which infos are cached to avoid fetching a cached one again. + cache.put(accessibilityNodeId, null); + outInfos.add(view.createAccessibilityNodeInfo()); + } + } + + private void addAndCacheNotCachedPredecessorInfos(long interrogatingPid, View view, + List<AccessibilityNodeInfo> outInfos) { + ViewParent predecessor = view.getParent(); + while (predecessor instanceof View + && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + View predecessorView = (View) predecessor; + addAndCacheNotCachedNodeInfo(interrogatingPid, predecessorView, outInfos); + predecessor = predecessor.getParent(); + } + } + + private void addAndCacheNotCachedDescendantInfos(long interrogatingPid, View view, + List<AccessibilityNodeInfo> outInfos) { + if (outInfos.size() > MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE + || view.getAccessibilityNodeProvider() != null) { + return; + } + addAndCacheNotCachedNodeInfo(interrogatingPid, view, outInfos); + if (view instanceof ViewGroup) { + ViewGroup rootGroup = (ViewGroup) view; + final int childCount = rootGroup.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = rootGroup.getChildAt(i); + addAndCacheNotCachedDescendantInfos(interrogatingPid, child, outInfos); + } + } + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 95c070cf5b2c..072fdd86ed08 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -24,7 +24,9 @@ import android.os.SystemClock; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; +import android.view.AccessibilityNodeInfoCache; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -97,6 +99,11 @@ public final class AccessibilityInteractionClient private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = new SparseArray<IAccessibilityServiceConnection>(); + // The connection cache is shared between all interrogating threads since + // at any given time there is only one window allowing querying. + private static final AccessibilityNodeInfoCache sAccessibilityNodeInfoCache = + AccessibilityNodeInfoCache.newSynchronizedAccessibilityNodeInfoCache(); + /** * @return The client for the current thread. */ @@ -145,7 +152,9 @@ public final class AccessibilityInteractionClient * Finds an {@link AccessibilityNodeInfo} by accessibility id. * * @param connectionId The id of a connection for interacting with the system. - * @param accessibilityWindowId A unique window id. + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID} + * to query the currently active window. * @param accessibilityNodeId A unique node accessibility id * (accessibility view and virtual descendant id). * @return An {@link AccessibilityNodeInfo} if found, null otherwise. @@ -155,16 +164,22 @@ public final class AccessibilityInteractionClient try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { + AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(accessibilityNodeId); + if (cachedInfo != null) { + return cachedInfo; + } final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( accessibilityWindowId, accessibilityNodeId, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { - AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( + List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAccessibilityNodeInfo(info, connectionId, windowScale); - return info; + finalizeAccessibilityNodeInfos(infos, connectionId, windowScale); + if (infos != null && !infos.isEmpty()) { + return infos.get(0); + } } } else { if (DEBUG) { @@ -181,22 +196,30 @@ public final class AccessibilityInteractionClient } /** - * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed - * in the currently active window and starts from the root View in the window. + * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in + * the window whose id is specified and starts from the node whose accessibility + * id is specified. * * @param connectionId The id of a connection for interacting with the system. + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id from where to start the search. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID} + * to start from the root. * @param viewId The id of the view. * @return An {@link AccessibilityNodeInfo} if found, null otherwise. */ - public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId, - int viewId) { + public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int connectionId, + int accessibilityWindowId, long accessibilityNodeId, int viewId) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = - connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId, - interactionId, this, Thread.currentThread().getId()); + connection.findAccessibilityNodeInfoByViewId(accessibilityWindowId, + accessibilityNodeId, viewId, interactionId, this, + Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( @@ -220,64 +243,27 @@ public final class AccessibilityInteractionClient /** * Finds {@link AccessibilityNodeInfo}s by View text. The match is case - * insensitive containment. The search is performed in the currently - * active window and starts from the root View in the window. - * - * @param connectionId The id of a connection for interacting with the system. - * @param text The searched text. - * @return A list of found {@link AccessibilityNodeInfo}s. - */ - public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow( - int connectionId, String text) { - try { - IAccessibilityServiceConnection connection = getConnection(connectionId); - if (connection != null) { - final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = - connection.findAccessibilityNodeInfosByTextInActiveWindow(text, - interactionId, this, Thread.currentThread().getId()); - // If the scale is zero the call has failed. - if (windowScale > 0) { - List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( - interactionId); - finalizeAccessibilityNodeInfos(infos, connectionId, windowScale); - return infos; - } - } 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" - + " findAccessibilityNodeInfosByViewTextInActiveWindow", re); - } - } - return null; - } - - /** - * Finds {@link AccessibilityNodeInfo}s by View text. The match is case * insensitive containment. The search is performed in the window whose - * id is specified and starts from the View whose accessibility id is + * 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 com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id from where to start the search. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#ROOT_NODE_ID} * @param text The searched text. - * @param accessibilityWindowId A unique window id. - * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id) from - * where to start the search. Use {@link android.view.View#NO_ID} to start from the root. * @return A list of found {@link AccessibilityNodeInfo}s. */ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, - String text, int accessibilityWindowId, long accessibilityNodeId) { + int accessibilityWindowId, long accessibilityNodeId, String text) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); - final float windowScale = connection.findAccessibilityNodeInfosByText(text, - accessibilityWindowId, accessibilityNodeId, interactionId, this, + final float windowScale = connection.findAccessibilityNodeInfosByText( + accessibilityWindowId, accessibilityNodeId, text, interactionId, this, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { @@ -304,7 +290,9 @@ public final class AccessibilityInteractionClient * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * * @param connectionId The id of a connection for interacting with the system. - * @param accessibilityWindowId The id of the window. + * @param accessibilityWindowId A unique window id. Use + * {@link com.android.server.accessibility.AccessibilityManagerService#ACTIVE_WINDOW_ID} + * to query the currently active window. * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). * @param action The action to perform. * @return Whether the action was performed. @@ -319,7 +307,7 @@ public final class AccessibilityInteractionClient accessibilityWindowId, accessibilityNodeId, action, interactionId, this, Thread.currentThread().getId()); if (success) { - return getPerformAccessibilityActionResult(interactionId); + return getPerformAccessibilityActionResultAndClear(interactionId); } } else { if (DEBUG) { @@ -334,6 +322,24 @@ public final class AccessibilityInteractionClient return false; } + public void clearCache() { + if (DEBUG) { + Log.w(LOG_TAG, "clearCache()"); + } + sAccessibilityNodeInfoCache.clear(); + } + + public void removeCachedNode(long accessibilityNodeId) { + if (DEBUG) { + Log.w(LOG_TAG, "removeCachedNode(" + accessibilityNodeId +")"); + } + sAccessibilityNodeInfoCache.remove(accessibilityNodeId); + } + + public void onAccessibilityEvent(AccessibilityEvent event) { + sAccessibilityNodeInfoCache.onAccessibilityEvent(event); + } + /** * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. * @@ -358,6 +364,9 @@ public final class AccessibilityInteractionClient if (interactionId > mInteractionId) { mFindAccessibilityNodeInfoResult = info; mInteractionId = interactionId; + if (info != null) { + sAccessibilityNodeInfoCache.put(info.getSourceNodeId(), info); + } } mInstanceLock.notifyAll(); } @@ -386,8 +395,20 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { if (interactionId > mInteractionId) { - mFindAccessibilityNodeInfosResult = infos; + // 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); + } else { + mFindAccessibilityNodeInfosResult = infos; + } mInteractionId = interactionId; + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i ++) { + AccessibilityNodeInfo info = infos.get(i); + sAccessibilityNodeInfoCache.put(info.getSourceNodeId(), info); + } } mInstanceLock.notifyAll(); } @@ -399,7 +420,7 @@ public final class AccessibilityInteractionClient * @param interactionId The interaction id to match the result with the request. * @return Whether the action was performed. */ - private boolean getPerformAccessibilityActionResult(int interactionId) { + private boolean getPerformAccessibilityActionResultAndClear(int interactionId) { synchronized (mInstanceLock) { final boolean success = waitForResultTimedLocked(interactionId); final boolean result = success ? mPerformAccessibilityActionResult : false; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 6939c2cf1ddf..d7d67928e158 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -380,8 +380,8 @@ public class AccessibilityNodeInfo implements Parcelable { return Collections.emptyList(); } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); - return client.findAccessibilityNodeInfosByText(mConnectionId, text, mWindowId, - mSourceNodeId); + return client.findAccessibilityNodeInfosByText(mConnectionId, mWindowId, mSourceNodeId, + text); } /** @@ -903,6 +903,17 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets the id of the source node. + * + * @return The id. + * + * @hide + */ + public long getSourceNodeId() { + return mSourceNodeId; + } + + /** * Sets if this instance is sealed. * * @param sealed Whether is sealed. diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 07aeb9ae5b7d..b60f50eb5d9d 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -564,6 +564,17 @@ public class AccessibilityRecord { } /** + * Gets the id of the source node. + * + * @return The id. + * + * @hide + */ + public long getSourceNodeId() { + return mSourceNodeId; + } + + /** * Sets the unique id of the IAccessibilityServiceConnection over which * this instance can send requests to the system. * diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index a90c427f4ab8..ae6869cad9f5 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -31,13 +31,13 @@ oneway interface IAccessibilityInteractionConnection { IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); - void findAccessibilityNodeInfoByViewId(int id, int interactionId, + void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int id, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, long interrogatingTid); - void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId, - int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid); + void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + long interrogatingTid); void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index c3794bec3ef0..320c75da0233 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -49,5 +49,7 @@ interface IAccessibilityManager { void removeAccessibilityInteractionConnection(IWindow windowToken); - void registerEventListener(IEventListener client); + void registerUiTestAutomationService(IEventListener listener, in AccessibilityServiceInfo info); + + void unregisterUiTestAutomationService(IEventListener listener); } diff --git a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java index b4a05819020b..a9f144bdca49 100644 --- a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java +++ b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivity.java @@ -14,12 +14,12 @@ package android.accessibilityservice; -import com.android.frameworks.coretests.R; - import android.app.Activity; import android.os.Bundle; import android.view.View; +import com.android.frameworks.coretests.R; + /** * Activity for testing the accessibility APIs for "interrogation" of * the screen content. These APIs allow exploring the screen and diff --git a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java index 259a09448e2f..fa4809331ac3 100644 --- a/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java +++ b/core/tests/coretests/src/android/accessibilityservice/InterrogationActivityTest.java @@ -14,26 +14,21 @@ package android.accessibilityservice; -import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS; -import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_FOCUS; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SELECT; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION; -import android.content.Context; import android.graphics.Rect; -import android.os.ServiceManager; import android.os.SystemClock; import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.LargeTest; import android.util.Log; -import android.view.View; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityInteractionClient; -import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.IAccessibilityManager; import com.android.frameworks.coretests.R; +import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.LinkedList; @@ -48,21 +43,15 @@ import java.util.Queue; */ public class InterrogationActivityTest extends ActivityInstrumentationTestCase2<InterrogationActivity> { - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; private static String LOG_TAG = "InterrogationActivityTest"; - // Timeout before give up wait for the system to process an accessibility setting change. - private static final int TIMEOUT_PROPAGATE_ACCESSIBLITY_SETTING = 2000; - // Timeout for the accessibility state of an Activity to be fully initialized. - private static final int TIMEOUT_ACCESSIBLITY_STATE_INITIALIZED_MILLIS = 100; + private static final int TIMEOUT_PROPAGATE_ACCESSIBILITY_EVENT_MILLIS = 5000; // Handle to a connection to the AccessibilityManagerService - private static int sConnectionId = View.NO_ID; - - // The last received accessibility event - private volatile AccessibilityEvent mLastAccessibilityEvent; + private UiTestAutomationBridge mUiTestAutomationBridge; public InterrogationActivityTest() { super(InterrogationActivity.class); @@ -70,16 +59,39 @@ public class InterrogationActivityTest @Override public void setUp() throws Exception { - ensureConnection(); - bringUpActivityWithInitalizedAccessbility(); + super.setUp(); + mUiTestAutomationBridge = new UiTestAutomationBridge(); + mUiTestAutomationBridge.connect(); + mUiTestAutomationBridge.executeCommandAndWaitForAccessibilityEvent(new Runnable() { + // wait for the first accessibility event + @Override + public void run() { + // bring up the activity + getActivity(); + } + }, + new Predicate<AccessibilityEvent>() { + @Override + public boolean apply(AccessibilityEvent event) { + return (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && event.getPackageName().equals(getActivity().getPackageName())); + } + }, + TIMEOUT_PROPAGATE_ACCESSIBILITY_EVENT_MILLIS); + } + + @Override + public void tearDown() throws Exception { + mUiTestAutomationBridge.disconnect(); + super.tearDown(); } @LargeTest public void testFindAccessibilityNodeInfoByViewId() throws Exception { final long startTimeMillis = SystemClock.uptimeMillis(); try { - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertNotNull(button); assertEquals(0, button.getChildCount()); @@ -125,8 +137,8 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view by text - List<AccessibilityNodeInfo> buttons = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfosByTextInActiveWindow(sConnectionId, "butto"); + List<AccessibilityNodeInfo> buttons = mUiTestAutomationBridge + .findAccessibilityNodeInfosByTextInActiveWindow("butto"); assertEquals(9, buttons.size()); } finally { if (DEBUG) { @@ -141,12 +153,9 @@ public class InterrogationActivityTest public void testFindAccessibilityNodeInfoByViewTextContentDescription() throws Exception { final long startTimeMillis = SystemClock.uptimeMillis(); try { - bringUpActivityWithInitalizedAccessbility(); - // find a view by text - List<AccessibilityNodeInfo> buttons = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfosByTextInActiveWindow(sConnectionId, - "contentDescription"); + List<AccessibilityNodeInfo> buttons = mUiTestAutomationBridge + .findAccessibilityNodeInfosByTextInActiveWindow("contentDescription"); assertEquals(1, buttons.size()); } finally { if (DEBUG) { @@ -177,8 +186,8 @@ public class InterrogationActivityTest classNameAndTextList.add("android.widget.ButtonButton8"); classNameAndTextList.add("android.widget.ButtonButton9"); - AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.root); + AccessibilityNodeInfo root = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.root); assertNotNull("We must find the existing root.", root); Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); @@ -216,16 +225,16 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view and make sure it is not focused - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertFalse(button.isFocused()); // focus the view assertTrue(button.performAction(ACTION_FOCUS)); // find the view again and make sure it is focused - button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertTrue(button.isFocused()); } finally { if (DEBUG) { @@ -240,24 +249,24 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view and make sure it is not focused - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertFalse(button.isFocused()); // focus the view assertTrue(button.performAction(ACTION_FOCUS)); // find the view again and make sure it is focused - button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertTrue(button.isFocused()); // unfocus the view assertTrue(button.performAction(ACTION_CLEAR_FOCUS)); // find the view again and make sure it is not focused - button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertFalse(button.isFocused()); } finally { if (DEBUG) { @@ -273,16 +282,16 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view and make sure it is not selected - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertFalse(button.isSelected()); // select the view assertTrue(button.performAction(ACTION_SELECT)); // find the view again and make sure it is selected - button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertTrue(button.isSelected()); } finally { if (DEBUG) { @@ -297,24 +306,24 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view and make sure it is not selected - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertFalse(button.isSelected()); // select the view assertTrue(button.performAction(ACTION_SELECT)); // find the view again and make sure it is selected - button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertTrue(button.isSelected()); // unselect the view assertTrue(button.performAction(ACTION_CLEAR_SELECTION)); // find the view again and make sure it is not selected - button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); assertFalse(button.isSelected()); } finally { if (DEBUG) { @@ -330,23 +339,33 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view and make sure it is not focused - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); - assertFalse(button.isSelected()); - - // focus the view - assertTrue(button.performAction(ACTION_FOCUS)); + final AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); + assertFalse(button.isFocused()); - synchronized (this) { - try { - wait(TIMEOUT_ACCESSIBLITY_STATE_INITIALIZED_MILLIS); - } catch (InterruptedException ie) { - /* ignore */ + AccessibilityEvent event = mUiTestAutomationBridge + .executeCommandAndWaitForAccessibilityEvent(new Runnable() { + @Override + public void run() { + // focus the view + assertTrue(button.performAction(ACTION_FOCUS)); } - } + }, + new Predicate<AccessibilityEvent>() { + @Override + public boolean apply(AccessibilityEvent event) { + return (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED + && event.getPackageName().equals(getActivity().getPackageName()) + && event.getText().get(0).equals(button.getText())); + } + }, + TIMEOUT_PROPAGATE_ACCESSIBILITY_EVENT_MILLIS); + + // check the last event + assertNotNull(event); // check that last event source - AccessibilityNodeInfo source = mLastAccessibilityEvent.getSource(); + AccessibilityNodeInfo source = event.getSource(); assertNotNull(source); // bounds @@ -389,8 +408,9 @@ public class InterrogationActivityTest final long startTimeMillis = SystemClock.uptimeMillis(); try { // find a view and make sure it is not focused - AccessibilityNodeInfo button = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByViewIdInActiveWindow(sConnectionId, R.id.button5); + AccessibilityNodeInfo button = mUiTestAutomationBridge + .findAccessibilityNodeInfoByViewIdInActiveWindow(R.id.button5); + assertNotNull(button); AccessibilityNodeInfo parent = button.getParent(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { @@ -410,71 +430,4 @@ public class InterrogationActivityTest } } } - - private void bringUpActivityWithInitalizedAccessbility() { - mLastAccessibilityEvent = null; - // bring up the activity - getActivity(); - - final long startTimeMillis = SystemClock.uptimeMillis(); - while (true) { - if (mLastAccessibilityEvent != null) { - final int eventType = mLastAccessibilityEvent.getEventType(); - if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - return; - } - } - final long remainingTimeMillis = TIMEOUT_ACCESSIBLITY_STATE_INITIALIZED_MILLIS - - (SystemClock.uptimeMillis() - startTimeMillis); - if (remainingTimeMillis <= 0) { - return; - } - synchronized (this) { - try { - wait(remainingTimeMillis); - } catch (InterruptedException e) { - /* ignore */ - } - } - } - } - - private void ensureConnection() throws Exception { - if (sConnectionId == View.NO_ID) { - IEventListener listener = new IEventListener.Stub() { - public void setConnection(IAccessibilityServiceConnection connection, - int connectionId) { - sConnectionId = connectionId; - if (connection != null) { - AccessibilityInteractionClient.getInstance().addConnection(connectionId, - connection); - } else { - AccessibilityInteractionClient.getInstance().removeConnection(connectionId); - } - synchronized (this) { - notifyAll(); - } - } - - public void onInterrupt() {} - - public void onAccessibilityEvent(AccessibilityEvent event) { - mLastAccessibilityEvent = AccessibilityEvent.obtain(event); - synchronized (this) { - notifyAll(); - } - } - }; - - AccessibilityManager accessibilityManager = - AccessibilityManager.getInstance(getInstrumentation().getContext()); - - synchronized (this) { - IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( - ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); - manager.registerEventListener(listener); - wait(TIMEOUT_PROPAGATE_ACCESSIBLITY_SETTING); - } - } - } } diff --git a/docs/html/guide/guide_toc.cs b/docs/html/guide/guide_toc.cs index 6c01d44edfce..4a9a6848632a 100644 --- a/docs/html/guide/guide_toc.cs +++ b/docs/html/guide/guide_toc.cs @@ -131,7 +131,7 @@ </a></li> <li><a href="<?cs var:toroot ?>guide/topics/ui/menus.html"> <span class="en">Menus</span> - </a></li> + </a> <span class="new">updated</span></li> <li><a href="<?cs var:toroot ?>guide/topics/ui/actionbar.html"> <span class="en">Action Bar</span> </a></li> diff --git a/docs/html/guide/topics/ui/actionbar.jd b/docs/html/guide/topics/ui/actionbar.jd index b83bde75dd2a..e59fa0f1aaca 100644 --- a/docs/html/guide/topics/ui/actionbar.jd +++ b/docs/html/guide/topics/ui/actionbar.jd @@ -113,9 +113,10 @@ accessible to the user in a predictable way. href="{@docRoot}guide/topics/ui/menus.html#OptionsMenu">options menu</a> directly in the action bar, as "action items." Action items can also provide an "action view," which provides an embedded widget for even more immediate action behaviors. Menu items that are not promoted -to an action item are available in the overflow menu, revealed by either the device MENU button +to an action item are available in the overflow menu, revealed by either the device <em>Menu</em> +button (when available) or by an "overflow menu" button in the action bar (when the device does not -include a MENU button).</p> +include a <em>Menu</em> button).</p> </li> </ul> @@ -125,6 +126,10 @@ href="{@docRoot}resources/samples/HoneycombGallery/index.html">Honeycomb Gallery landscape handset), showing the logo on the left, navigation tabs, and an action item on the right (plus the overflow menu button).</p> +<p class="note"><strong>Note:</strong> If you're looking for information about the contextual +action bar for displaying contextual action items, see the <a +href="{@docRoot}guide/topics/ui/menus.html#context-menu">Menu</a> guide.</p> + <div class="design-announce"> <p><strong>Action Bar Design</strong></p> @@ -225,9 +230,10 @@ later—calling {@link android.app.Activity#getActionBar()} will return null href="{@docRoot}guide/topics/ui/menus.html#OptionsMenu">options menu</a>. To do this, you can declare that the menu item should appear in the action bar as an "action item." An action item can include an icon and/or a text title. If a menu item does not appear as an action item, then the -system places it in the overflow menu. The overflow menu is revealed either by the device MENU +system places it in the overflow menu. The overflow menu is revealed either by the device +<em>Menu</em> button (if provided by the device) or an additional button in the action bar (if the device does not -provide the MENU button).</p> +provide the <em>Menu</em> button).</p> <div class="figure" style="width:359px"> <img src="{@docRoot}images/ui/actionbar-item-withtext.png" height="57" alt="" /> @@ -1421,7 +1427,7 @@ href="#ActionView">action views</a>. (Added in API level 14.)</dd> </style> <!-- style for the action bar tab text --> - <style name="CustomTabTextStyle"> + <style name="CustomTabTextStyle" parent="@android:style/TextAppearance.Holo"> <item name="android:textColor">#2456c2</item> </style> </resources> @@ -1437,8 +1443,7 @@ action bar styles you want to change without re-implementing the styles you want manifest file like this:</p> <pre> -<application android:theme="@style/CustomActivityTheme" - ... /> +<application android:theme="@style/CustomActivityTheme" ... /> </pre> <p>For more information about using style and theme resources in your application, read <a @@ -1457,7 +1462,7 @@ android:backgroundStacked}. If you override these action bar styles, be sure tha parent action bar style such as {@link android.R.style#Widget_Holo_ActionBar Widget.Holo.ActionBar}.</p> -<p>For example, if you want to change the action bar's background, you could use the following +<p>For example, if you want to change the action bar's background, you can use the following styles:</p> <pre> @@ -1465,14 +1470,15 @@ styles:</p> <resources> <!-- the theme applied to the application or activity --> <style name="CustomActivityTheme" parent="@android:style/Theme.Holo"> - Â <item name="android:actionBarTabTextStyle">@style/customTabTextStyle</item> + <item name="android:actionBarStyle">@style/MyActionBar</item> <!-- other activity and action bar styles here --> </style> - <!-- style for the action bar, simply to change the background --> - <style parent="@android:style/Widget.Holo.ActionBar"> + <!-- style for the action bar backgrounds --> + <style name="MyActionBar" parent="@android:style/Widget.Holo.ActionBar"> <item name="android:background">@drawable/ab_background</item> - <item name="android:backgroundSplit">@drawable/ab_background</item> + <item name="android:backgroundStacked">@drawable/ab_background</item> + <item name="android:backgroundSplit">@drawable/ab_split_background</item> </style> </resources> </pre> diff --git a/docs/html/guide/topics/ui/menus.jd b/docs/html/guide/topics/ui/menus.jd index 7b5b3dc2f3c1..a2313b368b82 100644 --- a/docs/html/guide/topics/ui/menus.jd +++ b/docs/html/guide/topics/ui/menus.jd @@ -6,77 +6,129 @@ parent.link=index.html <div id="qv-wrapper"> <div id="qv"> <h2>In this document</h2> - <ol> - <li><a href="#xml">Creating a Menu Resource</a></li> - <li><a href="#Inflating">Inflating a Menu Resource</a> - <li><a href="#options-menu">Creating an Options Menu</a> - <ol> - <li><a href="#ChangingTheMenu">Changing menu items at runtime</a></li> - </ol> - </li> - <li><a href="#context-menu">Creating a Context Menu</a></li> - <li><a href="#submenu">Creating a Submenu</a></li> - <li><a href="#features">Other Menu Features</a> - <ol> - <li><a href="#groups">Menu groups</a></li> - <li><a href="#checkable">Checkable menu items</a></li> - <li><a href="#shortcuts">Shortcut keys</a></li> - <li><a href="#intents">Dynamically adding menu intents</a></li> - </ol> - </li> - </ol> +<ol> + <li><a href="#xml">Defining a Menu in XML</a></li> + <li><a href="#options-menu">Creating an Options Menu</a> + <ol> + <li><a href="#RespondingOptionsMenu">Handling click events</a></li> + <li><a href="#ChangingTheMenu">Changing menu items at runtime</a></li> + </ol> + </li> + <li><a href="#context-menu">Creating Contextual Menus</a> + <ol> + <li><a href="#FloatingContextMenu">Creating a floating context menu</a></li> + <li><a href="#CAB">Using the contextual action bar</a></li> + </ol> + </li> + <li><a href="#PopupMenu">Creating a Popup Menu</a> + <ol> + <li><a href="#PopupEvents">Handling click events</a></li> + </ol> + </li> + <li><a href="#groups">Creating Menu Groups</a> + <ol> + <li><a href="#checkable">Using checkable menu items</a></li> + </ol> + </li> + <li><a href="#intents">Adding Menu Items Based on an Intent</a> + <ol> + <li><a href="#AllowingToAdd">Allowing your activity to be added to other menus</a></li> + </ol> + </li> +</ol> <h2>Key classes</h2> <ol> <li>{@link android.view.Menu}</li> <li>{@link android.view.MenuItem}</li> <li>{@link android.view.ContextMenu}</li> - <li>{@link android.view.SubMenu}</li> + <li>{@link android.view.ActionMode}</li> </ol> <h2>See also</h2> <ol> <li><a href="{@docRoot}guide/topics/ui/actionbar.html">Action Bar</a></li> <li><a href="{@docRoot}guide/topics/resources/menu-resource.html">Menu Resource</a></li> + <li><a +href="http://android-developers.blogspot.com/2012/01/say-goodbye-to-menu-button.html">Say +Goodbye to the Menu Button</a></li> </ol> </div> </div> -<p>Menus are an important part of an activity's user interface, which provide users a familiar -way to perform actions. Android offers a simple framework for you to add standard -menus to your application.</p> +<p>Menus are a common user interface component in many types of applications. To provide a familiar +and consistent user experience, you should use the {@link android.view.Menu} APIs to present user +actions and other options in your activities.</p> + +<p>Beginning with Android 3.0 (API level 11), Android-powered devices are no longer required to +provide a dedicated <em>Menu</em> button. With this change, Android apps should migrate away from a +dependence on the traditional 6-item menu panel and instead provide an action bar to present common +user actions.</p> + +<p>Although the design and user experience for some menu items have changed, the semantics to define +a set of actions and options is still based on the {@link android.view.Menu} APIs. This +guide shows how to create the three fundamental types of menus or action presentations on all +versions of Android:</p> -<p>There are three types of application menus:</p> <dl> - <dt><strong>Options Menu</strong></dt> - <dd>The primary collection of menu items for an activity, which appears when the user touches -the MENU button. When your application is running on Android 3.0 or later, you can provide -quick access to select menu items by placing them directly in the <a -href="{@docRoot}guide/topics/ui/actionbar.html">Action Bar</a>, as "action items."</dd> - <dt><strong>Context Menu</strong></dt> - <dd>A floating list of menu items that appears when the user touches and holds a view -that's registered to provide a context menu. + <dt><strong>Options menu and action bar</strong></dt> + <dd>The <a href="#options-menu">options menu</a> is the primary collection of menu items for an +activity. It's where you should place actions that have a global impact on the app, such as +"Search," "Compose email," and "Settings." + <p>If you're developing for Android 2.3 or lower, users can +reveal the options menu panel by pressing the <em>Menu</em> button.</p> + <p>On Android 3.0 and higher, items from the options menu are presented by the <a +href="{@docRoot}guide/topics/ui/actionbar.html">action bar</a> as a combination of on-screen action +items and overflow options. Beginning with Android 3.0, the <em>Menu</em> button is deprecated (some +devices +don't have one), so you should migrate toward using the action bar to provide access to actions and +other options.</p> + <p>See the section about <a href="#options-menu">Creating an Options Menu</a>.</p> + </dd> + + <dt><strong>Context menu and contextual action mode</strong></dt> + + <dd>A context menu is a <a href="#FloatingContextMenu">floating menu</a> that appears when the +user performs a long-click on an element. It provides actions that affect the selected content or +context frame. + <p>When developing for Android 3.0 and higher, you should instead use the <a +href="#CAB">contextual action mode</a> to enable actions on selected content. This mode displays +action items that affect the selected content in a bar at the top of the screen and allows the user +to select multiple items.</p> + <p>See the section about <a href="#context-menu">Creating Contextual Menus</a>.</p> +</dd> + + <dt><strong>Popup menu</strong></dt> + <dd>A popup menu displays a list of items in a vertical list that's anchored to the view that +invoked the menu. It's good for providing an overflow of actions that relate to specific content or +to provide options for a second part of a command. Actions in a popup menu should +<strong>not</strong> directly affect the corresponding content—that's what contextual actions +are for. Rather, the popup menu is for extended actions that relate to regions of content in your +activity. + <p>See the section about <a href="#PopupMenu">Creating a Popup Menu</a>.</p> </dd> - <dt><strong>Submenu</strong></dt> - <dd>A floating list of menu items that appears when the user touches a menu item that contains -a nested menu.</dd> </dl> -<p>This document shows you how to create each type of menu, using XML to define the content of -the menu and callback methods in your activity to respond when the user selects an item.</p> +<h2 id="xml">Defining a Menu in XML</h2> -<h2 id="xml">Creating a Menu Resource</h2> +<p>For all menu types, Android provides a standard XML format to define menu items. +Instead of building a menu in your activity's code, you should define a menu and all its items in an +XML <a href="{@docRoot}guide/topics/resources/menu-resource.html">menu resource</a>. You can then +inflate the menu resource (load it as a {@link android.view.Menu} object) in your activity or +fragment.</p> -<p>Instead of instantiating a {@link android.view.Menu} in your application code, you should -define a menu and all its items in an XML <a -href="{@docRoot}guide/topics/resources/menu-resource.html">menu resource</a>, then inflate the menu -resource (load it as a programmable object) in your application code. Using a menu resource to -define your menu is a good practice because it separates the content for the menu from your -application code. It's also easier to visualize the structure and content of a menu in XML.</p> +<p>Using a menu resource is a good practice for a few reasons:</p> +<ul> + <li>It's easier to visualize the menu structure in XML.</li> + <li>It separates the content for the menu from your application's behavioral code.</li> + <li>It allows you to create alternative menu configurations for different platform versions, +screen sizes, and other configurations by leveraging the <a +href="{@docRoot}guide/topics/resources/index.html">app resources</a> framework.</li> +</ul> -<p>To create a menu resource, create an XML file inside your project's <code>res/menu/</code> +<p>To define the menu, create an XML file inside your project's <code>res/menu/</code> directory and build the menu with the following elements:</p> <dl> <dt><code><menu></code></dt> @@ -90,8 +142,8 @@ element may contain a nested <code><menu></code> element in order to create a <dt><code><group></code></dt> <dd>An optional, invisible container for {@code <item>} elements. It allows you to -categorize menu items so they share properties such as active state and visibility. See the -section about <a href="#groups">Menu groups</a>.</dd> +categorize menu items so they share properties such as active state and visibility. For more +information, see the section about <a href="#groups">Creating Menu Groups</a>.</dd> </dl> @@ -101,14 +153,17 @@ section about <a href="#groups">Menu groups</a>.</dd> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/new_game" android:icon="@drawable/ic_new_game" - android:title="@string/new_game" /> + android:title="@string/new_game" + android:showAsAction="ifRoom"/> <item android:id="@+id/help" android:icon="@drawable/ic_help" android:title="@string/help" /> </menu> </pre> -<p>This example defines a menu with two items. Each item includes the attributes:</p> +<p>The <code><item></code> element supports several attributes you can use to define an item's +appearance and behavior. The items in the above menu include the following attributes:</p> + <dl> <dt>{@code android:id}</dt> <dd>A resource ID that's unique to the item, which allows the application can recognize the item @@ -117,158 +172,175 @@ when the user selects it.</dd> <dd>A reference to a drawable to use as the item's icon.</dd> <dt>{@code android:title}</dt> <dd>A reference to a string to use as the item's title.</dd> + <dt>{@code android:showAsAction}</dt> + <dd>Specifies when and how this item should appear as an action item in the <a +href="{@docRoot}guide/topics/ui/actionbar.html">action bar</a>.</dd> </dl> -<p>There are many more attributes you can include in an {@code <item>}, including some that - specify how the item may appear in the <a -href="{@docRoot}guide/topics/ui/actionbar.html">Action Bar</a>. For more information about the XML -syntax and attributes for a menu resource, see the <a -href="{@docRoot}guide/topics/resources/menu-resource.html">Menu Resource</a> reference.</p> +<p>These are the most important attributes you should use, but there are many more available. +For information about all the supported attributes, see the <a +href="{@docRoot}guide/topics/resources/menu-resource.html">Menu Resource</a> document.</p> - - -<h2 id="Inflating">Inflating a Menu Resource</h2> - -<p>From your application code, you can inflate a menu resource (convert the XML resource into a -programmable object) using -{@link android.view.MenuInflater#inflate(int,Menu) MenuInflater.inflate()}. For -example, the following code inflates the <code>game_menu.xml</code> file defined above, during the -{@link android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} callback method, to -use the menu as the activity's Options Menu:</p> +<p>You can add a submenu to an item in any menu (except a submenu) by adding a {@code <menu>} +element as the child of an {@code <item>}. Submenus are useful when your application has a lot +of functions that can be organized into topics, like items in a PC application's menu bar (File, +Edit, View, etc.). For example:</p> <pre> -@Override -public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.game_menu, menu); - return true; -} +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/file" + android:title="@string/file" > + <!-- "file" submenu --> + <menu> + <item android:id="@+id/create_new" + android:title="@string/create_new" /> + <item android:id="@+id/open" + android:title="@string/open" /> + </menu> + </item> +</menu> </pre> -<p>The {@link android.app.Activity#getMenuInflater()} method returns a {@link -android.view.MenuInflater} for the activity. With this object, you can call {@link -android.view.MenuInflater#inflate(int,Menu) inflate()}, which inflates a menu resource into a -{@link android.view.Menu} object. In this example, the menu resource defined by -<code>game_menu.xml</code> -is inflated into the {@link android.view.Menu} that was passed into {@link -android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()}. (This callback method for -the Options Menu is discussed more in the next section.)</p> +<p>To use the menu in your activity, you need to inflate the menu resource (convert the XML +resource into a programmable object) using {@link android.view.MenuInflater#inflate(int,Menu) +MenuInflater.inflate()}. In the following sections, you'll see how to inflate a menu for each +menu type.</p> <h2 id="options-menu">Creating an Options Menu</h2> -<div class="figure" style="width:200px"> +<div class="figure" style="width:200px;margin:0"> <img src="{@docRoot}images/options_menu.png" height="333" alt="" /> - <p class="img-caption"><strong>Figure 1.</strong> Screenshot of the Options Menu in the -Browser.</p> + <p class="img-caption"><strong>Figure 1.</strong> Options menu in the +Browser, on Android 2.3.</p> </div> -<p>The Options Menu is where you should include basic activity actions and necessary navigation -items (for example, a button to open the application settings). Items in the Options Menu are -accessible in two distinct ways: the MENU button or in the <a -href="{@docRoot}guide/topics/ui/actionbar.html">Action Bar</a> (on devices running Android 3.0 -or higher).</p> - -<p>When running on a device with Android 2.3 and lower, the Options Menu appears at the bottom of -the screen, as shown in figure 1. When opened, the first visible portion of the Options Menu is -the icon menu. It holds the first six menu items. If you add more than six items to the -Options Menu, Android places the sixth item and those after it into the overflow menu, which the -user can open by touching the "More" menu item.</p> - -<p>On Android 3.0 and higher, items from the Options Menu is placed in the Action Bar, which appears -at the top of the activity in place of the traditional title bar. By default all items from the -Options Menu are placed in the overflow menu, which the user can open by touching the menu icon -on the right side of the Action Bar. However, you can place select menu items directly in the -Action Bar as "action items," for instant access, as shown in figure 2.</p> - -<p>When the Android system creates the Options Menu for the first time, it calls your -activity's {@link android.app.Activity#onCreateOptionsMenu(Menu) -onCreateOptionsMenu()} method. Override this method in your activity -and populate the {@link android.view.Menu} that is passed into the method, -{@link android.view.Menu} by inflating a menu resource as described above in <a -href="#Inflating">Inflating a Menu Resource</a>. For example:</p> +<p>The options menu is where you should include actions and other options that are relevant to the +current activity context, such as "Search," "Compose email," and "Settings."</p> + +<p>Where the items in your options menu appear on the screen depends on the version for which you've +developed your application:</p> + +<ul> + <li>If you've developed your application for <strong>Android 2.3.x (API level 10) or +lower</strong>, the contents of your options menu appear at the bottom of the screen when the user +presses the <em>Menu</em> button, as shown in figure 1. When opened, the first visible portion is +the icon +menu, which holds up to six menu items. If your menu includes more than six items, Android places +the sixth item and the rest into the overflow menu, which the user can open by selecting +<em>More</em>.</li> + + <li>If you've developed your application for <strong>Android 3.0 (API level 11) and +higher</strong>, items from the options menu are available in the <a +href="{@docRoot}guide/topics/ui/actionbar.html">action bar</a>. By default, the system +places all items in the action overflow, which the user can reveal with the action overflow icon on +the right side of the action bar (or by pressing the device <em>Menu</em> button, if available). To +enable +quick access to important actions, you can promote a few items to appear in the action bar by adding +{@code android:showAsAction="ifRoom"} to the corresponding {@code <item>} elements (see figure +2). <p>For more information about action items and other action bar behaviors, see the <a +href="{@docRoot}guide/topics/ui/actionbar.html">Action Bar</a> guide. </p> +<p class="note"><strong>Note:</strong> Even if you're <em>not</em> developing for Android 3.0 or +higher, you can build your own action bar layout for a similar effect. For an example of how you can +support older versions of Android with an action bar, see the <a +href="{@docRoot}resources/samples/ActionBarCompat/index.html">Action Bar Compatibility</a> +sample.</p> +</li> +</ul> + +<img src="{@docRoot}images/ui/actionbar.png" alt="" /> +<p class="img-caption"><strong>Figure 2.</strong> Action bar from the <a +href="{@docRoot}resources/samples/HoneycombGallery/index.html">Honeycomb Gallery</a> app, showing +navigation tabs and a camera action item (plus the action overflow button).</p> + +<p>You can declare items for the options menu from either your {@link android.app.Activity} +subclass or a {@link android.app.Fragment} subclass. If both your activity and fragment(s) +declare items for the options menu, they are combined in the UI. The activity's items appear +first, followed by those of each fragment in the order in which each fragment is added to the +activity. If necessary, you can re-order the menu items with the {@code android:orderInCategory} +attribute in each {@code <item>} you need to move.</p> + +<p>To specify the options menu for an activity, override {@link +android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} (fragments provide their +own {@link android.app.Fragment#onCreateOptionsMenu onCreateOptionsMenu()} callback). In this +method, you can inflate your menu resource (<a href="#xml">defined in XML</a>) into the {@link +android.view.Menu} provided in the callback. For example:</p> <pre> @Override public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); + MenuInflater inflater = {@link android.app.Activity#getMenuInflater()}; inflater.inflate(R.menu.game_menu, menu); return true; } </pre> -<div class="figure" style="width:450px"> -<img src="{@docRoot}images/ui/actionbar.png" alt="" /> -<p class="img-caption"><strong>Figure 2.</strong> Action bar from the <a -href="{@docRoot}resources/samples/HoneycombGallery/index.html">Honeycomb Gallery</a> app, including -navigation tabs and a camera action item (plus the overflow menu button).</p> -</div> +<p>You can also add menu items using {@link android.view.Menu#add(int,int,int,int) +add()} and retrieve items with {@link android.view.Menu#findItem findItem()} to revise their +properties with {@link android.view.MenuItem} APIs.</p> -<p>You can also populate the menu in code, using {@link android.view.Menu#add(int,int,int,int) -add()} to add items to the {@link android.view.Menu}.</p> +<p>If you've developed your application for Android 2.3.x and lower, the system calls {@link +android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} to create the options menu +when the user opens the menu for the first time. If you've developed for Android 3.0 and higher, the +system calls {@link android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} when +starting the activity, in order to show items to the action bar.</p> -<p class="note"><strong>Note:</strong> On Android 2.3 and lower, the system calls {@link -android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} to create the Options Menu -when the user opens it for the first time, but on Android 3.0 and greater, the system creates it as -soon as the activity is created, in order to populate the Action Bar.</p> -<h3 id="RespondingOptionsMenu">Responding to user action</h3> +<h3 id="RespondingOptionsMenu">Handling click events</h3> -<p>When the user selects a menu item from the Options Menu (including action items in the -Action Bar), the system calls your activity's -{@link android.app.Activity#onOptionsItemSelected(MenuItem) onOptionsItemSelected()} -method. This method passes the -{@link android.view.MenuItem} that the user selected. You can identify the menu item by calling -{@link android.view.MenuItem#getItemId()}, which returns the unique ID for the menu -item (defined by the {@code android:id} attribute in the menu resource or with an integer -given to the {@link android.view.Menu#add(int,int,int,int) add()} method). You can match this ID -against known menu items and perform the appropriate action. For example:</p> +<p>When the user selects an item from the options menu (including action items in the action bar), +the system calls your activity's {@link android.app.Activity#onOptionsItemSelected(MenuItem) +onOptionsItemSelected()} method. This method passes the {@link android.view.MenuItem} selected. You +can identify the item by calling {@link android.view.MenuItem#getItemId()}, which returns the unique +ID for the menu item (defined by the {@code android:id} attribute in the menu resource or with an +integer given to the {@link android.view.Menu#add(int,int,int,int) add()} method). You can match +this ID against known menu items to perform the appropriate action. For example:</p> <pre> @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { - case R.id.new_game: - newGame(); - return true; - case R.id.help: - showHelp(); - return true; - default: - return super.onOptionsItemSelected(item); + case R.id.new_game: + newGame(); + return true; + case R.id.help: + showHelp(); + return true; + default: + return super.onOptionsItemSelected(item); } } </pre> -<p>In this example, {@link android.view.MenuItem#getItemId()} queries the ID for the selected menu -item and the switch statement compares the ID against the resource IDs that were assigned to menu -items in the XML resource. When a switch case successfully handles the menu item, it -returns {@code true} to indicate that the item selection was handled. Otherwise, the default -statement passes the menu item to the super class, in -case it can handle the item selected. (If you've directly extended the {@link android.app.Activity} -class, then the super class returns {@code false}, but it's a good practice to -pass unhandled menu items to the super class instead of directly returning {@code false}.)</p> - -<p>Additionally, Android 3.0 adds the ability for you to define the on-click behavior for a menu -item in the <a href="{@docRoot}guide/topics/resources/menu-resource.html">menu resource</a> XML, -using the {@code android:onClick} attribute. So you don't need to implement {@link -android.app.Activity#onOptionsItemSelected(MenuItem) onOptionsItemSelected()}. Using the {@code -android:onClick} attribute, you can specify a method to call when the user selects the menu item. -Your activity must then implement the method specified in the {@code android:onClick} attribute so -that it accepts a single {@link android.view.MenuItem} parameter—when the system calls this -method, it passes the menu item selected.</p> +<p>When you successfully handle a menu item, return {@code true}. If you don't handle the menu +item, you should call the superclass implementation of {@link +android.app.Activity#onOptionsItemSelected(MenuItem) onOptionsItemSelected()} (the default +implementation returns false).</p> + +<p>If your activity includes fragments, the system first calls {@link +android.app.Activity#onOptionsItemSelected(MenuItem) onOptionsItemSelected()} for the activity then +for each fragment (in the order each fragment was added) until one returns +{@code true} or all fragments have been called.</p> + +<p class="note"><strong>Tip:</strong> Android 3.0 adds the ability for you to define the on-click +behavior for a menu item in XML, using the {@code android:onClick} attribute. The value for the +attribute must be the name of a method defined by the activity using the menu. The method +must be public and accept a single {@link android.view.MenuItem} parameter—when the system +calls this method, it passes the menu item selected. For more information and an example, see the <a +href="{@docRoot}guide/topics/resources/menu-resource.html">Menu Resource</a> document.</p> <p class="note"><strong>Tip:</strong> If your application contains multiple activities and -some of them provide the same Options Menu, consider creating +some of them provide the same options menu, consider creating an activity that implements nothing except the {@link android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} and {@link android.app.Activity#onOptionsItemSelected(MenuItem) onOptionsItemSelected()} methods. Then extend this class for each activity that should share the -same Options Menu. This way, you have to manage only one set of code for handling menu -actions and each descendant class inherits the menu behaviors.<br/><br/> -If you want to add menu items to one of your descendant activities, +same options menu. This way, you can manage one set of code for handling menu +actions and each descendant class inherits the menu behaviors. +If you want to add menu items to one of the descendant activities, override {@link android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} in that activity. Call {@code super.onCreateOptionsMenu(menu)} so the original menu items are created, then add new menu items with {@link @@ -278,180 +350,477 @@ behavior for individual menu items.</p> <h3 id="ChangingTheMenu">Changing menu items at runtime</h3> -<p>Once the activity is created, the {@link android.app.Activity#onCreateOptionsMenu(Menu) -onCreateOptionsMenu()} method is -called only once, as described above. The system keeps and re-uses the {@link -android.view.Menu} you define in this method until your activity is destroyed. If you want to change -the Options Menu any time after it's first created, you must override the -{@link android.app.Activity#onPrepareOptionsMenu(Menu) onPrepareOptionsMenu()} method. This passes -you the {@link android.view.Menu} object as it currently exists. This is useful if you'd like to -remove, add, disable, or enable menu items depending on the current state of your application.</p> - -<p>On Android 2.3 and lower, the system calls {@link android.app.Activity#onPrepareOptionsMenu(Menu) -onPrepareOptionsMenu()} each time the user opens the Options Menu.</p> +<p>After the system calls {@link android.app.Activity#onCreateOptionsMenu(Menu) +onCreateOptionsMenu()}, it retains an instance of the {@link android.view.Menu} you populate and +will not call {@link android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} +again unless the menu is invalidated for some reason. However, you should use {@link +android.app.Activity#onCreateOptionsMenu(Menu) onCreateOptionsMenu()} only to create the initial +menu state and not to make changes during the activity lifecycle.</p> + +<p>If you want to modify the options menu based on +events that occur during the activity lifecycle, you can do so in +the {@link android.app.Activity#onPrepareOptionsMenu(Menu) onPrepareOptionsMenu()} method. This +method passes you the {@link android.view.Menu} object as it currently exists so you can modify it, +such as add, remove, or disable items. (Fragments also provide an {@link +android.app.Fragment#onPrepareOptionsMenu onPrepareOptionsMenu()} callback.)</p> + +<p>On Android 2.3.x and lower, the system calls {@link +android.app.Activity#onPrepareOptionsMenu(Menu) +onPrepareOptionsMenu()} each time the user opens the options menu (presses the <em>Menu</em> +button).</p> -<p>On Android 3.0 and higher, you must call {@link android.app.Activity#invalidateOptionsMenu -invalidateOptionsMenu()} when you want to update the menu, because the menu is always open. The -system will then call {@link android.app.Activity#onPrepareOptionsMenu(Menu) onPrepareOptionsMenu()} -so you can update the menu items.</p> +<p>On Android 3.0 and higher, the options menu is considered to always be open when menu items are +presented in the action bar. When an event occurs and you want to perform a menu update, you must +call {@link android.app.Activity#invalidateOptionsMenu invalidateOptionsMenu()} to request that the +system call {@link android.app.Activity#onPrepareOptionsMenu(Menu) onPrepareOptionsMenu()}.</p> <p class="note"><strong>Note:</strong> -You should never change items in the Options Menu based on the {@link android.view.View} currently +You should never change items in the options menu based on the {@link android.view.View} currently in focus. When in touch mode (when the user is not using a trackball or d-pad), views cannot take focus, so you should never use focus as the basis for modifying -items in the Options Menu. If you want to provide menu items that are context-sensitive to a {@link +items in the options menu. If you want to provide menu items that are context-sensitive to a {@link android.view.View}, use a <a href="#context-menu">Context Menu</a>.</p> -<p>If you're developing for Android 3.0 or higher, be sure to also read the <a -href="{@docRoot}guide/topics/ui/actionbar.html">Action Bar</a> developer guide.</p> +<h2 id="context-menu">Creating Contextual Menus</h2> -<h2 id="context-menu">Creating a Context Menu</h2> +<div class="figure" style="width:420px;margin-top:-1em"> + <img src="{@docRoot}images/ui/menu-context.png" alt="" /> + <p class="img-caption"><strong>Figure 3.</strong> Screenshots of a floating context menu (left) +and the contextual action bar (right).</p> +</div> -<p>A context menu is conceptually similar to the menu displayed when the user performs a -"right-click" on a PC. You should use a context menu to provide the user access to -actions that pertain to a specific item in the user interface. On Android, a context menu is -displayed when the user performs a "long press" (press and hold) on an item.</p> +<p>A contextual menu offers actions that affect a specific item or context frame in the UI. You +can provide a context menu for any view, but they are most often used for items in a {@link +android.widget.ListView}, {@link android.widget.GridView}, or other view collections in which +the user can perform direct actions on each item.</p> -<p>You can create a context menu for any View, though context menus are most often used for items in -a {@link android.widget.ListView}. When the user performs a long-press on an item in a ListView and -the list is registered to provide a context menu, the list item signals to the user that a context -menu is available by animating its background color—it transitions from -orange to white before opening the context menu. (The Contacts application demonstrates this -feature.)</p> +<p>There are two ways to provide contextual actions:</p> +<ul> + <li>In a <a href="#FloatingContextMenu">floating context menu</a>. A menu appears as a +floating list of menu items (similar to a dialog) when the user performs a long-click (press and +hold) on a view that declares support for a context menu. Users can perform a contextual +action on one item at a time.</li> + + <li>In the <a href="#CAB">contextual action mode</a>. This mode is a system implementation of +{@link android.view.ActionMode} that displays a <em>contextual action bar</em> at the top of the +screen with action items that affect the selected item(s). When this mode is active, users +can perform an action on multiple items at once (if your app allows it).</li> +</ul> -<div class="sidebox-wrapper"> -<div class="sidebox"> -<h3>Register a ListView</h3> -<p>If your activity uses a {@link android.widget.ListView} and -you want all list items to provide a context menu, register all items for a context -menu by passing the {@link android.widget.ListView} to {@link -android.app.Activity#registerForContextMenu(View) registerForContextMenu()}. For -example, if you're using a {@link android.app.ListActivity}, register all list items like this:</p> -<p><code>registerForContextMenu({@link android.app.ListActivity#getListView()});</code></p> -</div> -</div> +<p class="note"><strong>Note:</strong> The contextual action mode is available on Android 3.0 (API +level 11) and higher and is the preferred technique for displaying contextual actions when +available. If your app supports versions lower than 3.0 then you should fall back to a floating +context menu on those devices.</p> -<p>In order for a View to provide a context menu, you must "register" the view for a context -menu. Call {@link android.app.Activity#registerForContextMenu(View) registerForContextMenu()} and -pass it the {@link android.view.View} you want to give a context menu. When this View then -receives a long-press, it displays a context menu.</p> -<p>To define the context menu's appearance and behavior, override your activity's context menu -callback methods, {@link android.app.Activity#onCreateContextMenu(ContextMenu,View,ContextMenuInfo) -onCreateContextMenu()} and -{@link android.app.Activity#onContextItemSelected(MenuItem) onContextItemSelected()}.</p> +<h3 id="FloatingContextMenu">Creating a floating context menu</h3> -<p>For example, here's an {@link -android.app.Activity#onCreateContextMenu(ContextMenu,View,ContextMenuInfo) -onCreateContextMenu()} that uses the {@code context_menu.xml} menu resource:</p> +<p>To provide a floating context menu:</p> +<ol> + <li>Register the {@link android.view.View} to which the context menu should be associated by +calling {@link android.app.Activity#registerForContextMenu(View) registerForContextMenu()} and pass +it the {@link android.view.View}. + <p>If your activity uses a {@link android.widget.ListView} or {@link android.widget.GridView} and +you want each item to provide the same context menu, register all items for a context menu by +passing the {@link android.widget.ListView} or {@link android.widget.GridView} to {@link +android.app.Activity#registerForContextMenu(View) registerForContextMenu()}.</p> +</li> + + <li>Implement the {@link +android.view.View.OnCreateContextMenuListener#onCreateContextMenu onCreateContextMenu()} method +in your {@link android.app.Activity} or {@link android.app.Fragment}. + <p>When the registered view receives a long-click event, the system calls your {@link +android.view.View.OnCreateContextMenuListener#onCreateContextMenu onCreateContextMenu()} +method. This is where you define the menu items, usually by inflating a menu resource. For +example:</p> <pre> @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.context_menu, menu); + super.onCreateContextMenu(menu, v, menuInfo); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.context_menu, menu); } </pre> -<p>{@link android.view.MenuInflater} is used to inflate the context menu from a <a -href="{@docRoot}guide/topics/resources/menu-resource.html">menu resource</a>. (You can also use -{@link android.view.Menu#add(int,int,int,int) add()} to add menu items.) The callback method +<p>{@link android.view.MenuInflater} allows you to inflate the context menu from a <a +href="{@docRoot}guide/topics/resources/menu-resource.html">menu resource</a>. The callback method parameters include the {@link android.view.View} that the user selected and a {@link android.view.ContextMenu.ContextMenuInfo} object that provides -additional information about the item selected. You might use these parameters to determine -which context menu should be created, but in this example, all context menus for the activity are -the same.</p> +additional information about the item selected. If your activity has several views that each provide +a different context menu, you might use these parameters to determine which context menu to +inflate.</p> +</li> -<p>Then when the user selects an item from the context menu, the system calls {@link -android.app.Activity#onContextItemSelected(MenuItem) onContextItemSelected()}. Here is an example -of how you can handle selected items:</p> +<li>Implement {@link android.app.Activity#onContextItemSelected(MenuItem) +onContextItemSelected()}. + <p>When the user selects a menu item, the system calls this method so you can perform the +appropriate action. For example:</p> <pre> @Override public boolean onContextItemSelected(MenuItem item) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); - switch (item.getItemId()) { - case R.id.edit: - editNote(info.id); - return true; - case R.id.delete: - deleteNote(info.id); - return true; - default: - return super.onContextItemSelected(item); - } + AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); + switch (item.getItemId()) { + case R.id.edit: + editNote(info.id); + return true; + case R.id.delete: + deleteNote(info.id); + return true; + default: + return super.onContextItemSelected(item); + } } </pre> -<p>The structure of this code is similar to the example for <a href="#options-menu">Creating an -Options Menu</a>, in which {@link android.view.MenuItem#getItemId()} queries the ID for the selected -menu item and a switch statement matches the item to the IDs that are defined in the menu resource. -And like the options menu example, the default statement calls the super class in case it -can handle menu items not handled here, if necessary.</p> +<p>The {@link android.view.MenuItem#getItemId()} method queries the ID for +the selected menu item, which you should assign to each menu item in XML using the {@code +android:id} attribute, as shown in the section about <a href="#xml">Defining a Menu in +XML</a>.</p> + +<p>When you successfully handle a menu item, return {@code true}. If you don't handle the menu item, +you should pass the menu item to the superclass implementation. If your activity includes fragments, +the activity receives this callback first. By calling the superclass when unhandled, the system +passes the event to the respective callback method in each fragment, one at a time (in the order +each fragment was added) until {@code true} or {@code false} is returned. (The default +implementation for {@link android.app.Activity} and {@code android.app.Fragment} return {@code +false}, so you should always call the superclass when unhandled.)</p> +</li> +</ol> + + +<h3 id="CAB">Using the contextual action mode</h3> -<p>In this example, the selected item is an item from a {@link android.widget.ListView}. To -perform an action on the selected item, the application needs to know the list -ID for the selected item (it's position in the ListView). To get the ID, the application calls -{@link android.view.MenuItem#getMenuInfo()}, which returns a {@link -android.widget.AdapterView.AdapterContextMenuInfo} object that includes the list ID for the -selected item in the {@link android.widget.AdapterView.AdapterContextMenuInfo#id id} field. The -local methods <code>editNote()</code> and <code>deleteNote()</code> methods accept this list ID to -perform an action on the data specified by the list ID.</p> +<p>The contextual action mode is a system implementation of {@link android.view.ActionMode} that +focuses user interaction toward performing contextual actions. When a +user enables this mode by selecting an item, a <em>contextual action bar</em> appears at the top of +the screen to present actions the user can perform on the currently selected item(s). While this +mode is enabled, the user can select multiple items (if you allow it), deselect items, and continue +to navigate within the activity (as much as you're willing to allow). The action mode is disabled +and the contextual action bar disappears when the user deselects all items, presses the BACK button, +or selects the <em>Done</em> action on the left side of the bar.</p> -<p class="note"><strong>Note:</strong> Items in a context menu do not support icons or shortcut -keys.</p> +<p class="note"><strong>Note:</strong> The contextual action bar is not necessarily +associated with the <a href="{@docRoot}guide/topics/ui/actionbar.html">action bar</a>. They operate +independently, even though the contextual action bar visually overtakes the action bar +position.</p> +<p>If you're developing for Android 3.0 (API level 11) or higher, you +should usually use the contextual action mode to present contextual actions, instead of the <a +href="#FloatingContextMenu">floating context menu</a>.</p> +<p>For views that provide contextual actions, you should usually invoke the contextual action mode +upon one of two events (or both):</p> +<ul> + <li>The user performs a long-click on the view.</li> + <li>The user selects a checkbox or similar UI component within the view.</li> +</ul> + +<p>How your application invokes the contextual action mode and defines the behavior for each +action depends on your design. There are basically two designs:</p> +<ul> + <li>For contextual actions on individual, arbitrary views.</li> + <li>For batch contextual actions on groups of items in a {@link +android.widget.ListView} or {@link android.widget.GridView} (allowing the user to select multiple +items and perform an action on them all).</li> +</ul> -<h2 id="submenu">Creating Submenus</h2> +<p>The following sections describe the setup required for each scenario.</p> -<p>A submenu is a menu that the user can open by selecting an item in another menu. You can add a -submenu to any menu (except a submenu). Submenus are useful when your application has a lot of -functions that can be organized into topics, like items in a PC application's menu bar (File, Edit, -View, etc.).</p> -<p>When creating your <a href="{@docRoot}guide/topics/resources/menu-resource.html">menu -resource</a>, you can create a submenu by adding a {@code <menu>} element as the child of an -{@code <item>}. For example:</p> +<h4 id="CABforViews">Enabling the contextual action mode for individual views</h4> +<p>If you want to invoke the contextual action mode only when the user selects specific +views, you should:</p> +<ol> + <li>Implement the {@link android.view.ActionMode.Callback} interface. In its callback methods, you +can specify the actions for the contextual action bar, respond to click events on action items, and +handle other lifecycle events for the action mode.</li> + <li>Call {@link android.app.Activity#startActionMode startActionMode()} when you want to show the +bar (such as when the user long-clicks the view).</li> +</ol> + +<p>For example:</p> + +<ol> + <li>Implement the {@link android.view.ActionMode.Callback ActionMode.Callback} interface: <pre> -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@+id/file" - android:icon="@drawable/file" - android:title="@string/file" > - <!-- "file" submenu --> - <menu> - <item android:id="@+id/create_new" - android:title="@string/create_new" /> - <item android:id="@+id/open" - android:title="@string/open" /> - </menu> - </item> -</menu> +private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { + + // Called when the action mode is created; startActionMode() was called + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // Inflate a menu resource providing context menu items + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.context_menu, menu); + return true; + } + + // Called each time the action mode is shown. Always called after onCreateActionMode, but + // may be called multiple times if the mode is invalidated. + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; // Return false if nothing is done + } + + // Called when the user selects a contextual menu item + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_share: + shareCurrentItem(); + mode.finish(); // Action picked, so close the CAB + return true; + default: + return false; + } + } + + // Called when the user exits the action mode + @Override + public void onDestroyActionMode(ActionMode mode) { + mActionMode = null; + } +}; +</pre> + +<p>Notice that these event callbacks are almost exactly the same as the callbacks for the <a +href="#options-menu">options menu</a>, except each of these also pass the {@link +android.view.ActionMode} object associated with the event. You can use {@link +android.view.ActionMode} APIs to make various changes to the CAB, such as revise the title and +subtitle with {@link android.view.ActionMode#setTitle setTitle()} and {@link +android.view.ActionMode#setSubtitle setSubtitle()} (useful to indicate how many items are +selected).</p> + +<p>Also notice that the above sample sets the {@code mActionMode} variable null when the +action mode is destroyed. In the next step, you'll see how it's initialized and how saving +the member variable in your activity or fragment can be useful.</p> +</li> + + <li>Call {@link android.app.Activity#startActionMode startActionMode()} to enable the contextual +action mode when appropriate, such as in response to a long-click on a {@link +android.view.View}:</p> + +<pre> +someView.setOnLongClickListener(new View.OnLongClickListener() { + // Called when the user long-clicks on someView + public boolean onLongClick(View view) { + if (mActionMode != null) { + return false; + } + + // Start the CAB using the ActionMode.Callback defined above + mActionMode = getActivity().startActionMode(mActionModeCallback); + view.setSelected(true); + return true; + } +}); +</pre> + +<p>When you call {@link android.app.Activity#startActionMode startActionMode()}, the system returns +the {@link android.view.ActionMode} created. By saving this in a member variable, you can +make changes to the contextual action bar in response to other events. In the above sample, the +{@link android.view.ActionMode} is used to ensure that the {@link android.view.ActionMode} instance +is not recreated if it's already active, by checking whether the member is null before starting the +action mode.</p> +</li> +</ol> + + + +<h4 id="CABforListView">Enabling batch contextual actions in a ListView or GridView</h4> + +<p>If you have a collection of items in a {@link android.widget.ListView} or {@link +android.widget.GridView} (or another extension of {@link android.widget.AbsListView}) and want to +allow users to perform batch actions, you should:</p> + +<ul> + <li>Implement the {@link android.widget.AbsListView.MultiChoiceModeListener} interface and set it +for the view group with {@link android.widget.AbsListView#setMultiChoiceModeListener +setMultiChoiceModeListener()}. In the listener's callback methods, you can specify the actions +for the contextual action bar, respond to click events on action items, and handle other callbacks +inherited from the {@link android.view.ActionMode.Callback} interface.</li> + + <li>Call {@link android.widget.AbsListView#setChoiceMode setChoiceMode()} with the {@link +android.widget.AbsListView#CHOICE_MODE_MULTIPLE_MODAL} argument.</li> +</ul> + +<p>For example:</p> + +<pre> +ListView listView = getListView(); +listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); +listView.setMultiChoiceModeListener(new MultiChoiceModeListener() { + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, + long id, boolean checked) { + // Here you can do something when items are selected/de-selected, + // such as update the title in the CAB + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // Respond to clicks on the actions in the CAB + switch (item.getItemId()) { + case R.id.menu_delete: + deleteSelectedItems(); + mode.finish(); // Action picked, so close the CAB + return true; + default: + return false; + } + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // Inflate the menu for the CAB + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.context, menu); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // Here you can make any necessary updates to the activity when + // the CAB is removed. By default, selected items are deselected/unchecked. + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // Here you can perform updates to the CAB due to + // an {@link android.view.ActionMode#invalidate} request + return false; + } +}); +</pre> + +<p>That's it. Now when the user selects an item with a long-click, the system calls the {@link +android.widget.AbsListView.MultiChoiceModeListener#onCreateActionMode onCreateActionMode()} +method and displays the contextual action bar with the specified actions. While the contextual +action bar is visible, users can select additional items.</p> + +<p>In some cases in which the contextual actions provide common action items, you might +want to add a checkbox or a similar UI element that allows users to select items, because they +might not discover the long-click behavior. When a user selects the checkbox, you +can invoke the contextual action mode by setting the respective list item to the checked +state with {@link android.widget.AbsListView#setItemChecked setItemChecked()}.</p> + + + + +<h2 id="PopupMenu">Creating a Popup Menu</h2> + +<div class="figure" style="width:220px"> +<img src="{@docRoot}images/ui/popupmenu.png" alt="" /> +<p><strong>Figure 4.</strong> A popup menu in the Gmail app, anchored to the overflow +button at the top-right.</p> +</div> + +<p>A {@link android.widget.PopupMenu} is a modal menu anchored to a {@link android.view.View}. +It appears below the anchor view if there is room, or above the view otherwise. It's useful for:</p> +<ul> + <li>Providing an overflow-style menu for actions that <em>relate to</em> specific content (such as +Gmail's email headers, shown in figure 4). + <p class="note"><strong>Note:</strong> This is not the same as a context menu, which is +generally for actions that <em>affect</em> selected content. For actions that affect selected +content, use the <a href="#CAB">contextual action mode</a> or <a +href="#FloatingContextMenu">floating context menu</a>.</p></li> + <li>Providing a second part of a command sentence (such as a button marked "Add" +that produces a popup menu with different "Add" options).</li> + <li>Providing a drop-down similar to {@link android.widget.Spinner} that does not retain +a persistent selection.</li> +</ul> + + +<p class="note"><strong>Note:</strong> {@link android.widget.PopupMenu} is available with API +level 11 and higher.</p> + +<p>If you <a href="#xml">define your menu in XML</a>, here's how you can show the popup menu:</p> +<ol> + <li>Instantate a {@link android.widget.PopupMenu} with its constructor, which takes the +current application {@link android.content.Context} and the {@link android.view.View} to which the +menu should be anchored.</li> + <li>Use {@link android.view.MenuInflater} to inflate your menu resource into the {@link +android.view.Menu} object returned by {@link +android.widget.PopupMenu#getMenu() PopupMenu.getMenu()}. On API level 14 and above, you can use +{@link android.widget.PopupMenu#inflate PopupMenu.inflate()} instead.</li> + <li>Call {@link android.widget.PopupMenu#show() PopupMenu.show()}.</li> +</ol> + +<p>For example, here's a button with the {@link android.R.attr#onClick android:onClick} attribute +that shows a popup menu:</p> + +<pre> +<ImageButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_overflow_holo_dark" + android:contentDescription="@string/descr_overflow_button" + android:onClick="showPopup" /> +</pre> + +<p>The activity can then show the popup menu like this:</p> + +<pre> +public void showPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.actions, popup.getMenu()); + popup.show(); +} </pre> -<p>When the user selects an item from a submenu, the parent menu's respective on-item-selected -callback method receives the event. For instance, if the above menu is applied as an Options Menu, -then the {@link android.app.Activity#onOptionsItemSelected(MenuItem) onOptionsItemSelected()} method -is called when a submenu item is selected.</p> +<p>In API level 14 and higher, you can combine the two lines that inflate the menu with {@link +android.widget.PopupMenu#inflate PopupMenu.inflate()}.</p> + +<p>The menu is dismissed when the user selects an item or touches outside the menu +area. You can listen for the dismiss event using {@link +android.widget.PopupMenu.OnDismissListener}.</p> + +<h3 id="PopupEvents">Handling click events</h3> -<p>You can also use {@link android.view.Menu#addSubMenu(int,int,int,int) addSubMenu()} to -dynamically add a {@link android.view.SubMenu} to an existing {@link android.view.Menu}. This -returns the new {@link android.view.SubMenu} object, to which you can add -submenu items, using {@link android.view.Menu#add(int,int,int,int) add()}</p> +<p>To perform an +action when the user selects a menu item, you must implement the {@link +android.widget.PopupMenu.OnMenuItemClickListener} interface and register it with your {@link +android.widget.PopupMenu} by calling {@link android.widget.PopupMenu#setOnMenuItemClickListener +setOnMenuItemclickListener()}. When the user selects an item, the system calls the {@link +android.widget.PopupMenu.OnMenuItemClickListener#onMenuItemClick onMenuItemClick()} callback in +your interface.</p> +<p>For example:</p> +<pre> +public void showMenu(View v) { + PopupMenu popup = new PopupMenu(this, v); -<h2 id="features">Other Menu Features</h2> + // This activity implements OnMenuItemClickListener + popup.setOnMenuItemClickListener(this); + popup.inflate(R.menu.actions); + popup.show(); +} + +@Override +public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.archive: + archive(item); + return true; + case R.id.delete: + delete(item); + return true; + default: + return false; + } +} +</pre> -<p>Here are some other features that you can apply to most menu items.</p> -<h3 id="groups">Menu groups</h3> +<h2 id="groups">Creating Menu Groups</h2> <p>A menu group is a collection of menu items that share certain traits. With a group, you can:</p> @@ -473,38 +842,41 @@ android.view.Menu#add(int,int,int,int) add()} method.</p> <pre> <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@+id/item1" - android:icon="@drawable/item1" - android:title="@string/item1" /> + <item android:id="@+id/menu_save" + android:icon="@drawable/menu_save" + android:title="@string/menu_save" /> <!-- menu group --> - <group android:id="@+id/group1"> - <item android:id="@+id/groupItem1" - android:title="@string/groupItem1" /> - <item android:id="@+id/groupItem2" - android:title="@string/groupItem2" /> + <group android:id="@+id/group_delete"> + <item android:id="@+id/menu_archive" + android:title="@string/menu_archive" /> + <item android:id="@+id/menu_delete" + android:title="@string/menu_delete" /> </group> </menu> </pre> -<p>The items that are in the group appear the same as the first item that is not in a -group—all three items in the menu are siblings. However, you can modify the traits of the two -items in the group by referencing the group ID and using the methods listed above.</p> +<p>The items that are in the group appear at the same level as the first item—all three items +in the menu are siblings. However, you can modify the traits of the two +items in the group by referencing the group ID and using the methods listed above. The system +will also never separate grouped items. For example, if you declare {@code +android:showAsAction="ifRoom"} for each item, they will either both appear in the action +bar or both appear in the action overflow.</p> -<h3 id="checkable">Checkable menu items</h3> +<h3 id="checkable">Using checkable menu items</h3> <div class="figure" style="width:200px"> <img src="{@docRoot}images/radio_buttons.png" height="333" alt="" /> - <p class="img-caption"><strong>Figure 3.</strong> Screenshot of a submenu with checkable + <p class="img-caption"><strong>Figure 5.</strong> Screenshot of a submenu with checkable items.</p> </div> <p>A menu can be useful as an interface for turning options on and off, using a checkbox for stand-alone options, or radio buttons for groups of -mutually exclusive options. Figure 2 shows a submenu with items that are checkable with radio +mutually exclusive options. Figure 5 shows a submenu with items that are checkable with radio buttons.</p> -<p class="note"><strong>Note:</strong> Menu items in the Icon Menu (from the Options Menu) cannot +<p class="note"><strong>Note:</strong> Menu items in the Icon Menu (from the options menu) cannot display a checkbox or radio button. If you choose to make items in the Icon Menu checkable, you must manually indicate the checked state by swapping the icon and/or text each time the state changes.</p> @@ -550,15 +922,15 @@ user selected it) with {@link android.view.MenuItem#isChecked()} and then set th <pre> @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.vibrate: - case R.id.dont_vibrate: - if (item.isChecked()) item.setChecked(false); - else item.setChecked(true); - return true; - default: - return super.onOptionsItemSelected(item); - } + switch (item.getItemId()) { + case R.id.vibrate: + case R.id.dont_vibrate: + if (item.isChecked()) item.setChecked(false); + else item.setChecked(true); + return true; + default: + return super.onOptionsItemSelected(item); + } } </pre> @@ -575,30 +947,8 @@ you should store the data using <a href="{@docRoot}guide/topics/data/data-storage.html#pref">Shared Preferences</a>.</p> -<h3 id="shortcuts">Shortcut keys</h3> - -<p>To facilitate quick access to items in the Options Menu when the user's device has a hardware -keyboard, you can add quick-access shortcut keys using letters and/or numbers, with the -{@code android:alphabeticShortcut} and {@code android:numericShortcut} attributes in the {@code -<item>} element. You can also use the methods {@link -android.view.MenuItem#setAlphabeticShortcut(char)} and {@link -android.view.MenuItem#setNumericShortcut(char)}. Shortcut keys are <em>not</em> -case sensitive.</p> - -<p>For example, if you apply the "s" character as an alphabetic shortcut to a "save" menu item, then -when the menu is open (or while the user holds the MENU button) and the user presses the "s" key, -the "save" menu item is selected.</p> - -<p>This shortcut key is displayed as a tip in the menu item, below the menu item name -(except for items in the Icon Menu, which are displayed only if the user holds the MENU -button).</p> - -<p class="note"><strong>Note:</strong> Shortcut keys for menu items only work on devices with a -hardware keyboard. Shortcuts cannot be added to items in a Context Menu.</p> - - -<h3 id="intents">Dynamically adding menu intents</h3> +<h2 id="intents">Adding Menu Items Based on an Intent</h2> <p>Sometimes you'll want a menu item to launch an activity using an {@link android.content.Intent} (whether it's an activity in your application or another application). When you know the intent you @@ -671,7 +1021,7 @@ addIntentOptions()}, it overrides any and all menu items by the menu group speci argument.</p> -<h4>Allowing your activity to be added to other menus</h4> +<h3 id="AllowingToAdd">Allowing your activity to be added to other menus</h3> <p>You can also offer the services of your activity to other applications, so your application can be included in the menu of others (reverse the roles described above).</p> @@ -681,7 +1031,7 @@ filter as usual, but be sure to include the {@link android.content.Intent#CATEGO and/or {@link android.content.Intent#CATEGORY_SELECTED_ALTERNATIVE} values for the intent filter category. For example:</p> <pre> -<intent-filter label="Resize Image"> +<intent-filter label="@string/resize_image"> ... <category android:name="android.intent.category.ALTERNATIVE" /> <category android:name="android.intent.category.SELECTED_ALTERNATIVE" /> diff --git a/docs/html/images/ui/menu-context.png b/docs/html/images/ui/menu-context.png Binary files differnew file mode 100644 index 000000000000..f6975fbf2df4 --- /dev/null +++ b/docs/html/images/ui/menu-context.png diff --git a/docs/html/images/ui/popupmenu.png b/docs/html/images/ui/popupmenu.png Binary files differnew file mode 100644 index 000000000000..5c9982170461 --- /dev/null +++ b/docs/html/images/ui/popupmenu.png diff --git a/docs/html/resources/dashboard/opengl.jd b/docs/html/resources/dashboard/opengl.jd index 357c1ea4b797..d55ab2b54935 100644 --- a/docs/html/resources/dashboard/opengl.jd +++ b/docs/html/resources/dashboard/opengl.jd @@ -57,7 +57,7 @@ ending on the data collection date noted below.</p> <div class="dashboard-panel"> <img alt="" width="400" height="250" -src="http://chart.googleapis.com/chart?cht=p&chs=400x250&chco=c4df9b,6fad0c&chl=GL%201.1|GL%202.0%20%26%201.1&chd=t%3A9.5,90.5" /> +src="http://chart.googleapis.com/chart?cht=p&chs=400x250&chco=c4df9b,6fad0c&chl=GL%201.1%20only|GL%202.0%20%26%201.1&chd=t%3A10.7,89.3" /> <table> <tr> @@ -65,15 +65,15 @@ src="http://chart.googleapis.com/chart?cht=p&chs=400x250&chco=c4df9b,6fad0c&chl= <th scope="col">Distribution</th> </tr> <tr> -<td>1.1</th> -<td>9.5%</td> +<td>1.1 only</th> +<td>10.7%</td> </tr> <tr> -<td>2.0</th> -<td>90.5%</td> +<td>2.0 & 1.1</th> +<td>89.3%</td> </tr> </table> -<p><em>Data collected during a 7-day period ending on January 3, 2012</em></p> +<p><em>Data collected during a 7-day period ending on February 1, 2012</em></p> </div> diff --git a/docs/html/resources/dashboard/platform-versions.jd b/docs/html/resources/dashboard/platform-versions.jd index 2618a0424b49..4ea52aff393d 100644 --- a/docs/html/resources/dashboard/platform-versions.jd +++ b/docs/html/resources/dashboard/platform-versions.jd @@ -52,7 +52,7 @@ Android Market within a 14-day period ending on the data collection date noted b <div class="dashboard-panel"> <img alt="" height="250" width="470" -src="http://chart.apis.google.com/chart?&cht=p&chs=460x250&chd=t:0.6,1.1,8.5,30.4,0.6,54.9,0.1,1.5,1.7,0.3,0.3&chl=Android%201.5|Android%201.6|Android%202.1|Android%202.2|Android%202.3|Android%202.3.3|Android%203.0|Android%203.1|Android%203.2|Android%204.0|Android%204.0.3&chco=c4df9b,6fad0c" /> +src="http://chart.apis.google.com/chart?&cht=p&chs=460x250&chd=t:0.6,1.0,7.6,27.8,0.5,58.1,0.1,1.4,1.9,0.3,0.7&chl=Android%201.5|Android%201.6|Android%202.1|Android%202.2|Android%202.3|Android%202.3.3|Android%203.0|Android%203.1|Android%203.2|Android%204.0|Android%204.0.3&chco=c4df9b,6fad0c" /> <table> <tr> @@ -62,24 +62,24 @@ src="http://chart.apis.google.com/chart?&cht=p&chs=460x250&chd=t:0.6,1.1,8.5,30. <th>Distribution</th> </tr> <tr><td><a href="{@docRoot}sdk/android-1.5.html">Android 1.5</a></td><td>Cupcake</td> <td>3</td><td>0.6%</td></tr> -<tr><td><a href="{@docRoot}sdk/android-1.6.html">Android 1.6</a></td><td>Donut</td> <td>4</td><td>1.1%</td></tr> -<tr><td><a href="{@docRoot}sdk/android-2.1.html">Android 2.1</a></td><td>Eclair</td> <td>7</td><td>8.5%</td></tr> -<tr><td><a href="{@docRoot}sdk/android-2.2.html">Android 2.2</a></td><td>Froyo</td> <td>8</td><td>30.4%</td></tr> +<tr><td><a href="{@docRoot}sdk/android-1.6.html">Android 1.6</a></td><td>Donut</td> <td>4</td><td>1.0%</td></tr> +<tr><td><a href="{@docRoot}sdk/android-2.1.html">Android 2.1</a></td><td>Eclair</td> <td>7</td><td>7.6%</td></tr> +<tr><td><a href="{@docRoot}sdk/android-2.2.html">Android 2.2</a></td><td>Froyo</td> <td>8</td><td>27.8%</td></tr> <tr><td><a href="{@docRoot}sdk/android-2.3.html">Android 2.3 -<br/> - Android 2.3.2</a></td><td rowspan="2">Gingerbread</td> <td>9</td><td>0.6%</td></tr> + Android 2.3.2</a></td><td rowspan="2">Gingerbread</td> <td>9</td><td>0.5%</td></tr> <tr><td><a href="{@docRoot}sdk/android-2.3.3.html">Android 2.3.3 -<br/> - Android 2.3.7</a></td><!-- Gingerbread --> <td>10</td><td>54.9%</td></tr> + Android 2.3.7</a></td><!-- Gingerbread --> <td>10</td><td>58.1%</td></tr> <tr><td><a href="{@docRoot}sdk/android-3.0.html">Android 3.0</a></td> <td rowspan="3">Honeycomb</td> <td>11</td><td>0.1%</td></tr> -<tr><td><a href="{@docRoot}sdk/android-3.1.html">Android 3.1</a></td><!-- Honeycomb --><td>12</td><td>1.5%</td></tr> -<tr><td><a href="{@docRoot}sdk/android-3.2.html">Android 3.2</a></td><!-- Honeycomb --><td>13</td><td>1.7%</td></tr> +<tr><td><a href="{@docRoot}sdk/android-3.1.html">Android 3.1</a></td><!-- Honeycomb --><td>12</td><td>1.4%</td></tr> +<tr><td><a href="{@docRoot}sdk/android-3.2.html">Android 3.2</a></td><!-- Honeycomb --><td>13</td><td>1.9%</td></tr> <tr><td><a href="{@docRoot}sdk/android-4.0.html">Android 4.0 -<br/> Android 4.0.2</a></td> <td rowspan="2">Ice Cream Sandwich</td><td>14</td><td>0.3%</td></tr> -<tr><td><a href="{@docRoot}sdk/android-4.0.3.html">Android 4.0.3</a></td><!-- ICS --><td>15</td><td>0.3%</td></tr> +<tr><td><a href="{@docRoot}sdk/android-4.0.3.html">Android 4.0.3</a></td><!-- ICS --><td>15</td><td>0.7%</td></tr> </table> -<p><em>Data collected during a 14-day period ending on January 3, 2012</em></p> +<p><em>Data collected during a 14-day period ending on February 1, 2012</em></p> <!-- <p style="font-size:.9em">* <em>Other: 0.1% of devices running obsolete versions</em></p> --> @@ -108,9 +108,9 @@ Android Market within a 14-day period ending on the date indicated on the x-axis <div class="dashboard-panel"> <img alt="" height="250" width="660" style="padding:5px;background:#fff" -src="http://chart.apis.google.com/chart?&cht=lc&chs=660x250&chxt=x,x,y,r&chxr=0,0,12|1,0,12|2,0,100|3,0,100&chxl=0%3A%7C07/01%7C07/15%7C08/01%7C08/15%7C09/01%7C09/15%7C10/01%7C10/15%7C11/01%7C11/15%7C12/01%7C12/15%7C01/01%7C1%3A%7C2011%7C%7C%7C%7C%7C%7C%7C%7C%7C%7C%7C%7C2012%7C2%3A%7C0%25%7C25%25%7C50%25%7C75%25%7C100%25%7C3%3A%7C0%25%7C25%25%7C50%25%7C75%25%7C100%25&chxp=0,0,1,2,3,4,5,6,7,8,9,10,11,12&chxtc=0,5&chd=t:99.2,99.0,98.8,98.7,98.5,98.5,98.2,98.1,98.0,99.9,99.9,99.7,99.2|97.7,97.6,97.5,97.5,97.5,97.5,97.1,97.1,97.0,99.1,99.1,99.0,98.6|95.5,95.5,95.5,95.6,95.7,95.8,95.6,95.9,95.7,97.7,97.8,97.8,97.5|77.6,79.0,80.2,81.1,82.4,83.3,83.8,84.9,85.1,87.5,88.2,88.6,89.0|17.8,20.6,24.3,27.5,31.2,34.7,38.3,41.3,44.0,48.9,52.9,55.7,58.5|16.8,20.0,23.7,26.9,30.6,34.1,37.8,40.8,43.5,48.4,52.4,55.2,57.9|0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0,2.3,2.6,3.2|0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,1.2,1.3,1.7&chm=b,c3df9b,0,1,0|b,b8dc82,1,2,0|tAndroid 2.1,608920,2,0,15,,t::-5|b,addb67,2,3,0|tAndroid 2.2,517617,3,0,15,,t::-5|b,a3db4b,3,4,0|b,98dc2e,4,5,0|tAndroid 2.3.3,334d0a,5,0,15,,t::-5|b,8cd41b,5,6,0|b,7ec113,6,7,0|B,6fad0c,7,8,0&chg=7,25&chdl=Android 1.5|Android 1.6|Android 2.1|Android 2.2|Android 2.3|Android 2.3.3|Android 3.1|Android 3.2&chco=add274,a2d15a,97d13e,8bcb28,7dba1e,6ea715,5f920e,507d08" /> +src="http://chart.apis.google.com/chart?&cht=lc&chs=660x250&chxt=x,x,y,r&chxr=0,0,12|1,0,12|2,0,100|3,0,100&chxl=0%3A%7C08/01%7C08/15%7C09/01%7C09/15%7C10/01%7C10/15%7C11/01%7C11/15%7C12/01%7C12/15%7C01/01%7C01/15%7C02/01%7C1%3A%7C2011%7C%7C%7C%7C%7C%7C%7C%7C%7C%7C2012%7C%7C2012%7C2%3A%7C0%25%7C25%25%7C50%25%7C75%25%7C100%25%7C3%3A%7C0%25%7C25%25%7C50%25%7C75%25%7C100%25&chxp=0,0,1,2,3,4,5,6,7,8,9,10,11,12&chxtc=0,5&chd=t:98.2,98.1,97.9,97.9,97.7,97.6,97.5,99.4,99.4,99.2,98.6,98.4,98.5|96.9,96.9,96.9,96.9,96.6,96.6,96.5,98.6,98.6,98.5,98.0,97.8,97.9|94.9,95.0,95.1,95.2,95.1,95.4,95.2,97.2,97.3,97.3,96.9,96.8,96.9|79.6,80.5,81.8,82.7,83.3,84.4,84.6,87.0,87.7,88.1,88.4,88.8,89.2|23.7,26.9,30.6,34.1,37.8,40.8,43.5,48.4,52.4,55.2,57.9,59.7,61.3|0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0,2.3,2.6,3.2,3.2,3.3|0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,1.2,1.3,1.7,1.8,1.9&chm=b,c3df9b,0,1,0|b,b6dc7d,1,2,0|tAndroid%202.1,5b831d,2,0,15,,t::-5|b,aadb5e,2,3,0|tAndroid%202.2,496c13,3,0,15,,t::-5|b,9ddb3d,3,4,0|tAndroid%202.3.3,38540b,4,0,15,,t::-5|b,91da1e,4,5,0|b,80c414,5,6,0|B,6fad0c,6,7,0&chg=7,25&chdl=Android%201.5|Android%201.6|Android%202.1|Android%202.2|Android%202.3.3|Android%203.1|Android%203.2&chco=add274,a0d155,94d134,84c323,73ad18,62960f,507d08" /> -<p><em>Last historical dataset collected during a 14-day period ending on January 3, 2012</em></p> +<p><em>Last historical dataset collected during a 14-day period ending on February 1, 2012</em></p> </div><!-- end dashboard-panel --> diff --git a/docs/html/resources/dashboard/screens.jd b/docs/html/resources/dashboard/screens.jd index 79d59d91aa7b..ae5cdc735915 100644 --- a/docs/html/resources/dashboard/screens.jd +++ b/docs/html/resources/dashboard/screens.jd @@ -60,7 +60,7 @@ ending on the data collection date noted below.</p> <div class="dashboard-panel"> <img alt="" width="400" height="250" -src="http://chart.googleapis.com/chart?cht=p&chs=400x250&chco=c4df9b,6fad0c&chl=Xlarge%20/%20mdpi|Large%20/%20ldpi|Large%20/%20mdpi|Normal%20/%20hdpi|Normal%20/%20ldpi|Normal%20/%20mdpi|Small%20/%20hdpi|Small%20/%20ldpi&chd=t%3A3.1,0.1,3.1,71.0,1.0,17.5,2.9,1.3" /> +src="http://chart.googleapis.com/chart?cht=p&chs=400x250&chco=c4df9b,6fad0c&chl=Xlarge%20/%20mdpi|Large%20/%20ldpi|Large%20/%20mdpi|Normal%20/%20hdpi|Normal%20/%20ldpi|Normal%20/%20mdpi|Normal%20/%20xhdpi|Small%20/%20hdpi|Small%20/%20ldpi&chd=t%3A4.8,0.2,2.9,67.1,0.7,18.4,1.8,2.5,1.6" /> <table> <tr> @@ -71,31 +71,31 @@ src="http://chart.googleapis.com/chart?cht=p&chs=400x250&chco=c4df9b,6fad0c&chl= <th scope="col">xhdpi</th> </tr> <tr><th scope="row">small</th> -<td>1.3%</td> <!-- small/ldpi --> +<td>1.6%</td> <!-- small/ldpi --> <td></td> <!-- small/mdpi --> -<td>2.9%</td> <!-- small/hdpi --> +<td>2.5%</td> <!-- small/hdpi --> <td></td> <!-- small/xhdpi --> </tr> <tr><th scope="row">normal</th> -<td>1.0%</td> <!-- normal/ldpi --> -<td>17.5%</td> <!-- normal/mdpi --> -<td>71%</td> <!-- normal/hdpi --> -<td></td> <!-- normal/xhdpi --> +<td>0.7%</td> <!-- normal/ldpi --> +<td>18.4%</td> <!-- normal/mdpi --> +<td>67.1%</td> <!-- normal/hdpi --> +<td>1.8%</td> <!-- normal/xhdpi --> </tr> <tr><th scope="row">large</th> -<td>0.1%</td> <!-- large/ldpi --> -<td>3.1%</td> <!-- large/mdpi --> +<td>0.2%</td> <!-- large/ldpi --> +<td>2.9%</td> <!-- large/mdpi --> <td></td> <!-- large/hdpi --> <td></td> <!-- large/xhdpi --> </tr> <tr><th scope="row">xlarge</th> <td></td> <!-- xlarge/ldpi --> -<td>3.1%</td> <!-- xlarge/mdpi --> +<td>4.8%</td> <!-- xlarge/mdpi --> <td></td> <!-- xlarge/hdpi --> <td></td> <!-- xlarge/xhdpi --> </tr> </table> -<p><em>Data collected during a 7-day period ending on December 1, 2011</em></p> +<p><em>Data collected during a 7-day period ending on February 1, 2012</em></p> </div> diff --git a/media/libstagefright/Android.mk b/media/libstagefright/Android.mk index a452ad57d34e..03e8a0611cd6 100644 --- a/media/libstagefright/Android.mk +++ b/media/libstagefright/Android.mk @@ -78,8 +78,6 @@ LOCAL_SHARED_LIBRARIES := \ LOCAL_STATIC_LIBRARIES := \ libstagefright_color_conversion \ - libstagefright_amrnbenc \ - libstagefright_amrwbenc \ libstagefright_avcenc \ libstagefright_m4vh263enc \ libstagefright_matroska \ @@ -141,7 +139,6 @@ endif # ifeq ($(HTTP_STACK),chrome) ################################################################################ LOCAL_SHARED_LIBRARIES += \ - libstagefright_amrnb_common \ libstagefright_enc_common \ libstagefright_avc_common \ libstagefright_foundation \ diff --git a/media/libstagefright/OMXCodec.cpp b/media/libstagefright/OMXCodec.cpp index 9b947e5dbda2..af4aa797fb70 100755 --- a/media/libstagefright/OMXCodec.cpp +++ b/media/libstagefright/OMXCodec.cpp @@ -18,7 +18,6 @@ #define LOG_TAG "OMXCodec" #include <utils/Log.h> -#include "include/AMRWBEncoder.h" #include "include/AVCEncoder.h" #include "include/M4vH263Encoder.h" @@ -69,7 +68,6 @@ static sp<MediaSource> Make##name(const sp<MediaSource> &source, const sp<MetaDa #define FACTORY_REF(name) { #name, Make##name }, -FACTORY_CREATE_ENCODER(AMRWBEncoder) FACTORY_CREATE_ENCODER(AVCEncoder) FACTORY_CREATE_ENCODER(M4vH263Encoder) @@ -82,7 +80,6 @@ static sp<MediaSource> InstantiateSoftwareEncoder( }; static const FactoryInfo kFactoryInfo[] = { - FACTORY_REF(AMRWBEncoder) FACTORY_REF(AVCEncoder) FACTORY_REF(M4vH263Encoder) }; @@ -145,7 +142,7 @@ static const CodecInfo kEncoderInfo[] = { { MEDIA_MIMETYPE_AUDIO_AMR_NB, "OMX.TI.AMR.encode" }, { MEDIA_MIMETYPE_AUDIO_AMR_NB, "OMX.google.amrnb.encoder" }, { MEDIA_MIMETYPE_AUDIO_AMR_WB, "OMX.TI.WBAMR.encode" }, - { MEDIA_MIMETYPE_AUDIO_AMR_WB, "AMRWBEncoder" }, + { MEDIA_MIMETYPE_AUDIO_AMR_WB, "OMX.google.amrwb.encoder" }, { MEDIA_MIMETYPE_AUDIO_AAC, "OMX.TI.AAC.encode" }, { MEDIA_MIMETYPE_AUDIO_AAC, "OMX.google.aac.encoder" }, { MEDIA_MIMETYPE_VIDEO_MPEG4, "OMX.TI.DUCATI1.VIDEO.MPEG4E" }, diff --git a/media/libstagefright/codecs/amrwbenc/Android.mk b/media/libstagefright/codecs/amrwbenc/Android.mk index ae43870b5461..6ce61713d6df 100644 --- a/media/libstagefright/codecs/amrwbenc/Android.mk +++ b/media/libstagefright/codecs/amrwbenc/Android.mk @@ -117,4 +117,26 @@ endif include $(BUILD_STATIC_LIBRARY) +################################################################################ +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + SoftAMRWBEncoder.cpp + +LOCAL_C_INCLUDES := \ + frameworks/base/media/libstagefright/include \ + frameworks/base/include/media/stagefright/openmax \ + frameworks/base/media/libstagefright/codecs/common/include \ + +LOCAL_STATIC_LIBRARIES := \ + libstagefright_amrwbenc + +LOCAL_SHARED_LIBRARIES := \ + libstagefright_omx libstagefright_foundation libutils \ + libstagefright_enc_common + +LOCAL_MODULE := libstagefright_soft_amrwbenc +LOCAL_MODULE_TAGS := optional + +include $(BUILD_SHARED_LIBRARY) diff --git a/media/libstagefright/codecs/amrwbenc/SoftAMRWBEncoder.cpp b/media/libstagefright/codecs/amrwbenc/SoftAMRWBEncoder.cpp new file mode 100644 index 000000000000..9ccb49c6b8b8 --- /dev/null +++ b/media/libstagefright/codecs/amrwbenc/SoftAMRWBEncoder.cpp @@ -0,0 +1,459 @@ +/* + * 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "SoftAMRWBEncoder" +#include <utils/Log.h> + +#include "SoftAMRWBEncoder.h" + +#include "cmnMemory.h" + +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/hexdump.h> + +namespace android { + +static const int32_t kSampleRate = 16000; + +template<class T> +static void InitOMXParams(T *params) { + params->nSize = sizeof(T); + params->nVersion.s.nVersionMajor = 1; + params->nVersion.s.nVersionMinor = 0; + params->nVersion.s.nRevision = 0; + params->nVersion.s.nStep = 0; +} + +SoftAMRWBEncoder::SoftAMRWBEncoder( + const char *name, + const OMX_CALLBACKTYPE *callbacks, + OMX_PTR appData, + OMX_COMPONENTTYPE **component) + : SimpleSoftOMXComponent(name, callbacks, appData, component), + mEncoderHandle(NULL), + mApiHandle(NULL), + mMemOperator(NULL), + mBitRate(0), + mMode(VOAMRWB_MD66), + mInputSize(0), + mInputTimeUs(-1ll), + mSawInputEOS(false), + mSignalledError(false) { + initPorts(); + CHECK_EQ(initEncoder(), (status_t)OK); +} + +SoftAMRWBEncoder::~SoftAMRWBEncoder() { + if (mEncoderHandle != NULL) { + CHECK_EQ(VO_ERR_NONE, mApiHandle->Uninit(mEncoderHandle)); + mEncoderHandle = NULL; + } + + delete mApiHandle; + mApiHandle = NULL; + + delete mMemOperator; + mMemOperator = NULL; +} + +void SoftAMRWBEncoder::initPorts() { + OMX_PARAM_PORTDEFINITIONTYPE def; + InitOMXParams(&def); + + def.nPortIndex = 0; + def.eDir = OMX_DirInput; + def.nBufferCountMin = kNumBuffers; + def.nBufferCountActual = def.nBufferCountMin; + def.nBufferSize = kNumSamplesPerFrame * sizeof(int16_t); + def.bEnabled = OMX_TRUE; + def.bPopulated = OMX_FALSE; + def.eDomain = OMX_PortDomainAudio; + def.bBuffersContiguous = OMX_FALSE; + def.nBufferAlignment = 1; + + def.format.audio.cMIMEType = const_cast<char *>("audio/raw"); + def.format.audio.pNativeRender = NULL; + def.format.audio.bFlagErrorConcealment = OMX_FALSE; + def.format.audio.eEncoding = OMX_AUDIO_CodingPCM; + + addPort(def); + + def.nPortIndex = 1; + def.eDir = OMX_DirOutput; + def.nBufferCountMin = kNumBuffers; + def.nBufferCountActual = def.nBufferCountMin; + def.nBufferSize = 8192; + def.bEnabled = OMX_TRUE; + def.bPopulated = OMX_FALSE; + def.eDomain = OMX_PortDomainAudio; + def.bBuffersContiguous = OMX_FALSE; + def.nBufferAlignment = 2; + + def.format.audio.cMIMEType = const_cast<char *>("audio/amr-wb"); + def.format.audio.pNativeRender = NULL; + def.format.audio.bFlagErrorConcealment = OMX_FALSE; + def.format.audio.eEncoding = OMX_AUDIO_CodingAMR; + + addPort(def); +} + +status_t SoftAMRWBEncoder::initEncoder() { + mApiHandle = new VO_AUDIO_CODECAPI; + + if (VO_ERR_NONE != voGetAMRWBEncAPI(mApiHandle)) { + ALOGE("Failed to get api handle"); + return UNKNOWN_ERROR; + } + + mMemOperator = new VO_MEM_OPERATOR; + mMemOperator->Alloc = cmnMemAlloc; + mMemOperator->Copy = cmnMemCopy; + mMemOperator->Free = cmnMemFree; + mMemOperator->Set = cmnMemSet; + mMemOperator->Check = cmnMemCheck; + + VO_CODEC_INIT_USERDATA userData; + memset(&userData, 0, sizeof(userData)); + userData.memflag = VO_IMF_USERMEMOPERATOR; + userData.memData = (VO_PTR) mMemOperator; + + if (VO_ERR_NONE != mApiHandle->Init( + &mEncoderHandle, VO_AUDIO_CodingAMRWB, &userData)) { + ALOGE("Failed to init AMRWB encoder"); + return UNKNOWN_ERROR; + } + + VOAMRWBFRAMETYPE type = VOAMRWB_RFC3267; + if (VO_ERR_NONE != mApiHandle->SetParam( + mEncoderHandle, VO_PID_AMRWB_FRAMETYPE, &type)) { + ALOGE("Failed to set AMRWB encoder frame type to %d", type); + return UNKNOWN_ERROR; + } + + return OK; +} + +OMX_ERRORTYPE SoftAMRWBEncoder::internalGetParameter( + OMX_INDEXTYPE index, OMX_PTR params) { + switch (index) { + case OMX_IndexParamAudioPortFormat: + { + OMX_AUDIO_PARAM_PORTFORMATTYPE *formatParams = + (OMX_AUDIO_PARAM_PORTFORMATTYPE *)params; + + if (formatParams->nPortIndex > 1) { + return OMX_ErrorUndefined; + } + + if (formatParams->nIndex > 0) { + return OMX_ErrorNoMore; + } + + formatParams->eEncoding = + (formatParams->nPortIndex == 0) + ? OMX_AUDIO_CodingPCM : OMX_AUDIO_CodingAMR; + + return OMX_ErrorNone; + } + + case OMX_IndexParamAudioAmr: + { + OMX_AUDIO_PARAM_AMRTYPE *amrParams = + (OMX_AUDIO_PARAM_AMRTYPE *)params; + + if (amrParams->nPortIndex != 1) { + return OMX_ErrorUndefined; + } + + amrParams->nChannels = 1; + amrParams->nBitRate = mBitRate; + + amrParams->eAMRBandMode = + (OMX_AUDIO_AMRBANDMODETYPE)(mMode + OMX_AUDIO_AMRBandModeWB0); + + amrParams->eAMRDTXMode = OMX_AUDIO_AMRDTXModeOff; + amrParams->eAMRFrameFormat = OMX_AUDIO_AMRFrameFormatFSF; + + return OMX_ErrorNone; + } + + case OMX_IndexParamAudioPcm: + { + OMX_AUDIO_PARAM_PCMMODETYPE *pcmParams = + (OMX_AUDIO_PARAM_PCMMODETYPE *)params; + + if (pcmParams->nPortIndex != 0) { + return OMX_ErrorUndefined; + } + + pcmParams->eNumData = OMX_NumericalDataSigned; + pcmParams->eEndian = OMX_EndianBig; + pcmParams->bInterleaved = OMX_TRUE; + pcmParams->nBitPerSample = 16; + pcmParams->ePCMMode = OMX_AUDIO_PCMModeLinear; + pcmParams->eChannelMapping[0] = OMX_AUDIO_ChannelCF; + + pcmParams->nChannels = 1; + pcmParams->nSamplingRate = kSampleRate; + + return OMX_ErrorNone; + } + + default: + return SimpleSoftOMXComponent::internalGetParameter(index, params); + } +} + +OMX_ERRORTYPE SoftAMRWBEncoder::internalSetParameter( + OMX_INDEXTYPE index, const OMX_PTR params) { + switch (index) { + case OMX_IndexParamStandardComponentRole: + { + const OMX_PARAM_COMPONENTROLETYPE *roleParams = + (const OMX_PARAM_COMPONENTROLETYPE *)params; + + if (strncmp((const char *)roleParams->cRole, + "audio_encoder.amrwb", + OMX_MAX_STRINGNAME_SIZE - 1)) { + return OMX_ErrorUndefined; + } + + return OMX_ErrorNone; + } + + case OMX_IndexParamAudioPortFormat: + { + const OMX_AUDIO_PARAM_PORTFORMATTYPE *formatParams = + (const OMX_AUDIO_PARAM_PORTFORMATTYPE *)params; + + if (formatParams->nPortIndex > 1) { + return OMX_ErrorUndefined; + } + + if (formatParams->nIndex > 0) { + return OMX_ErrorNoMore; + } + + if ((formatParams->nPortIndex == 0 + && formatParams->eEncoding != OMX_AUDIO_CodingPCM) + || (formatParams->nPortIndex == 1 + && formatParams->eEncoding != OMX_AUDIO_CodingAMR)) { + return OMX_ErrorUndefined; + } + + return OMX_ErrorNone; + } + + case OMX_IndexParamAudioAmr: + { + OMX_AUDIO_PARAM_AMRTYPE *amrParams = + (OMX_AUDIO_PARAM_AMRTYPE *)params; + + if (amrParams->nPortIndex != 1) { + return OMX_ErrorUndefined; + } + + if (amrParams->nChannels != 1 + || amrParams->eAMRDTXMode != OMX_AUDIO_AMRDTXModeOff + || amrParams->eAMRFrameFormat + != OMX_AUDIO_AMRFrameFormatFSF + || amrParams->eAMRBandMode < OMX_AUDIO_AMRBandModeWB0 + || amrParams->eAMRBandMode > OMX_AUDIO_AMRBandModeWB8) { + return OMX_ErrorUndefined; + } + + mBitRate = amrParams->nBitRate; + + mMode = (VOAMRWBMODE)( + amrParams->eAMRBandMode - OMX_AUDIO_AMRBandModeWB0); + + amrParams->eAMRDTXMode = OMX_AUDIO_AMRDTXModeOff; + amrParams->eAMRFrameFormat = OMX_AUDIO_AMRFrameFormatFSF; + + if (VO_ERR_NONE != + mApiHandle->SetParam( + mEncoderHandle, VO_PID_AMRWB_MODE, &mMode)) { + ALOGE("Failed to set AMRWB encoder mode to %d", mMode); + return OMX_ErrorUndefined; + } + + return OMX_ErrorNone; + } + + case OMX_IndexParamAudioPcm: + { + OMX_AUDIO_PARAM_PCMMODETYPE *pcmParams = + (OMX_AUDIO_PARAM_PCMMODETYPE *)params; + + if (pcmParams->nPortIndex != 0) { + return OMX_ErrorUndefined; + } + + if (pcmParams->nChannels != 1 + || pcmParams->nSamplingRate != (OMX_U32)kSampleRate) { + return OMX_ErrorUndefined; + } + + return OMX_ErrorNone; + } + + + default: + return SimpleSoftOMXComponent::internalSetParameter(index, params); + } +} + +void SoftAMRWBEncoder::onQueueFilled(OMX_U32 portIndex) { + if (mSignalledError) { + return; + } + + List<BufferInfo *> &inQueue = getPortQueue(0); + List<BufferInfo *> &outQueue = getPortQueue(1); + + size_t numBytesPerInputFrame = kNumSamplesPerFrame * sizeof(int16_t); + + for (;;) { + // We do the following until we run out of buffers. + + while (mInputSize < numBytesPerInputFrame) { + // As long as there's still input data to be read we + // will drain "kNumSamplesPerFrame" samples + // into the "mInputFrame" buffer and then encode those + // as a unit into an output buffer. + + if (mSawInputEOS || inQueue.empty()) { + return; + } + + BufferInfo *inInfo = *inQueue.begin(); + OMX_BUFFERHEADERTYPE *inHeader = inInfo->mHeader; + + const void *inData = inHeader->pBuffer + inHeader->nOffset; + + size_t copy = numBytesPerInputFrame - mInputSize; + if (copy > inHeader->nFilledLen) { + copy = inHeader->nFilledLen; + } + + if (mInputSize == 0) { + mInputTimeUs = inHeader->nTimeStamp; + } + + memcpy((uint8_t *)mInputFrame + mInputSize, inData, copy); + mInputSize += copy; + + inHeader->nOffset += copy; + inHeader->nFilledLen -= copy; + + // "Time" on the input buffer has in effect advanced by the + // number of audio frames we just advanced nOffset by. + inHeader->nTimeStamp += + (copy * 1000000ll / kSampleRate) / sizeof(int16_t); + + if (inHeader->nFilledLen == 0) { + if (inHeader->nFlags & OMX_BUFFERFLAG_EOS) { + ALOGV("saw input EOS"); + mSawInputEOS = true; + + // Pad any remaining data with zeroes. + memset((uint8_t *)mInputFrame + mInputSize, + 0, + numBytesPerInputFrame - mInputSize); + + mInputSize = numBytesPerInputFrame; + } + + inQueue.erase(inQueue.begin()); + inInfo->mOwnedByUs = false; + notifyEmptyBufferDone(inHeader); + + inData = NULL; + inHeader = NULL; + inInfo = NULL; + } + } + + // At this point we have all the input data necessary to encode + // a single frame, all we need is an output buffer to store the result + // in. + + if (outQueue.empty()) { + return; + } + + BufferInfo *outInfo = *outQueue.begin(); + OMX_BUFFERHEADERTYPE *outHeader = outInfo->mHeader; + + uint8_t *outPtr = outHeader->pBuffer + outHeader->nOffset; + size_t outAvailable = outHeader->nAllocLen - outHeader->nOffset; + + VO_CODECBUFFER inputData; + memset(&inputData, 0, sizeof(inputData)); + inputData.Buffer = (unsigned char *) mInputFrame; + inputData.Length = mInputSize; + + CHECK_EQ(VO_ERR_NONE, + mApiHandle->SetInputData(mEncoderHandle, &inputData)); + + VO_CODECBUFFER outputData; + memset(&outputData, 0, sizeof(outputData)); + VO_AUDIO_OUTPUTINFO outputInfo; + memset(&outputInfo, 0, sizeof(outputInfo)); + + outputData.Buffer = outPtr; + outputData.Length = outAvailable; + VO_U32 ret = mApiHandle->GetOutputData( + mEncoderHandle, &outputData, &outputInfo); + CHECK(ret == VO_ERR_NONE || ret == VO_ERR_INPUT_BUFFER_SMALL); + + outHeader->nFilledLen = outputData.Length; + outHeader->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; + + if (mSawInputEOS) { + // We also tag this output buffer with EOS if it corresponds + // to the final input buffer. + outHeader->nFlags = OMX_BUFFERFLAG_EOS; + } + + outHeader->nTimeStamp = mInputTimeUs; + +#if 0 + ALOGI("sending %ld bytes of data (time = %lld us, flags = 0x%08lx)", + outHeader->nFilledLen, mInputTimeUs, outHeader->nFlags); + + hexdump(outHeader->pBuffer + outHeader->nOffset, outHeader->nFilledLen); +#endif + + outQueue.erase(outQueue.begin()); + outInfo->mOwnedByUs = false; + notifyFillBufferDone(outHeader); + + outHeader = NULL; + outInfo = NULL; + + mInputSize = 0; + } +} + +} // namespace android + +android::SoftOMXComponent *createSoftOMXComponent( + const char *name, const OMX_CALLBACKTYPE *callbacks, + OMX_PTR appData, OMX_COMPONENTTYPE **component) { + return new android::SoftAMRWBEncoder(name, callbacks, appData, component); +} diff --git a/media/libstagefright/codecs/amrwbenc/SoftAMRWBEncoder.h b/media/libstagefright/codecs/amrwbenc/SoftAMRWBEncoder.h new file mode 100644 index 000000000000..d0c1dabf9313 --- /dev/null +++ b/media/libstagefright/codecs/amrwbenc/SoftAMRWBEncoder.h @@ -0,0 +1,76 @@ +/* + * 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. + */ + +#ifndef SOFT_AMRWB_ENCODER_H_ + +#define SOFT_AMRWB_ENCODER_H_ + +#include "SimpleSoftOMXComponent.h" + +#include "voAMRWB.h" + +struct VO_AUDIO_CODECAPI; +struct VO_MEM_OPERATOR; + +namespace android { + +struct SoftAMRWBEncoder : public SimpleSoftOMXComponent { + SoftAMRWBEncoder( + const char *name, + const OMX_CALLBACKTYPE *callbacks, + OMX_PTR appData, + OMX_COMPONENTTYPE **component); + +protected: + virtual ~SoftAMRWBEncoder(); + + virtual OMX_ERRORTYPE internalGetParameter( + OMX_INDEXTYPE index, OMX_PTR params); + + virtual OMX_ERRORTYPE internalSetParameter( + OMX_INDEXTYPE index, const OMX_PTR params); + + virtual void onQueueFilled(OMX_U32 portIndex); + +private: + enum { + kNumBuffers = 4, + kNumSamplesPerFrame = 320, + }; + + void *mEncoderHandle; + VO_AUDIO_CODECAPI *mApiHandle; + VO_MEM_OPERATOR *mMemOperator; + + OMX_U32 mBitRate; + VOAMRWBMODE mMode; + + size_t mInputSize; + int16_t mInputFrame[kNumSamplesPerFrame]; + int64_t mInputTimeUs; + + bool mSawInputEOS; + bool mSignalledError; + + void initPorts(); + status_t initEncoder(); + + DISALLOW_EVIL_CONSTRUCTORS(SoftAMRWBEncoder); +}; + +} // namespace android + +#endif // SOFT_AMRWB_ENCODER_H_ diff --git a/media/libstagefright/omx/SoftOMXPlugin.cpp b/media/libstagefright/omx/SoftOMXPlugin.cpp index 26b2fa337a92..99ffe7d65814 100644 --- a/media/libstagefright/omx/SoftOMXPlugin.cpp +++ b/media/libstagefright/omx/SoftOMXPlugin.cpp @@ -39,6 +39,7 @@ static const struct { { "OMX.google.amrnb.decoder", "amrdec", "audio_decoder.amrnb" }, { "OMX.google.amrnb.encoder", "amrnbenc", "audio_encoder.amrnb" }, { "OMX.google.amrwb.decoder", "amrdec", "audio_decoder.amrwb" }, + { "OMX.google.amrwb.encoder", "amrwbenc", "audio_encoder.amrwb" }, { "OMX.google.h264.decoder", "h264dec", "video_decoder.avc" }, { "OMX.google.g711.alaw.decoder", "g711dec", "audio_decoder.g711alaw" }, { "OMX.google.g711.mlaw.decoder", "g711dec", "audio_decoder.g711mlaw" }, diff --git a/opengl/libs/GLES_trace/src/gltrace_fixup.cpp b/opengl/libs/GLES_trace/src/gltrace_fixup.cpp index daba3ff53082..871b5dc720b8 100644 --- a/opengl/libs/GLES_trace/src/gltrace_fixup.cpp +++ b/opengl/libs/GLES_trace/src/gltrace_fixup.cpp @@ -358,6 +358,14 @@ void fixupGLMessage(GLTraceContext *context, nsecs_t start, nsecs_t end, GLMessa fixup_addFBContents(context, glmsg, CURRENTLY_BOUND_FB); } break; + case GLMessage::glPushGroupMarkerEXT: + /* void PushGroupMarkerEXT(sizei length, const char *marker); */ + fixup_CStringPtr(1, glmsg); + break; + case GLMessage::glInsertEventMarkerEXT: + /* void InsertEventMarkerEXT(sizei length, const char *marker); */ + fixup_CStringPtr(1, glmsg); + break; default: break; } diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java index 23fa94a5a888..8bda7559b729 100644 --- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -46,7 +46,6 @@ import android.text.TextUtils.SimpleStringSplitter; import android.util.Slog; import android.util.SparseArray; import android.view.IWindow; -import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; @@ -96,6 +95,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private static final int DO_SET_SERVICE_INFO = 10; + public static final int ACTIVE_WINDOW_ID = -1; + + public static final long ROOT_NODE_ID = -1; + private static int sNextWindowId; final HandlerCaller mCaller; @@ -467,7 +470,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - public void registerEventListener(IEventListener listener) { + public void registerUiTestAutomationService(IEventListener listener, + AccessibilityServiceInfo accessibilityServiceInfo) { mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT, FUNCTION_REGISTER_EVENT_LISTENER); ComponentName componentName = new ComponentName("foo.bar", @@ -490,13 +494,23 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } // Hook the automation service up. - AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo(); - accessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; - accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; Service service = new Service(componentName, accessibilityServiceInfo, true); service.onServiceConnected(componentName, listener.asBinder()); } + public void unregisterUiTestAutomationService(IEventListener listener) { + synchronized (mLock) { + final int serviceCount = mServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = mServices.get(i); + if (service.mServiceInterface == listener && service.mIsAutomation) { + // Automation service is not bound, so pretend it died to perform clean up. + service.binderDied(); + } + } + } + } + /** * Removes an AccessibilityInteractionConnection. * @@ -1070,10 +1084,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - public float findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId, - int interactionId, IAccessibilityInteractionConnectionCallback callback, - long interrogatingTid) + public float findAccessibilityNodeInfoByViewId(int accessibilityWindowId, + long accessibilityNodeId, int viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) throws RemoteException { + final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); IAccessibilityInteractionConnection connection = null; synchronized (mLock) { mSecurityPolicy.enforceCanRetrieveWindowContent(this); @@ -1081,12 +1096,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (!permissionGranted) { return 0; } else { - connection = getConnectionToRetrievalAllowingWindowLocked(); + connection = getConnectionLocked(resolvedWindowId); if (connection == null) { - if (DEBUG) { - Slog.e(LOG_TAG, "No interaction connection to a retrieve " - + "allowing window."); - } return 0; } } @@ -1094,44 +1105,33 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { - connection.findAccessibilityNodeInfoByViewId(viewId, interactionId, callback, - interrogatingPid, interrogatingTid); + connection.findAccessibilityNodeInfoByViewId(accessibilityNodeId, viewId, + interactionId, callback, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { - Slog.e(LOG_TAG, "Error finding node."); + Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId()."); } } finally { Binder.restoreCallingIdentity(identityToken); } - return getCompatibilityScale(mSecurityPolicy.getRetrievalAllowingWindowLocked()); - } - - public float findAccessibilityNodeInfosByTextInActiveWindow( - String text, int interactionId, - IAccessibilityInteractionConnectionCallback callback, long threadId) - throws RemoteException { - return findAccessibilityNodeInfosByText(text, - mSecurityPolicy.mRetrievalAlowingWindowId, View.NO_ID, interactionId, callback, - threadId); + return getCompatibilityScale(resolvedWindowId); } - public float findAccessibilityNodeInfosByText(String text, - int accessibilityWindowId, long accessibilityNodeId, int interactionId, + public float findAccessibilityNodeInfosByText(int accessibilityWindowId, + long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) throws RemoteException { + final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); IAccessibilityInteractionConnection connection = null; synchronized (mLock) { mSecurityPolicy.enforceCanRetrieveWindowContent(this); final boolean permissionGranted = - mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, accessibilityWindowId); + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); if (!permissionGranted) { return 0; } else { - connection = getConnectionToRetrievalAllowingWindowLocked(); + connection = getConnectionLocked(resolvedWindowId); if (connection == null) { - if (DEBUG) { - Slog.e(LOG_TAG, "No interaction connection to focused window."); - } return 0; } } @@ -1139,40 +1139,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { - connection.findAccessibilityNodeInfosByText(text, accessibilityNodeId, + connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text, interactionId, callback, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { - Slog.e(LOG_TAG, "Error finding node."); + Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()"); } } finally { Binder.restoreCallingIdentity(identityToken); } - return getCompatibilityScale(accessibilityWindowId); + return getCompatibilityScale(resolvedWindowId); } public float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) throws RemoteException { + final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); IAccessibilityInteractionConnection connection = null; synchronized (mLock) { mSecurityPolicy.enforceCanRetrieveWindowContent(this); final boolean permissionGranted = - mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, accessibilityWindowId); + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); if (!permissionGranted) { return 0; } else { - AccessibilityConnectionWrapper wrapper = - mWindowIdToInteractionConnectionWrapperMap.get(accessibilityWindowId); - if (wrapper == null) { - if (DEBUG) { - Slog.e(LOG_TAG, "No interaction connection to window: " - + accessibilityWindowId); - } + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { return 0; } - connection = wrapper.mConnection; } } final int interrogatingPid = Binder.getCallingPid(); @@ -1182,35 +1177,29 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub interactionId, callback, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { - Slog.e(LOG_TAG, "Error requesting node with accessibilityNodeId: " - + accessibilityNodeId); + Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()"); } } finally { Binder.restoreCallingIdentity(identityToken); } - return getCompatibilityScale(accessibilityWindowId); + return getCompatibilityScale(resolvedWindowId); } public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) { + final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); IAccessibilityInteractionConnection connection = null; synchronized (mLock) { final boolean permissionGranted = mSecurityPolicy.canPerformActionLocked(this, - accessibilityWindowId, action); + resolvedWindowId, action); if (!permissionGranted) { return false; } else { - AccessibilityConnectionWrapper wrapper = - mWindowIdToInteractionConnectionWrapperMap.get(accessibilityWindowId); - if (wrapper == null) { - if (DEBUG) { - Slog.e(LOG_TAG, "No interaction connection to window: " - + accessibilityWindowId); - } + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { return false; } - connection = wrapper.mConnection; } } final int interrogatingPid = Binder.getCallingPid(); @@ -1220,8 +1209,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub callback, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { - Slog.e(LOG_TAG, "Error requesting node with accessibilityNodeId: " - + accessibilityNodeId); + Slog.e(LOG_TAG, "Error calling performAccessibilityAction()"); } } finally { Binder.restoreCallingIdentity(identityToken); @@ -1265,14 +1253,26 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - private IAccessibilityInteractionConnection getConnectionToRetrievalAllowingWindowLocked() { - final int windowId = mSecurityPolicy.getRetrievalAllowingWindowLocked(); + private IAccessibilityInteractionConnection getConnectionLocked(int windowId) { if (DEBUG) { Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId); } - AccessibilityConnectionWrapper wrapper = - mWindowIdToInteractionConnectionWrapperMap.get(windowId); - return (wrapper != null) ? wrapper.mConnection : null; + AccessibilityConnectionWrapper wrapper = mWindowIdToInteractionConnectionWrapperMap.get( + windowId); + if (wrapper != null && wrapper.mConnection != null) { + return wrapper.mConnection; + } + if (DEBUG) { + Slog.e(LOG_TAG, "No interaction connection to window: " + windowId); + } + return null; + } + + private int resolveAccessibilityWindowId(int accessibilityWindowId) { + if (accessibilityWindowId == ACTIVE_WINDOW_ID) { + return mSecurityPolicy.mRetrievalAlowingWindowId; + } + return accessibilityWindowId; } private float getCompatibilityScale(int windowId) { diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index db0a736dfefa..3a6a3de8f299 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -158,6 +158,7 @@ public final class ActivityManagerService extends ActivityManagerNative static final boolean DEBUG_OOM_ADJ = localLOGV || false; static final boolean DEBUG_TRANSITION = localLOGV || false; static final boolean DEBUG_BROADCAST = localLOGV || false; + static final boolean DEBUG_BACKGROUND_BROADCAST = DEBUG_BROADCAST || false; static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false; static final boolean DEBUG_SERVICE = localLOGV || false; static final boolean DEBUG_SERVICE_EXECUTING = localLOGV || false; @@ -221,7 +222,8 @@ public final class ActivityManagerService extends ActivityManagerNative static final int CPU_MIN_CHECK_DURATION = (DEBUG_POWER_QUICK ? 1 : 5) * 60*1000; // How long we allow a receiver to run before giving up on it. - static final int BROADCAST_TIMEOUT = 10*1000; + static final int BROADCAST_FG_TIMEOUT = 10*1000; + static final int BROADCAST_BG_TIMEOUT = 60*1000; // How long we wait for a service to finish executing. static final int SERVICE_TIMEOUT = 20*1000; @@ -276,33 +278,823 @@ public final class ActivityManagerService extends ActivityManagerNative = new ArrayList<PendingActivityLaunch>(); /** - * List of all active broadcasts that are to be executed immediately - * (without waiting for another broadcast to finish). Currently this only - * contains broadcasts to registered receivers, to avoid spinning up - * a bunch of processes to execute IntentReceiver components. + * BROADCASTS + * + * We keep two broadcast queues and associated bookkeeping, one for those at + * foreground priority, and one for normal (background-priority) broadcasts. */ - final ArrayList<BroadcastRecord> mParallelBroadcasts - = new ArrayList<BroadcastRecord>(); + public class BroadcastQueue { + static final String TAG = "BroadcastQueue"; - /** - * List of all active broadcasts that are to be executed one at a time. - * The object at the top of the list is the currently activity broadcasts; - * those after it are waiting for the top to finish.. - */ - final ArrayList<BroadcastRecord> mOrderedBroadcasts - = new ArrayList<BroadcastRecord>(); + static final int MAX_BROADCAST_HISTORY = 25; - /** - * Historical data of past broadcasts, for debugging. - */ - static final int MAX_BROADCAST_HISTORY = 25; - final BroadcastRecord[] mBroadcastHistory - = new BroadcastRecord[MAX_BROADCAST_HISTORY]; + /** + * Recognizable moniker for this queue + */ + String mQueueName; - /** - * Set when we current have a BROADCAST_INTENT_MSG in flight. - */ - boolean mBroadcastsScheduled = false; + /** + * Timeout period for this queue's broadcasts + */ + long mTimeoutPeriod; + + /** + * Lists of all active broadcasts that are to be executed immediately + * (without waiting for another broadcast to finish). Currently this only + * contains broadcasts to registered receivers, to avoid spinning up + * a bunch of processes to execute IntentReceiver components. Background- + * and foreground-priority broadcasts are queued separately. + */ + final ArrayList<BroadcastRecord> mParallelBroadcasts + = new ArrayList<BroadcastRecord>(); + /** + * List of all active broadcasts that are to be executed one at a time. + * The object at the top of the list is the currently activity broadcasts; + * those after it are waiting for the top to finish. As with parallel + * broadcasts, separate background- and foreground-priority queues are + * maintained. + */ + final ArrayList<BroadcastRecord> mOrderedBroadcasts + = new ArrayList<BroadcastRecord>(); + + /** + * Historical data of past broadcasts, for debugging. + */ + final BroadcastRecord[] mBroadcastHistory + = new BroadcastRecord[MAX_BROADCAST_HISTORY]; + + /** + * Set when we current have a BROADCAST_INTENT_MSG in flight. + */ + boolean mBroadcastsScheduled = false; + + /** + * True if we have a pending unexpired BROADCAST_TIMEOUT_MSG posted to our handler. + */ + boolean mPendingBroadcastTimeoutMessage; + + /** + * Intent broadcasts that we have tried to start, but are + * waiting for the application's process to be created. We only + * need one per scheduling class (instead of a list) because we always + * process broadcasts one at a time, so no others can be started while + * waiting for this one. + */ + BroadcastRecord mPendingBroadcast = null; + + /** + * The receiver index that is pending, to restart the broadcast if needed. + */ + int mPendingBroadcastRecvIndex; + + BroadcastQueue(String name, long timeoutPeriod) { + mQueueName = name; + mTimeoutPeriod = timeoutPeriod; + } + + public boolean isPendingBroadcastProcessLocked(int pid) { + return mPendingBroadcast != null && mPendingBroadcast.curApp.pid == pid; + } + + public void enqueueParallelBroadcastLocked(BroadcastRecord r) { + mParallelBroadcasts.add(r); + } + + public void enqueueOrderedBroadcastLocked(BroadcastRecord r) { + mOrderedBroadcasts.add(r); + } + + public final boolean replaceParallelBroadcastLocked(BroadcastRecord r) { + for (int i=mParallelBroadcasts.size()-1; i>=0; i--) { + if (r.intent.filterEquals(mParallelBroadcasts.get(i).intent)) { + if (DEBUG_BROADCAST) Slog.v(TAG, + "***** DROPPING PARALLEL [" + + mQueueName + "]: " + r.intent); + mParallelBroadcasts.set(i, r); + return true; + } + } + return false; + } + + public final boolean replaceOrderedBroadcastLocked(BroadcastRecord r) { + for (int i=mOrderedBroadcasts.size()-1; i>0; i--) { + if (r.intent.filterEquals(mOrderedBroadcasts.get(i).intent)) { + if (DEBUG_BROADCAST) Slog.v(TAG, + "***** DROPPING ORDERED [" + + mQueueName + "]: " + r.intent); + mOrderedBroadcasts.set(i, r); + return true; + } + } + return false; + } + + public boolean sendPendingBroadcastsLocked(ProcessRecord app) { + boolean didSomething = false; + final BroadcastRecord br = mPendingBroadcast; + if (br != null && br.curApp.pid == app.pid) { + try { + mPendingBroadcast = null; + processCurBroadcastLocked(br, app); + didSomething = true; + } catch (Exception e) { + Slog.w(TAG, "Exception in new application when starting receiver " + + br.curComponent.flattenToShortString(), e); + logBroadcastReceiverDiscardLocked(br); + finishReceiverLocked(br, br.resultCode, br.resultData, + br.resultExtras, br.resultAbort, true); + scheduleBroadcastsLocked(); + // We need to reset the state if we fails to start the receiver. + br.state = BroadcastRecord.IDLE; + throw new RuntimeException(e.getMessage()); + } + } + return didSomething; + } + + public void skipPendingBroadcastLocked(int pid) { + final BroadcastRecord br = mPendingBroadcast; + if (br != null && br.curApp.pid == pid) { + br.state = BroadcastRecord.IDLE; + br.nextReceiver = mPendingBroadcastRecvIndex; + mPendingBroadcast = null; + scheduleBroadcastsLocked(); + } + } + + public void skipCurrentReceiverLocked(ProcessRecord app) { + boolean reschedule = false; + BroadcastRecord r = app.curReceiver; + if (r != null) { + // The current broadcast is waiting for this app's receiver + // to be finished. Looks like that's not going to happen, so + // let the broadcast continue. + logBroadcastReceiverDiscardLocked(r); + finishReceiverLocked(r, r.resultCode, r.resultData, + r.resultExtras, r.resultAbort, true); + reschedule = true; + } + + r = mPendingBroadcast; + if (r != null && r.curApp == app) { + if (DEBUG_BROADCAST) Slog.v(TAG, + "[" + mQueueName + "] skip & discard pending app " + r); + logBroadcastReceiverDiscardLocked(r); + finishReceiverLocked(r, r.resultCode, r.resultData, + r.resultExtras, r.resultAbort, true); + reschedule = true; + } + if (reschedule) { + scheduleBroadcastsLocked(); + } + } + + public void scheduleBroadcastsLocked() { + if (DEBUG_BROADCAST) Slog.v(TAG, "Schedule broadcasts [" + + mQueueName + "]: current=" + + mBroadcastsScheduled); + + if (mBroadcastsScheduled) { + return; + } + mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this)); + mBroadcastsScheduled = true; + } + + public BroadcastRecord getMatchingOrderedReceiver(IBinder receiver) { + if (mOrderedBroadcasts.size() > 0) { + final BroadcastRecord r = mOrderedBroadcasts.get(0); + if (r != null && r.receiver == receiver) { + return r; + } + } + return null; + } + + public boolean finishReceiverLocked(BroadcastRecord r, int resultCode, + String resultData, Bundle resultExtras, boolean resultAbort, + boolean explicit) { + int state = r.state; + r.state = BroadcastRecord.IDLE; + if (state == BroadcastRecord.IDLE) { + if (explicit) { + Slog.w(TAG, "finishReceiver [" + mQueueName + "] called but state is IDLE"); + } + } + r.receiver = null; + r.intent.setComponent(null); + if (r.curApp != null) { + r.curApp.curReceiver = null; + } + if (r.curFilter != null) { + r.curFilter.receiverList.curBroadcast = null; + } + r.curFilter = null; + r.curApp = null; + r.curComponent = null; + r.curReceiver = null; + mPendingBroadcast = null; + + r.resultCode = resultCode; + r.resultData = resultData; + r.resultExtras = resultExtras; + r.resultAbort = resultAbort; + + // We will process the next receiver right now if this is finishing + // an app receiver (which is always asynchronous) or after we have + // come back from calling a receiver. + return state == BroadcastRecord.APP_RECEIVE + || state == BroadcastRecord.CALL_DONE_RECEIVE; + } + + private final void processNextBroadcast(boolean fromMsg) { + synchronized(ActivityManagerService.this) { + BroadcastRecord r; + + if (DEBUG_BROADCAST) Slog.v(TAG, "processNextBroadcast [" + + mQueueName + "]: " + + mParallelBroadcasts.size() + " broadcasts, " + + mOrderedBroadcasts.size() + " ordered broadcasts"); + + updateCpuStats(); + + if (fromMsg) { + mBroadcastsScheduled = false; + } + + // First, deliver any non-serialized broadcasts right away. + while (mParallelBroadcasts.size() > 0) { + r = mParallelBroadcasts.remove(0); + r.dispatchTime = SystemClock.uptimeMillis(); + r.dispatchClockTime = System.currentTimeMillis(); + final int N = r.receivers.size(); + if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Processing parallel broadcast [" + + mQueueName + "] " + r); + for (int i=0; i<N; i++) { + Object target = r.receivers.get(i); + if (DEBUG_BROADCAST) Slog.v(TAG, + "Delivering non-ordered on [" + mQueueName + "] to registered " + + target + ": " + r); + deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false); + } + addBroadcastToHistoryLocked(r); + if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Done with parallel broadcast [" + + mQueueName + "] " + r); + } + + // Now take care of the next serialized one... + + // If we are waiting for a process to come up to handle the next + // broadcast, then do nothing at this point. Just in case, we + // check that the process we're waiting for still exists. + if (mPendingBroadcast != null) { + if (DEBUG_BROADCAST_LIGHT) { + Slog.v(TAG, "processNextBroadcast [" + + mQueueName + "]: waiting for " + + mPendingBroadcast.curApp); + } + + boolean isDead; + synchronized (mPidsSelfLocked) { + isDead = (mPidsSelfLocked.get(mPendingBroadcast.curApp.pid) == null); + } + if (!isDead) { + // It's still alive, so keep waiting + return; + } else { + Slog.w(TAG, "pending app [" + + mQueueName + "]" + mPendingBroadcast.curApp + + " died before responding to broadcast"); + mPendingBroadcast.state = BroadcastRecord.IDLE; + mPendingBroadcast.nextReceiver = mPendingBroadcastRecvIndex; + mPendingBroadcast = null; + } + } + + boolean looped = false; + + do { + if (mOrderedBroadcasts.size() == 0) { + // No more broadcasts pending, so all done! + scheduleAppGcsLocked(); + if (looped) { + // If we had finished the last ordered broadcast, then + // make sure all processes have correct oom and sched + // adjustments. + updateOomAdjLocked(); + } + return; + } + r = mOrderedBroadcasts.get(0); + boolean forceReceive = false; + + // Ensure that even if something goes awry with the timeout + // detection, we catch "hung" broadcasts here, discard them, + // and continue to make progress. + // + // This is only done if the system is ready so that PRE_BOOT_COMPLETED + // receivers don't get executed with timeouts. They're intended for + // one time heavy lifting after system upgrades and can take + // significant amounts of time. + int numReceivers = (r.receivers != null) ? r.receivers.size() : 0; + if (mProcessesReady && r.dispatchTime > 0) { + long now = SystemClock.uptimeMillis(); + if ((numReceivers > 0) && + (now > r.dispatchTime + (2*mTimeoutPeriod*numReceivers))) { + Slog.w(TAG, "Hung broadcast [" + + mQueueName + "] discarded after timeout failure:" + + " now=" + now + + " dispatchTime=" + r.dispatchTime + + " startTime=" + r.receiverTime + + " intent=" + r.intent + + " numReceivers=" + numReceivers + + " nextReceiver=" + r.nextReceiver + + " state=" + r.state); + broadcastTimeoutLocked(false); // forcibly finish this broadcast + forceReceive = true; + r.state = BroadcastRecord.IDLE; + } + } + + if (r.state != BroadcastRecord.IDLE) { + if (DEBUG_BROADCAST) Slog.d(TAG, + "processNextBroadcast(" + + mQueueName + ") called when not idle (state=" + + r.state + ")"); + return; + } + + if (r.receivers == null || r.nextReceiver >= numReceivers + || r.resultAbort || forceReceive) { + // No more receivers for this broadcast! Send the final + // result if requested... + if (r.resultTo != null) { + try { + if (DEBUG_BROADCAST) { + int seq = r.intent.getIntExtra("seq", -1); + Slog.i(TAG, "Finishing broadcast [" + + mQueueName + "] " + r.intent.getAction() + + " seq=" + seq + " app=" + r.callerApp); + } + performReceiveLocked(r.callerApp, r.resultTo, + new Intent(r.intent), r.resultCode, + r.resultData, r.resultExtras, false, false); + // Set this to null so that the reference + // (local and remote) isnt kept in the mBroadcastHistory. + r.resultTo = null; + } catch (RemoteException e) { + Slog.w(TAG, "Failure [" + + mQueueName + "] sending broadcast result of " + + r.intent, e); + } + } + + if (DEBUG_BROADCAST) Slog.v(TAG, "Cancelling BROADCAST_TIMEOUT_MSG"); + cancelBroadcastTimeoutLocked(); + + if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Finished with ordered broadcast " + + r); + + // ... and on to the next... + addBroadcastToHistoryLocked(r); + mOrderedBroadcasts.remove(0); + r = null; + looped = true; + continue; + } + } while (r == null); + + // Get the next receiver... + int recIdx = r.nextReceiver++; + + // Keep track of when this receiver started, and make sure there + // is a timeout message pending to kill it if need be. + r.receiverTime = SystemClock.uptimeMillis(); + if (recIdx == 0) { + r.dispatchTime = r.receiverTime; + r.dispatchClockTime = System.currentTimeMillis(); + if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Processing ordered broadcast [" + + mQueueName + "] " + r); + } + if (! mPendingBroadcastTimeoutMessage) { + long timeoutTime = r.receiverTime + mTimeoutPeriod; + if (DEBUG_BROADCAST) Slog.v(TAG, + "Submitting BROADCAST_TIMEOUT_MSG [" + + mQueueName + "] for " + r + " at " + timeoutTime); + setBroadcastTimeoutLocked(timeoutTime); + } + + Object nextReceiver = r.receivers.get(recIdx); + if (nextReceiver instanceof BroadcastFilter) { + // Simple case: this is a registered receiver who gets + // a direct call. + BroadcastFilter filter = (BroadcastFilter)nextReceiver; + if (DEBUG_BROADCAST) Slog.v(TAG, + "Delivering ordered [" + + mQueueName + "] to registered " + + filter + ": " + r); + deliverToRegisteredReceiverLocked(r, filter, r.ordered); + if (r.receiver == null || !r.ordered) { + // The receiver has already finished, so schedule to + // process the next one. + if (DEBUG_BROADCAST) Slog.v(TAG, "Quick finishing [" + + mQueueName + "]: ordered=" + + r.ordered + " receiver=" + r.receiver); + r.state = BroadcastRecord.IDLE; + scheduleBroadcastsLocked(); + } + return; + } + + // Hard case: need to instantiate the receiver, possibly + // starting its application process to host it. + + ResolveInfo info = + (ResolveInfo)nextReceiver; + + boolean skip = false; + int perm = checkComponentPermission(info.activityInfo.permission, + r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid, + info.activityInfo.exported); + if (perm != PackageManager.PERMISSION_GRANTED) { + if (!info.activityInfo.exported) { + Slog.w(TAG, "Permission Denial: broadcasting " + + r.intent.toString() + + " from " + r.callerPackage + " (pid=" + r.callingPid + + ", uid=" + r.callingUid + ")" + + " is not exported from uid " + info.activityInfo.applicationInfo.uid + + " due to receiver " + info.activityInfo.packageName + + "/" + info.activityInfo.name); + } else { + Slog.w(TAG, "Permission Denial: broadcasting " + + r.intent.toString() + + " from " + r.callerPackage + " (pid=" + r.callingPid + + ", uid=" + r.callingUid + ")" + + " requires " + info.activityInfo.permission + + " due to receiver " + info.activityInfo.packageName + + "/" + info.activityInfo.name); + } + skip = true; + } + if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && + r.requiredPermission != null) { + try { + perm = AppGlobals.getPackageManager(). + checkPermission(r.requiredPermission, + info.activityInfo.applicationInfo.packageName); + } catch (RemoteException e) { + perm = PackageManager.PERMISSION_DENIED; + } + if (perm != PackageManager.PERMISSION_GRANTED) { + Slog.w(TAG, "Permission Denial: receiving " + + r.intent + " to " + + info.activityInfo.applicationInfo.packageName + + " requires " + r.requiredPermission + + " due to sender " + r.callerPackage + + " (uid " + r.callingUid + ")"); + skip = true; + } + } + if (r.curApp != null && r.curApp.crashing) { + // If the target process is crashing, just skip it. + if (DEBUG_BROADCAST) Slog.v(TAG, + "Skipping deliver ordered [" + + mQueueName + "] " + r + " to " + r.curApp + + ": process crashing"); + skip = true; + } + + if (skip) { + if (DEBUG_BROADCAST) Slog.v(TAG, + "Skipping delivery of ordered [" + + mQueueName + "] " + r + " for whatever reason"); + r.receiver = null; + r.curFilter = null; + r.state = BroadcastRecord.IDLE; + scheduleBroadcastsLocked(); + return; + } + + r.state = BroadcastRecord.APP_RECEIVE; + String targetProcess = info.activityInfo.processName; + r.curComponent = new ComponentName( + info.activityInfo.applicationInfo.packageName, + info.activityInfo.name); + r.curReceiver = info.activityInfo; + + // Broadcast is being executed, its package can't be stopped. + try { + AppGlobals.getPackageManager().setPackageStoppedState( + r.curComponent.getPackageName(), false); + } catch (RemoteException e) { + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Failed trying to unstop package " + + r.curComponent.getPackageName() + ": " + e); + } + + // Is this receiver's application already running? + ProcessRecord app = getProcessRecordLocked(targetProcess, + info.activityInfo.applicationInfo.uid); + if (app != null && app.thread != null) { + try { + app.addPackage(info.activityInfo.packageName); + processCurBroadcastLocked(r, app); + return; + } catch (RemoteException e) { + Slog.w(TAG, "Exception when sending broadcast to " + + r.curComponent, e); + } + + // If a dead object exception was thrown -- fall through to + // restart the application. + } + + // Not running -- get it started, to be executed when the app comes up. + if (DEBUG_BROADCAST) Slog.v(TAG, + "Need to start app [" + + mQueueName + "] " + targetProcess + " for broadcast " + r); + if ((r.curApp=startProcessLocked(targetProcess, + info.activityInfo.applicationInfo, true, + r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND, + "broadcast", r.curComponent, + (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0)) + == null) { + // Ah, this recipient is unavailable. Finish it if necessary, + // and mark the broadcast record as ready for the next. + Slog.w(TAG, "Unable to launch app " + + info.activityInfo.applicationInfo.packageName + "/" + + info.activityInfo.applicationInfo.uid + " for broadcast " + + r.intent + ": process is bad"); + logBroadcastReceiverDiscardLocked(r); + finishReceiverLocked(r, r.resultCode, r.resultData, + r.resultExtras, r.resultAbort, true); + scheduleBroadcastsLocked(); + r.state = BroadcastRecord.IDLE; + return; + } + + mPendingBroadcast = r; + mPendingBroadcastRecvIndex = recIdx; + } + } + + final void setBroadcastTimeoutLocked(long timeoutTime) { + if (! mPendingBroadcastTimeoutMessage) { + Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this); + mHandler.sendMessageAtTime(msg, timeoutTime); + mPendingBroadcastTimeoutMessage = true; + } + } + + final void cancelBroadcastTimeoutLocked() { + if (mPendingBroadcastTimeoutMessage) { + mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this); + mPendingBroadcastTimeoutMessage = false; + } + } + + final void broadcastTimeoutLocked(boolean fromMsg) { + if (fromMsg) { + mPendingBroadcastTimeoutMessage = false; + } + + if (mOrderedBroadcasts.size() == 0) { + return; + } + + long now = SystemClock.uptimeMillis(); + BroadcastRecord r = mOrderedBroadcasts.get(0); + if (fromMsg) { + if (mDidDexOpt) { + // Delay timeouts until dexopt finishes. + mDidDexOpt = false; + long timeoutTime = SystemClock.uptimeMillis() + mTimeoutPeriod; + setBroadcastTimeoutLocked(timeoutTime); + return; + } + if (! mProcessesReady) { + // Only process broadcast timeouts if the system is ready. That way + // PRE_BOOT_COMPLETED broadcasts can't timeout as they are intended + // to do heavy lifting for system up. + return; + } + + long timeoutTime = r.receiverTime + mTimeoutPeriod; + if (timeoutTime > now) { + // We can observe premature timeouts because we do not cancel and reset the + // broadcast timeout message after each receiver finishes. Instead, we set up + // an initial timeout then kick it down the road a little further as needed + // when it expires. + if (DEBUG_BROADCAST) Slog.v(TAG, + "Premature timeout [" + + mQueueName + "] @ " + now + ": resetting BROADCAST_TIMEOUT_MSG for " + + timeoutTime); + setBroadcastTimeoutLocked(timeoutTime); + return; + } + } + + Slog.w(TAG, "Timeout of broadcast " + r + " - receiver=" + r.receiver + + ", started " + (now - r.receiverTime) + "ms ago"); + r.receiverTime = now; + r.anrCount++; + + // Current receiver has passed its expiration date. + if (r.nextReceiver <= 0) { + Slog.w(TAG, "Timeout on receiver with nextReceiver <= 0"); + return; + } + + ProcessRecord app = null; + String anrMessage = null; + + Object curReceiver = r.receivers.get(r.nextReceiver-1); + Slog.w(TAG, "Receiver during timeout: " + curReceiver); + logBroadcastReceiverDiscardLocked(r); + if (curReceiver instanceof BroadcastFilter) { + BroadcastFilter bf = (BroadcastFilter)curReceiver; + if (bf.receiverList.pid != 0 + && bf.receiverList.pid != MY_PID) { + synchronized (ActivityManagerService.this.mPidsSelfLocked) { + app = ActivityManagerService.this.mPidsSelfLocked.get( + bf.receiverList.pid); + } + } + } else { + app = r.curApp; + } + + if (app != null) { + anrMessage = "Broadcast of " + r.intent.toString(); + } + + if (mPendingBroadcast == r) { + mPendingBroadcast = null; + } + + // Move on to the next receiver. + finishReceiverLocked(r, r.resultCode, r.resultData, + r.resultExtras, r.resultAbort, true); + scheduleBroadcastsLocked(); + + if (anrMessage != null) { + // Post the ANR to the handler since we do not want to process ANRs while + // potentially holding our lock. + mHandler.post(new AppNotResponding(app, anrMessage)); + } + } + + private final void addBroadcastToHistoryLocked(BroadcastRecord r) { + if (r.callingUid < 0) { + // This was from a registerReceiver() call; ignore it. + return; + } + System.arraycopy(mBroadcastHistory, 0, mBroadcastHistory, 1, + MAX_BROADCAST_HISTORY-1); + r.finishTime = SystemClock.uptimeMillis(); + mBroadcastHistory[0] = r; + } + + final void logBroadcastReceiverDiscardLocked(BroadcastRecord r) { + if (r.nextReceiver > 0) { + Object curReceiver = r.receivers.get(r.nextReceiver-1); + if (curReceiver instanceof BroadcastFilter) { + BroadcastFilter bf = (BroadcastFilter) curReceiver; + EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_FILTER, + System.identityHashCode(r), + r.intent.getAction(), + r.nextReceiver - 1, + System.identityHashCode(bf)); + } else { + EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_APP, + System.identityHashCode(r), + r.intent.getAction(), + r.nextReceiver - 1, + ((ResolveInfo)curReceiver).toString()); + } + } else { + Slog.w(TAG, "Discarding broadcast before first receiver is invoked: " + + r); + EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_APP, + System.identityHashCode(r), + r.intent.getAction(), + r.nextReceiver, + "NONE"); + } + } + + final boolean dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args, + int opti, boolean dumpAll, String dumpPackage, boolean needSep) { + if (mParallelBroadcasts.size() > 0 || mOrderedBroadcasts.size() > 0 + || mPendingBroadcast != null) { + boolean printed = false; + for (int i=mParallelBroadcasts.size()-1; i>=0; i--) { + BroadcastRecord br = mParallelBroadcasts.get(i); + if (dumpPackage != null && !dumpPackage.equals(br.callerPackage)) { + continue; + } + if (!printed) { + if (needSep) { + pw.println(); + needSep = false; + } + printed = true; + pw.println(" Active broadcasts [" + mQueueName + "]:"); + } + pw.println(" Broadcast #" + i + ":"); + br.dump(pw, " "); + } + printed = false; + needSep = true; + for (int i=mOrderedBroadcasts.size()-1; i>=0; i--) { + BroadcastRecord br = mOrderedBroadcasts.get(i); + if (dumpPackage != null && !dumpPackage.equals(br.callerPackage)) { + continue; + } + if (!printed) { + if (needSep) { + pw.println(); + } + needSep = true; + pw.println(" Active ordered broadcasts [" + mQueueName + "]:"); + } + pw.println(" Ordered Broadcast #" + i + ":"); + mOrderedBroadcasts.get(i).dump(pw, " "); + } + if (dumpPackage == null || (mPendingBroadcast != null + && dumpPackage.equals(mPendingBroadcast.callerPackage))) { + if (needSep) { + pw.println(); + } + pw.println(" Pending broadcast [" + mQueueName + "]:"); + if (mPendingBroadcast != null) { + mPendingBroadcast.dump(pw, " "); + } else { + pw.println(" (null)"); + } + needSep = true; + } + } + + boolean printed = false; + for (int i=0; i<MAX_BROADCAST_HISTORY; i++) { + BroadcastRecord r = mBroadcastHistory[i]; + if (r == null) { + break; + } + if (dumpPackage != null && !dumpPackage.equals(r.callerPackage)) { + continue; + } + if (!printed) { + if (needSep) { + pw.println(); + } + needSep = true; + pw.println(" Historical broadcasts [" + mQueueName + "]:"); + printed = true; + } + if (dumpAll) { + pw.print(" Historical Broadcast #"); pw.print(i); pw.println(":"); + r.dump(pw, " "); + } else { + if (i >= 50) { + pw.println(" ..."); + break; + } + pw.print(" #"); pw.print(i); pw.print(": "); pw.println(r); + } + } + + return needSep; + } + } + + final BroadcastQueue mFgBroadcastQueue = new BroadcastQueue("foreground", BROADCAST_FG_TIMEOUT); + final BroadcastQueue mBgBroadcastQueue = new BroadcastQueue("background", BROADCAST_BG_TIMEOUT); + // Convenient for easy iteration over the queues. Foreground is first + // so that dispatch of foreground broadcasts gets precedence. + final BroadcastQueue[] mBroadcastQueues = { mFgBroadcastQueue, mBgBroadcastQueue }; + + BroadcastQueue broadcastQueueForIntent(Intent intent) { + final boolean isFg = (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0; + if (DEBUG_BACKGROUND_BROADCAST) { + Slog.i(TAG, "Broadcast intent " + intent + " on " + + (isFg ? "foreground" : "background") + + " queue"); + } + return (isFg) ? mFgBroadcastQueue : mBgBroadcastQueue; + } + + BroadcastRecord broadcastRecordForReceiverLocked(IBinder receiver) { + for (BroadcastQueue queue : mBroadcastQueues) { + BroadcastRecord r = queue.getMatchingOrderedReceiver(receiver); + if (r != null) { + return r; + } + } + return null; + } /** * Activity we have told the window manager to have key focus. @@ -456,25 +1248,6 @@ public final class ActivityManagerService extends ActivityManagerNative private final StringBuilder mStrictModeBuffer = new StringBuilder(); /** - * True if we have a pending unexpired BROADCAST_TIMEOUT_MSG posted to our handler. - */ - private boolean mPendingBroadcastTimeoutMessage; - - /** - * Intent broadcast that we have tried to start, but are - * waiting for its application's process to be created. We only - * need one (instead of a list) because we always process broadcasts - * one at a time, so no others can be started while waiting for this - * one. - */ - BroadcastRecord mPendingBroadcast = null; - - /** - * The receiver index that is pending, to restart the broadcast if needed. - */ - int mPendingBroadcastRecvIndex; - - /** * Keeps track of all IIntentReceivers that have been registered for * broadcasts. Hash keys are the receiver IBinder, hash value is * a ReceiverList. @@ -910,7 +1683,8 @@ public final class ActivityManagerService extends ActivityManagerNative Intent intent = new Intent("android.intent.action.ANR"); if (!mProcessesReady) { - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND); } broadcastIntentLocked(null, null, intent, null, null, 0, null, null, null, @@ -987,11 +1761,13 @@ public final class ActivityManagerService extends ActivityManagerNative case BROADCAST_INTENT_MSG: { if (DEBUG_BROADCAST) Slog.v( TAG, "Received BROADCAST_INTENT_MSG"); - processNextBroadcast(true); + BroadcastQueue queue = (BroadcastQueue) msg.obj; + queue.processNextBroadcast(true); } break; case BROADCAST_TIMEOUT_MSG: { + final BroadcastQueue queue = (BroadcastQueue) msg.obj; synchronized (ActivityManagerService.this) { - broadcastTimeoutLocked(true); + queue.broadcastTimeoutLocked(true); } } break; case SERVICE_TIMEOUT_MSG: { @@ -3708,12 +4484,9 @@ public final class ActivityManagerService extends ActivityManagerNative // Can't happen; the backup manager is local } } - if (mPendingBroadcast != null && mPendingBroadcast.curApp.pid == pid) { + if (isPendingBroadcastProcessLocked(pid)) { Slog.w(TAG, "Unattached app died before broadcast acknowledged, skipping"); - mPendingBroadcast.state = BroadcastRecord.IDLE; - mPendingBroadcast.nextReceiver = mPendingBroadcastRecvIndex; - mPendingBroadcast = null; - scheduleBroadcastsLocked(); + skipPendingBroadcastLocked(pid); } } else { Slog.w(TAG, "Spurious process start timeout - pid not known for " + app); @@ -3912,23 +4685,13 @@ public final class ActivityManagerService extends ActivityManagerNative } } - // Check if the next broadcast receiver is in this process... - BroadcastRecord br = mPendingBroadcast; - if (!badApp && br != null && br.curApp == app) { + // Check if a next-broadcast receiver is in this process... + if (!badApp && isPendingBroadcastProcessLocked(pid)) { try { - mPendingBroadcast = null; - processCurBroadcastLocked(br, app); - didSomething = true; + didSomething = sendPendingBroadcastsLocked(app); } catch (Exception e) { - Slog.w(TAG, "Exception in new application when starting receiver " - + br.curComponent.flattenToShortString(), e); + // If the app died trying to launch the receiver we declare it 'bad' badApp = true; - logBroadcastReceiverDiscardLocked(br); - finishReceiverLocked(br.receiver, br.resultCode, br.resultData, - br.resultExtras, br.resultAbort, true); - scheduleBroadcastsLocked(); - // We need to reset the state if we fails to start the receiver. - br.state = BroadcastRecord.IDLE; } } @@ -7227,28 +7990,8 @@ public final class ActivityManagerService extends ActivityManagerNative } void skipCurrentReceiverLocked(ProcessRecord app) { - boolean reschedule = false; - BroadcastRecord r = app.curReceiver; - if (r != null) { - // The current broadcast is waiting for this app's receiver - // to be finished. Looks like that's not going to happen, so - // let the broadcast continue. - logBroadcastReceiverDiscardLocked(r); - finishReceiverLocked(r.receiver, r.resultCode, r.resultData, - r.resultExtras, r.resultAbort, true); - reschedule = true; - } - r = mPendingBroadcast; - if (r != null && r.curApp == app) { - if (DEBUG_BROADCAST) Slog.v(TAG, - "skip & discard pending app " + r); - logBroadcastReceiverDiscardLocked(r); - finishReceiverLocked(r.receiver, r.resultCode, r.resultData, - r.resultExtras, r.resultAbort, true); - reschedule = true; - } - if (reschedule) { - scheduleBroadcastsLocked(); + for (BroadcastQueue queue : mBroadcastQueues) { + queue.skipCurrentReceiverLocked(app); } } @@ -8971,84 +9714,11 @@ public final class ActivityManagerService extends ActivityManagerNative needSep = true; } } - - if (mParallelBroadcasts.size() > 0 || mOrderedBroadcasts.size() > 0 - || mPendingBroadcast != null) { - boolean printed = false; - for (int i=mParallelBroadcasts.size()-1; i>=0; i--) { - BroadcastRecord br = mParallelBroadcasts.get(i); - if (dumpPackage != null && !dumpPackage.equals(br.callerPackage)) { - continue; - } - if (!printed) { - if (needSep) { - pw.println(); - } - needSep = true; - pw.println(" Active broadcasts:"); - } - pw.println(" Broadcast #" + i + ":"); - br.dump(pw, " "); - } - printed = false; - for (int i=mOrderedBroadcasts.size()-1; i>=0; i--) { - BroadcastRecord br = mOrderedBroadcasts.get(i); - if (dumpPackage != null && !dumpPackage.equals(br.callerPackage)) { - continue; - } - if (!printed) { - if (needSep) { - pw.println(); - } - needSep = true; - pw.println(" Active ordered broadcasts:"); - } - pw.println(" Ordered Broadcast #" + i + ":"); - mOrderedBroadcasts.get(i).dump(pw, " "); - } - if (dumpPackage == null || (mPendingBroadcast != null - && dumpPackage.equals(mPendingBroadcast.callerPackage))) { - if (needSep) { - pw.println(); - } - pw.println(" Pending broadcast:"); - if (mPendingBroadcast != null) { - mPendingBroadcast.dump(pw, " "); - } else { - pw.println(" (null)"); - } - needSep = true; - } - } - boolean printed = false; - for (int i=0; i<MAX_BROADCAST_HISTORY; i++) { - BroadcastRecord r = mBroadcastHistory[i]; - if (r == null) { - break; - } - if (dumpPackage != null && !dumpPackage.equals(r.callerPackage)) { - continue; - } - if (!printed) { - if (needSep) { - pw.println(); - } - needSep = true; - pw.println(" Historical broadcasts:"); - printed = true; - } - if (dumpAll) { - pw.print(" Historical Broadcast #"); pw.print(i); pw.println(":"); - r.dump(pw, " "); - } else { - if (i >= 50) { - pw.println(" ..."); - break; - } - pw.print(" #"); pw.print(i); pw.print(": "); pw.println(r); - } + for (BroadcastQueue q : mBroadcastQueues) { + needSep = q.dumpLocked(fd, pw, args, opti, dumpAll, dumpPackage, needSep); } + needSep = true; if (mStickyBroadcasts != null && dumpPackage == null) { @@ -9085,7 +9755,10 @@ public final class ActivityManagerService extends ActivityManagerNative if (dumpAll) { pw.println(); - pw.println(" mBroadcastsScheduled=" + mBroadcastsScheduled); + for (BroadcastQueue queue : mBroadcastQueues) { + pw.println(" mBroadcastsScheduled [" + queue.mQueueName + "]=" + + queue.mBroadcastsScheduled); + } pw.println(" mHandler:"); mHandler.dump(new PrintWriterPrinter(pw), " "); needSep = true; @@ -12072,15 +12745,25 @@ public final class ActivityManagerService extends ActivityManagerNative return cur; } - private final void scheduleBroadcastsLocked() { - if (DEBUG_BROADCAST) Slog.v(TAG, "Schedule broadcasts: current=" - + mBroadcastsScheduled); + boolean isPendingBroadcastProcessLocked(int pid) { + return mFgBroadcastQueue.isPendingBroadcastProcessLocked(pid) + || mBgBroadcastQueue.isPendingBroadcastProcessLocked(pid); + } - if (mBroadcastsScheduled) { - return; + void skipPendingBroadcastLocked(int pid) { + Slog.w(TAG, "Unattached app died before broadcast acknowledged, skipping"); + for (BroadcastQueue queue : mBroadcastQueues) { + queue.skipPendingBroadcastLocked(pid); + } + } + + // The app just attached; send any pending broadcasts that it should receive + boolean sendPendingBroadcastsLocked(ProcessRecord app) { + boolean didSomething = false; + for (BroadcastQueue queue : mBroadcastQueues) { + didSomething |= queue.sendPendingBroadcastsLocked(app); } - mHandler.sendEmptyMessage(BROADCAST_INTENT_MSG); - mBroadcastsScheduled = true; + return didSomething; } public Intent registerReceiver(IApplicationThread caller, String callerPackage, @@ -12162,13 +12845,12 @@ public final class ActivityManagerService extends ActivityManagerNative int N = allSticky.size(); for (int i=0; i<N; i++) { Intent intent = (Intent)allSticky.get(i); - BroadcastRecord r = new BroadcastRecord(intent, null, + BroadcastQueue queue = broadcastQueueForIntent(intent); + BroadcastRecord r = new BroadcastRecord(queue, intent, null, null, -1, -1, null, receivers, null, 0, null, null, false, true, true); - if (mParallelBroadcasts.size() == 0) { - scheduleBroadcastsLocked(); - } - mParallelBroadcasts.add(r); + queue.enqueueParallelBroadcastLocked(r); + queue.scheduleBroadcastsLocked(); } } @@ -12179,38 +12861,46 @@ public final class ActivityManagerService extends ActivityManagerNative public void unregisterReceiver(IIntentReceiver receiver) { if (DEBUG_BROADCAST) Slog.v(TAG, "Unregister receiver: " + receiver); - boolean doNext = false; + final long origId = Binder.clearCallingIdentity(); + try { + boolean doTrim = false; - synchronized(this) { - ReceiverList rl + synchronized(this) { + ReceiverList rl = (ReceiverList)mRegisteredReceivers.get(receiver.asBinder()); - if (rl != null) { - if (rl.curBroadcast != null) { - BroadcastRecord r = rl.curBroadcast; - doNext = finishReceiverLocked( - receiver.asBinder(), r.resultCode, r.resultData, - r.resultExtras, r.resultAbort, true); - } + if (rl != null) { + if (rl.curBroadcast != null) { + BroadcastRecord r = rl.curBroadcast; + final boolean doNext = finishReceiverLocked( + receiver.asBinder(), r.resultCode, r.resultData, + r.resultExtras, r.resultAbort, true); + if (doNext) { + doTrim = true; + r.queue.processNextBroadcast(false); + } + } - if (rl.app != null) { - rl.app.receivers.remove(rl); - } - removeReceiverLocked(rl); - if (rl.linkedToDeath) { - rl.linkedToDeath = false; - rl.receiver.asBinder().unlinkToDeath(rl, 0); + if (rl.app != null) { + rl.app.receivers.remove(rl); + } + removeReceiverLocked(rl); + if (rl.linkedToDeath) { + rl.linkedToDeath = false; + rl.receiver.asBinder().unlinkToDeath(rl, 0); + } } } - } - if (!doNext) { - return; + // If we actually concluded any broadcasts, we might now be able + // to trim the recipients' apps from our working set + if (doTrim) { + trimApplications(); + return; + } + + } finally { + Binder.restoreCallingIdentity(origId); } - - final long origId = Binder.clearCallingIdentity(); - processNextBroadcast(false); - trimApplications(); - Binder.restoreCallingIdentity(origId); } void removeReceiverLocked(ReceiverList rl) { @@ -12440,28 +13130,17 @@ public final class ActivityManagerService extends ActivityManagerNative // If we are not serializing this broadcast, then send the // registered receivers separately so they don't wait for the // components to be launched. - BroadcastRecord r = new BroadcastRecord(intent, callerApp, + final BroadcastQueue queue = broadcastQueueForIntent(intent); + BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage, callingPid, callingUid, requiredPermission, registeredReceivers, resultTo, resultCode, resultData, map, ordered, sticky, false); if (DEBUG_BROADCAST) Slog.v( - TAG, "Enqueueing parallel broadcast " + r - + ": prev had " + mParallelBroadcasts.size()); - boolean replaced = false; - if (replacePending) { - for (int i=mParallelBroadcasts.size()-1; i>=0; i--) { - if (intent.filterEquals(mParallelBroadcasts.get(i).intent)) { - if (DEBUG_BROADCAST) Slog.v(TAG, - "***** DROPPING PARALLEL: " + intent); - mParallelBroadcasts.set(i, r); - replaced = true; - break; - } - } - } + TAG, "Enqueueing parallel broadcast " + r); + final boolean replaced = replacePending && queue.replaceParallelBroadcastLocked(r); if (!replaced) { - mParallelBroadcasts.add(r); - scheduleBroadcastsLocked(); + queue.enqueueParallelBroadcastLocked(r); + queue.scheduleBroadcastsLocked(); } registeredReceivers = null; NR = 0; @@ -12541,32 +13220,22 @@ public final class ActivityManagerService extends ActivityManagerNative if ((receivers != null && receivers.size() > 0) || resultTo != null) { - BroadcastRecord r = new BroadcastRecord(intent, callerApp, + BroadcastQueue queue = broadcastQueueForIntent(intent); + BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage, callingPid, callingUid, requiredPermission, receivers, resultTo, resultCode, resultData, map, ordered, sticky, false); if (DEBUG_BROADCAST) Slog.v( TAG, "Enqueueing ordered broadcast " + r - + ": prev had " + mOrderedBroadcasts.size()); + + ": prev had " + queue.mOrderedBroadcasts.size()); if (DEBUG_BROADCAST) { int seq = r.intent.getIntExtra("seq", -1); Slog.i(TAG, "Enqueueing broadcast " + r.intent.getAction() + " seq=" + seq); } - boolean replaced = false; - if (replacePending) { - for (int i=mOrderedBroadcasts.size()-1; i>0; i--) { - if (intent.filterEquals(mOrderedBroadcasts.get(i).intent)) { - if (DEBUG_BROADCAST) Slog.v(TAG, - "***** DROPPING ORDERED: " + intent); - mOrderedBroadcasts.set(i, r); - replaced = true; - break; - } - } - } + boolean replaced = replacePending && queue.replaceOrderedBroadcastLocked(r); if (!replaced) { - mOrderedBroadcasts.add(r); - scheduleBroadcastsLocked(); + queue.enqueueOrderedBroadcastLocked(r); + queue.scheduleBroadcastsLocked(); } } @@ -12673,54 +13342,14 @@ public final class ActivityManagerService extends ActivityManagerNative private final boolean finishReceiverLocked(IBinder receiver, int resultCode, String resultData, Bundle resultExtras, boolean resultAbort, boolean explicit) { - if (mOrderedBroadcasts.size() == 0) { - if (explicit) { - Slog.w(TAG, "finishReceiver called but no pending broadcasts"); - } - return false; - } - BroadcastRecord r = mOrderedBroadcasts.get(0); - if (r.receiver == null) { - if (explicit) { - Slog.w(TAG, "finishReceiver called but none active"); - } - return false; - } - if (r.receiver != receiver) { - Slog.w(TAG, "finishReceiver called but active receiver is different"); + final BroadcastRecord r = broadcastRecordForReceiverLocked(receiver); + if (r == null) { + Slog.w(TAG, "finishReceiver called but not found on queue"); return false; } - int state = r.state; - r.state = BroadcastRecord.IDLE; - if (state == BroadcastRecord.IDLE) { - if (explicit) { - Slog.w(TAG, "finishReceiver called but state is IDLE"); - } - } - r.receiver = null; - r.intent.setComponent(null); - if (r.curApp != null) { - r.curApp.curReceiver = null; - } - if (r.curFilter != null) { - r.curFilter.receiverList.curBroadcast = null; - } - r.curFilter = null; - r.curApp = null; - r.curComponent = null; - r.curReceiver = null; - mPendingBroadcast = null; - r.resultCode = resultCode; - r.resultData = resultData; - r.resultExtras = resultExtras; - r.resultAbort = resultAbort; - - // We will process the next receiver right now if this is finishing - // an app receiver (which is always asynchronous) or after we have - // come back from calling a receiver. - return state == BroadcastRecord.APP_RECEIVE - || state == BroadcastRecord.CALL_DONE_RECEIVE; + return r.queue.finishReceiverLocked(r, resultCode, resultData, resultExtras, resultAbort, + explicit); } public void finishReceiver(IBinder who, int resultCode, String resultData, @@ -12732,153 +13361,25 @@ public final class ActivityManagerService extends ActivityManagerNative throw new IllegalArgumentException("File descriptors passed in Bundle"); } - boolean doNext; - final long origId = Binder.clearCallingIdentity(); + try { + boolean doNext = false; + BroadcastRecord r = null; - synchronized(this) { - doNext = finishReceiverLocked( - who, resultCode, resultData, resultExtras, resultAbort, true); - } - - if (doNext) { - processNextBroadcast(false); - } - trimApplications(); - - Binder.restoreCallingIdentity(origId); - } - - private final void logBroadcastReceiverDiscardLocked(BroadcastRecord r) { - if (r.nextReceiver > 0) { - Object curReceiver = r.receivers.get(r.nextReceiver-1); - if (curReceiver instanceof BroadcastFilter) { - BroadcastFilter bf = (BroadcastFilter) curReceiver; - EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_FILTER, - System.identityHashCode(r), - r.intent.getAction(), - r.nextReceiver - 1, - System.identityHashCode(bf)); - } else { - EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_APP, - System.identityHashCode(r), - r.intent.getAction(), - r.nextReceiver - 1, - ((ResolveInfo)curReceiver).toString()); - } - } else { - Slog.w(TAG, "Discarding broadcast before first receiver is invoked: " - + r); - EventLog.writeEvent(EventLogTags.AM_BROADCAST_DISCARD_APP, - System.identityHashCode(r), - r.intent.getAction(), - r.nextReceiver, - "NONE"); - } - } - - private final void setBroadcastTimeoutLocked(long timeoutTime) { - if (! mPendingBroadcastTimeoutMessage) { - Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG); - mHandler.sendMessageAtTime(msg, timeoutTime); - mPendingBroadcastTimeoutMessage = true; - } - } - - private final void cancelBroadcastTimeoutLocked() { - if (mPendingBroadcastTimeoutMessage) { - mHandler.removeMessages(BROADCAST_TIMEOUT_MSG); - mPendingBroadcastTimeoutMessage = false; - } - } - - private final void broadcastTimeoutLocked(boolean fromMsg) { - if (fromMsg) { - mPendingBroadcastTimeoutMessage = false; - } - - if (mOrderedBroadcasts.size() == 0) { - return; - } - - long now = SystemClock.uptimeMillis(); - BroadcastRecord r = mOrderedBroadcasts.get(0); - if (fromMsg) { - if (mDidDexOpt) { - // Delay timeouts until dexopt finishes. - mDidDexOpt = false; - long timeoutTime = SystemClock.uptimeMillis() + BROADCAST_TIMEOUT; - setBroadcastTimeoutLocked(timeoutTime); - return; - } - if (! mProcessesReady) { - // Only process broadcast timeouts if the system is ready. That way - // PRE_BOOT_COMPLETED broadcasts can't timeout as they are intended - // to do heavy lifting for system up. - return; - } - - long timeoutTime = r.receiverTime + BROADCAST_TIMEOUT; - if (timeoutTime > now) { - // We can observe premature timeouts because we do not cancel and reset the - // broadcast timeout message after each receiver finishes. Instead, we set up - // an initial timeout then kick it down the road a little further as needed - // when it expires. - if (DEBUG_BROADCAST) Slog.v(TAG, - "Premature timeout @ " + now + ": resetting BROADCAST_TIMEOUT_MSG for " - + timeoutTime); - setBroadcastTimeoutLocked(timeoutTime); - return; - } - } - - Slog.w(TAG, "Timeout of broadcast " + r + " - receiver=" + r.receiver - + ", started " + (now - r.receiverTime) + "ms ago"); - r.receiverTime = now; - r.anrCount++; - - // Current receiver has passed its expiration date. - if (r.nextReceiver <= 0) { - Slog.w(TAG, "Timeout on receiver with nextReceiver <= 0"); - return; - } - - ProcessRecord app = null; - String anrMessage = null; - - Object curReceiver = r.receivers.get(r.nextReceiver-1); - Slog.w(TAG, "Receiver during timeout: " + curReceiver); - logBroadcastReceiverDiscardLocked(r); - if (curReceiver instanceof BroadcastFilter) { - BroadcastFilter bf = (BroadcastFilter)curReceiver; - if (bf.receiverList.pid != 0 - && bf.receiverList.pid != MY_PID) { - synchronized (this.mPidsSelfLocked) { - app = this.mPidsSelfLocked.get( - bf.receiverList.pid); + synchronized(this) { + r = broadcastRecordForReceiverLocked(who); + if (r != null) { + doNext = r.queue.finishReceiverLocked(r, resultCode, + resultData, resultExtras, resultAbort, true); } } - } else { - app = r.curApp; - } - - if (app != null) { - anrMessage = "Broadcast of " + r.intent.toString(); - } - - if (mPendingBroadcast == r) { - mPendingBroadcast = null; - } - - // Move on to the next receiver. - finishReceiverLocked(r.receiver, r.resultCode, r.resultData, - r.resultExtras, r.resultAbort, true); - scheduleBroadcastsLocked(); - if (anrMessage != null) { - // Post the ANR to the handler since we do not want to process ANRs while - // potentially holding our lock. - mHandler.post(new AppNotResponding(app, anrMessage)); + if (doNext) { + r.queue.processNextBroadcast(false); + } + trimApplications(); + } finally { + Binder.restoreCallingIdentity(origId); } } @@ -13013,333 +13514,6 @@ public final class ActivityManagerService extends ActivityManagerNative } } - private final void addBroadcastToHistoryLocked(BroadcastRecord r) { - if (r.callingUid < 0) { - // This was from a registerReceiver() call; ignore it. - return; - } - System.arraycopy(mBroadcastHistory, 0, mBroadcastHistory, 1, - MAX_BROADCAST_HISTORY-1); - r.finishTime = SystemClock.uptimeMillis(); - mBroadcastHistory[0] = r; - } - - private final void processNextBroadcast(boolean fromMsg) { - synchronized(this) { - BroadcastRecord r; - - if (DEBUG_BROADCAST) Slog.v(TAG, "processNextBroadcast: " - + mParallelBroadcasts.size() + " broadcasts, " - + mOrderedBroadcasts.size() + " ordered broadcasts"); - - updateCpuStats(); - - if (fromMsg) { - mBroadcastsScheduled = false; - } - - // First, deliver any non-serialized broadcasts right away. - while (mParallelBroadcasts.size() > 0) { - r = mParallelBroadcasts.remove(0); - r.dispatchTime = SystemClock.uptimeMillis(); - r.dispatchClockTime = System.currentTimeMillis(); - final int N = r.receivers.size(); - if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Processing parallel broadcast " - + r); - for (int i=0; i<N; i++) { - Object target = r.receivers.get(i); - if (DEBUG_BROADCAST) Slog.v(TAG, - "Delivering non-ordered to registered " - + target + ": " + r); - deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false); - } - addBroadcastToHistoryLocked(r); - if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Done with parallel broadcast " - + r); - } - - // Now take care of the next serialized one... - - // If we are waiting for a process to come up to handle the next - // broadcast, then do nothing at this point. Just in case, we - // check that the process we're waiting for still exists. - if (mPendingBroadcast != null) { - if (DEBUG_BROADCAST_LIGHT) { - Slog.v(TAG, "processNextBroadcast: waiting for " - + mPendingBroadcast.curApp); - } - - boolean isDead; - synchronized (mPidsSelfLocked) { - isDead = (mPidsSelfLocked.get(mPendingBroadcast.curApp.pid) == null); - } - if (!isDead) { - // It's still alive, so keep waiting - return; - } else { - Slog.w(TAG, "pending app " + mPendingBroadcast.curApp - + " died before responding to broadcast"); - mPendingBroadcast.state = BroadcastRecord.IDLE; - mPendingBroadcast.nextReceiver = mPendingBroadcastRecvIndex; - mPendingBroadcast = null; - } - } - - boolean looped = false; - - do { - if (mOrderedBroadcasts.size() == 0) { - // No more broadcasts pending, so all done! - scheduleAppGcsLocked(); - if (looped) { - // If we had finished the last ordered broadcast, then - // make sure all processes have correct oom and sched - // adjustments. - updateOomAdjLocked(); - } - return; - } - r = mOrderedBroadcasts.get(0); - boolean forceReceive = false; - - // Ensure that even if something goes awry with the timeout - // detection, we catch "hung" broadcasts here, discard them, - // and continue to make progress. - // - // This is only done if the system is ready so that PRE_BOOT_COMPLETED - // receivers don't get executed with timeouts. They're intended for - // one time heavy lifting after system upgrades and can take - // significant amounts of time. - int numReceivers = (r.receivers != null) ? r.receivers.size() : 0; - if (mProcessesReady && r.dispatchTime > 0) { - long now = SystemClock.uptimeMillis(); - if ((numReceivers > 0) && - (now > r.dispatchTime + (2*BROADCAST_TIMEOUT*numReceivers))) { - Slog.w(TAG, "Hung broadcast discarded after timeout failure:" - + " now=" + now - + " dispatchTime=" + r.dispatchTime - + " startTime=" + r.receiverTime - + " intent=" + r.intent - + " numReceivers=" + numReceivers - + " nextReceiver=" + r.nextReceiver - + " state=" + r.state); - broadcastTimeoutLocked(false); // forcibly finish this broadcast - forceReceive = true; - r.state = BroadcastRecord.IDLE; - } - } - - if (r.state != BroadcastRecord.IDLE) { - if (DEBUG_BROADCAST) Slog.d(TAG, - "processNextBroadcast() called when not idle (state=" - + r.state + ")"); - return; - } - - if (r.receivers == null || r.nextReceiver >= numReceivers - || r.resultAbort || forceReceive) { - // No more receivers for this broadcast! Send the final - // result if requested... - if (r.resultTo != null) { - try { - if (DEBUG_BROADCAST) { - int seq = r.intent.getIntExtra("seq", -1); - Slog.i(TAG, "Finishing broadcast " + r.intent.getAction() - + " seq=" + seq + " app=" + r.callerApp); - } - performReceiveLocked(r.callerApp, r.resultTo, - new Intent(r.intent), r.resultCode, - r.resultData, r.resultExtras, false, false); - // Set this to null so that the reference - // (local and remote) isnt kept in the mBroadcastHistory. - r.resultTo = null; - } catch (RemoteException e) { - Slog.w(TAG, "Failure sending broadcast result of " + r.intent, e); - } - } - - if (DEBUG_BROADCAST) Slog.v(TAG, "Cancelling BROADCAST_TIMEOUT_MSG"); - cancelBroadcastTimeoutLocked(); - - if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Finished with ordered broadcast " - + r); - - // ... and on to the next... - addBroadcastToHistoryLocked(r); - mOrderedBroadcasts.remove(0); - r = null; - looped = true; - continue; - } - } while (r == null); - - // Get the next receiver... - int recIdx = r.nextReceiver++; - - // Keep track of when this receiver started, and make sure there - // is a timeout message pending to kill it if need be. - r.receiverTime = SystemClock.uptimeMillis(); - if (recIdx == 0) { - r.dispatchTime = r.receiverTime; - r.dispatchClockTime = System.currentTimeMillis(); - if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Processing ordered broadcast " - + r); - } - if (! mPendingBroadcastTimeoutMessage) { - long timeoutTime = r.receiverTime + BROADCAST_TIMEOUT; - if (DEBUG_BROADCAST) Slog.v(TAG, - "Submitting BROADCAST_TIMEOUT_MSG for " + r + " at " + timeoutTime); - setBroadcastTimeoutLocked(timeoutTime); - } - - Object nextReceiver = r.receivers.get(recIdx); - if (nextReceiver instanceof BroadcastFilter) { - // Simple case: this is a registered receiver who gets - // a direct call. - BroadcastFilter filter = (BroadcastFilter)nextReceiver; - if (DEBUG_BROADCAST) Slog.v(TAG, - "Delivering ordered to registered " - + filter + ": " + r); - deliverToRegisteredReceiverLocked(r, filter, r.ordered); - if (r.receiver == null || !r.ordered) { - // The receiver has already finished, so schedule to - // process the next one. - if (DEBUG_BROADCAST) Slog.v(TAG, "Quick finishing: ordered=" - + r.ordered + " receiver=" + r.receiver); - r.state = BroadcastRecord.IDLE; - scheduleBroadcastsLocked(); - } - return; - } - - // Hard case: need to instantiate the receiver, possibly - // starting its application process to host it. - - ResolveInfo info = - (ResolveInfo)nextReceiver; - - boolean skip = false; - int perm = checkComponentPermission(info.activityInfo.permission, - r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid, - info.activityInfo.exported); - if (perm != PackageManager.PERMISSION_GRANTED) { - if (!info.activityInfo.exported) { - Slog.w(TAG, "Permission Denial: broadcasting " - + r.intent.toString() - + " from " + r.callerPackage + " (pid=" + r.callingPid - + ", uid=" + r.callingUid + ")" - + " is not exported from uid " + info.activityInfo.applicationInfo.uid - + " due to receiver " + info.activityInfo.packageName - + "/" + info.activityInfo.name); - } else { - Slog.w(TAG, "Permission Denial: broadcasting " - + r.intent.toString() - + " from " + r.callerPackage + " (pid=" + r.callingPid - + ", uid=" + r.callingUid + ")" - + " requires " + info.activityInfo.permission - + " due to receiver " + info.activityInfo.packageName - + "/" + info.activityInfo.name); - } - skip = true; - } - if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && - r.requiredPermission != null) { - try { - perm = AppGlobals.getPackageManager(). - checkPermission(r.requiredPermission, - info.activityInfo.applicationInfo.packageName); - } catch (RemoteException e) { - perm = PackageManager.PERMISSION_DENIED; - } - if (perm != PackageManager.PERMISSION_GRANTED) { - Slog.w(TAG, "Permission Denial: receiving " - + r.intent + " to " - + info.activityInfo.applicationInfo.packageName - + " requires " + r.requiredPermission - + " due to sender " + r.callerPackage - + " (uid " + r.callingUid + ")"); - skip = true; - } - } - if (r.curApp != null && r.curApp.crashing) { - // If the target process is crashing, just skip it. - if (DEBUG_BROADCAST) Slog.v(TAG, - "Skipping deliver ordered " + r + " to " + r.curApp - + ": process crashing"); - skip = true; - } - - if (skip) { - if (DEBUG_BROADCAST) Slog.v(TAG, - "Skipping delivery of ordered " + r + " for whatever reason"); - r.receiver = null; - r.curFilter = null; - r.state = BroadcastRecord.IDLE; - scheduleBroadcastsLocked(); - return; - } - - r.state = BroadcastRecord.APP_RECEIVE; - String targetProcess = info.activityInfo.processName; - r.curComponent = new ComponentName( - info.activityInfo.applicationInfo.packageName, - info.activityInfo.name); - r.curReceiver = info.activityInfo; - - // Broadcast is being executed, its package can't be stopped. - try { - AppGlobals.getPackageManager().setPackageStoppedState( - r.curComponent.getPackageName(), false); - } catch (RemoteException e) { - } catch (IllegalArgumentException e) { - Slog.w(TAG, "Failed trying to unstop package " - + r.curComponent.getPackageName() + ": " + e); - } - - // Is this receiver's application already running? - ProcessRecord app = getProcessRecordLocked(targetProcess, - info.activityInfo.applicationInfo.uid); - if (app != null && app.thread != null) { - try { - app.addPackage(info.activityInfo.packageName); - processCurBroadcastLocked(r, app); - return; - } catch (RemoteException e) { - Slog.w(TAG, "Exception when sending broadcast to " - + r.curComponent, e); - } - - // If a dead object exception was thrown -- fall through to - // restart the application. - } - - // Not running -- get it started, to be executed when the app comes up. - if (DEBUG_BROADCAST) Slog.v(TAG, - "Need to start app " + targetProcess + " for broadcast " + r); - if ((r.curApp=startProcessLocked(targetProcess, - info.activityInfo.applicationInfo, true, - r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND, - "broadcast", r.curComponent, - (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0)) - == null) { - // Ah, this recipient is unavailable. Finish it if necessary, - // and mark the broadcast record as ready for the next. - Slog.w(TAG, "Unable to launch app " - + info.activityInfo.applicationInfo.packageName + "/" - + info.activityInfo.applicationInfo.uid + " for broadcast " - + r.intent + ": process is bad"); - logBroadcastReceiverDiscardLocked(r); - finishReceiverLocked(r.receiver, r.resultCode, r.resultData, - r.resultExtras, r.resultAbort, true); - scheduleBroadcastsLocked(); - r.state = BroadcastRecord.IDLE; - return; - } - - mPendingBroadcast = r; - mPendingBroadcastRecvIndex = recIdx; - } - } // ========================================================= // INSTRUMENTATION @@ -13663,6 +13837,28 @@ public final class ActivityManagerService extends ActivityManagerNative // LIFETIME MANAGEMENT // ========================================================= + // Returns which broadcast queue the app is the current [or imminent] receiver + // on, or 'null' if the app is not an active broadcast recipient. + private BroadcastQueue isReceivingBroadcast(ProcessRecord app) { + BroadcastRecord r = app.curReceiver; + if (r != null) { + return r.queue; + } + + // It's not the current receiver, but it might be starting up to become one + synchronized (this) { + for (BroadcastQueue queue : mBroadcastQueues) { + r = queue.mPendingBroadcast; + if (r != null && r.curApp == app) { + // found it; report which queue it's in + return queue; + } + } + } + + return null; + } + private final int computeOomAdjLocked(ProcessRecord app, int hiddenAdj, ProcessRecord TOP_APP, boolean recursed, boolean doingAll) { if (mAdjSeq == app.adjSeq) { @@ -13728,6 +13924,7 @@ public final class ActivityManagerService extends ActivityManagerNative // important to least, and assign an appropriate OOM adjustment. int adj; int schedGroup; + BroadcastQueue queue; if (app == TOP_APP) { // The last app on the list is the foreground app. adj = ProcessList.FOREGROUND_APP_ADJ; @@ -13739,12 +13936,14 @@ public final class ActivityManagerService extends ActivityManagerNative adj = ProcessList.FOREGROUND_APP_ADJ; schedGroup = Process.THREAD_GROUP_DEFAULT; app.adjType = "instrumentation"; - } else if (app.curReceiver != null || - (mPendingBroadcast != null && mPendingBroadcast.curApp == app)) { + } else if ((queue = isReceivingBroadcast(app)) != null) { // An app that is currently receiving a broadcast also - // counts as being in the foreground. + // counts as being in the foreground for OOM killer purposes. + // It's placed in a sched group based on the nature of the + // broadcast as reflected by which queue it's active in. adj = ProcessList.FOREGROUND_APP_ADJ; - schedGroup = Process.THREAD_GROUP_DEFAULT; + schedGroup = (queue == mFgBroadcastQueue) + ? Process.THREAD_GROUP_DEFAULT : Process.THREAD_GROUP_BG_NONINTERACTIVE; app.adjType = "broadcast"; } else if (app.executingServices.size() > 0) { // An app that is currently executing a service callback also @@ -14185,8 +14384,13 @@ public final class ActivityManagerService extends ActivityManagerNative * Returns true if things are idle enough to perform GCs. */ private final boolean canGcNowLocked() { - return mParallelBroadcasts.size() == 0 - && mOrderedBroadcasts.size() == 0 + boolean processingBroadcasts = false; + for (BroadcastQueue q : mBroadcastQueues) { + if (q.mParallelBroadcasts.size() != 0 || q.mOrderedBroadcasts.size() != 0) { + processingBroadcasts = true; + } + } + return !processingBroadcasts && (mSleeping || (mMainStack.mResumedActivity != null && mMainStack.mResumedActivity.idle)); } diff --git a/services/java/com/android/server/am/BroadcastRecord.java b/services/java/com/android/server/am/BroadcastRecord.java index bcb013404ea9..6738e4fc76c0 100644 --- a/services/java/com/android/server/am/BroadcastRecord.java +++ b/services/java/com/android/server/am/BroadcastRecord.java @@ -59,6 +59,7 @@ class BroadcastRecord extends Binder { IBinder receiver; // who is currently running, null if none. int state; int anrCount; // has this broadcast record hit any ANRs? + ActivityManagerService.BroadcastQueue queue; // the outbound queue handling this broadcast static final int IDLE = 0; static final int APP_RECEIVE = 1; @@ -161,11 +162,13 @@ class BroadcastRecord extends Binder { } } - BroadcastRecord(Intent _intent, ProcessRecord _callerApp, String _callerPackage, + BroadcastRecord(ActivityManagerService.BroadcastQueue _queue, + Intent _intent, ProcessRecord _callerApp, String _callerPackage, int _callingPid, int _callingUid, String _requiredPermission, List _receivers, IIntentReceiver _resultTo, int _resultCode, String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky, boolean _initialSticky) { + queue = _queue; intent = _intent; callerApp = _callerApp; callerPackage = _callerPackage; diff --git a/tests/RenderScriptTests/tests/src/com/android/rs/test/RSTestCore.java b/tests/RenderScriptTests/tests/src/com/android/rs/test/RSTestCore.java index 451d53794689..22936786c9ac 100644 --- a/tests/RenderScriptTests/tests/src/com/android/rs/test/RSTestCore.java +++ b/tests/RenderScriptTests/tests/src/com/android/rs/test/RSTestCore.java @@ -65,6 +65,7 @@ public class RSTestCore { unitTests = new ArrayList<UnitTest>(); unitTests.add(new UT_primitives(this, mRes, mCtx)); + unitTests.add(new UT_constant(this, mRes, mCtx)); unitTests.add(new UT_vector(this, mRes, mCtx)); unitTests.add(new UT_rsdebug(this, mRes, mCtx)); unitTests.add(new UT_rstime(this, mRes, mCtx)); @@ -93,7 +94,7 @@ public class RSTestCore { for (int i = 0; i < uta.length; i++) { ScriptField_ListAllocs_s.Item listElem = new ScriptField_ListAllocs_s.Item(); listElem.text = Allocation.createFromString(mRS, uta[i].name, Allocation.USAGE_SCRIPT); - listElem.result = uta[i].result; + listElem.result = uta[i].getResult(); mListAllocs.set(listElem, i, false); uta[i].setItem(listElem); } diff --git a/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_constant.java b/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_constant.java new file mode 100644 index 000000000000..adda5a3cbb81 --- /dev/null +++ b/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_constant.java @@ -0,0 +1,57 @@ +/* + * 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 com.android.rs.test; + +import android.content.Context; +import android.content.res.Resources; +import android.renderscript.*; + +public class UT_constant extends UnitTest { + private Resources mRes; + + protected UT_constant(RSTestCore rstc, Resources res, Context ctx) { + super(rstc, "Const", ctx); + mRes = res; + } + + private void Assert(boolean b) { + if (!b) { + failTest(); + } + } + + public void run() { + Assert(ScriptC_constant.const_floatTest == 1.99f); + Assert(ScriptC_constant.const_doubleTest == 2.05); + Assert(ScriptC_constant.const_charTest == -8); + Assert(ScriptC_constant.const_shortTest == -16); + Assert(ScriptC_constant.const_intTest == -32); + Assert(ScriptC_constant.const_longTest == 17179869184l); + Assert(ScriptC_constant.const_longlongTest == 68719476736l); + + Assert(ScriptC_constant.const_ucharTest == 8); + Assert(ScriptC_constant.const_ushortTest == 16); + Assert(ScriptC_constant.const_uintTest == 32); + Assert(ScriptC_constant.const_ulongTest == 4611686018427387904L); + Assert(ScriptC_constant.const_int64_tTest == -17179869184l); + Assert(ScriptC_constant.const_uint64_tTest == 117179869184l); + + Assert(ScriptC_constant.const_boolTest == true); + + passTest(); + } +} diff --git a/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_primitives.java b/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_primitives.java index b7a65a57951e..18829c202e43 100644 --- a/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_primitives.java +++ b/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_primitives.java @@ -92,8 +92,7 @@ public class UT_primitives extends UnitTest { ScriptC_primitives s = new ScriptC_primitives(pRS, mRes, R.raw.primitives); pRS.setMessageHandler(mRsMessage); if (!initializeGlobals(s)) { - // initializeGlobals failed - result = -1; + failTest(); } else { s.invoke_primitives_test(0, 0); pRS.finish(); diff --git a/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_vector.java b/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_vector.java index 748701dd4e0a..0ac09ca5650c 100644 --- a/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_vector.java +++ b/tests/RenderScriptTests/tests/src/com/android/rs/test/UT_vector.java @@ -307,7 +307,7 @@ public class UT_vector extends UnitTest { ScriptC_vector s = new ScriptC_vector(pRS, mRes, R.raw.vector); pRS.setMessageHandler(mRsMessage); if (!initializeGlobals(s)) { - result = -1; + failTest(); } else { s.invoke_vector_test(); pRS.finish(); diff --git a/tests/RenderScriptTests/tests/src/com/android/rs/test/UnitTest.java b/tests/RenderScriptTests/tests/src/com/android/rs/test/UnitTest.java index a97ffa7d47ab..edff83f2756b 100644 --- a/tests/RenderScriptTests/tests/src/com/android/rs/test/UnitTest.java +++ b/tests/RenderScriptTests/tests/src/com/android/rs/test/UnitTest.java @@ -21,7 +21,7 @@ import android.renderscript.RenderScript.RSMessageHandler; public class UnitTest extends Thread { public String name; - public int result; + private int result; private ScriptField_ListAllocs_s.Item mItem; private RSTestCore mRSTC; private boolean msgHandled; @@ -63,7 +63,7 @@ public class UnitTest extends Thread { } } - protected void updateUI() { + private void updateUI() { if (mItem != null) { mItem.result = result; msgHandled = true; @@ -104,6 +104,22 @@ public class UnitTest extends Thread { } } + public int getResult() { + return result; + } + + public void failTest() { + result = -1; + updateUI(); + } + + public void passTest() { + if (result != -1) { + result = 1; + } + updateUI(); + } + public void setItem(ScriptField_ListAllocs_s.Item item) { mItem = item; } diff --git a/tests/RenderScriptTests/tests/src/com/android/rs/test/constant.rs b/tests/RenderScriptTests/tests/src/com/android/rs/test/constant.rs new file mode 100644 index 000000000000..732eaefa6bb8 --- /dev/null +++ b/tests/RenderScriptTests/tests/src/com/android/rs/test/constant.rs @@ -0,0 +1,19 @@ +#include "shared.rsh" + +const float floatTest = 1.99f; +const double doubleTest = 2.05; +const char charTest = -8; +const short shortTest = -16; +const int intTest = -32; +const long longTest = 17179869184l; // 1 << 34 +const long long longlongTest = 68719476736l; // 1 << 36 + +const uchar ucharTest = 8; +const ushort ushortTest = 16; +const uint uintTest = 32; +const ulong ulongTest = 4611686018427387904L; +const int64_t int64_tTest = -17179869184l; // - 1 << 34 +const uint64_t uint64_tTest = 117179869184l; + +const bool boolTest = true; + |